asciidoc_parser/blocks/
media.rs

1use crate::{
2    HasSpan, Parser, Span,
3    attributes::{Attrlist, AttrlistContext},
4    blocks::{ContentModel, IsBlock, metadata::BlockMetadata},
5    span::MatchedItem,
6    strings::CowStr,
7    warnings::{MatchAndWarnings, Warning, WarningType},
8};
9
10/// A media block is used to represent an image, video, or audio block macro.
11#[derive(Clone, Debug, Eq, PartialEq)]
12pub struct MediaBlock<'src> {
13    type_: MediaType,
14    target: Span<'src>,
15    macro_attrlist: Attrlist<'src>,
16    source: Span<'src>,
17    title_source: Option<Span<'src>>,
18    title: Option<String>,
19    anchor: Option<Span<'src>>,
20    anchor_reftext: Option<Span<'src>>,
21    attrlist: Option<Attrlist<'src>>,
22}
23
24/// A media type may be one of three different types.
25#[derive(Clone, Copy, Eq, PartialEq)]
26pub enum MediaType {
27    /// Still image
28    Image,
29
30    /// Video
31    Video,
32
33    /// Audio
34    Audio,
35}
36
37impl std::fmt::Debug for MediaType {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        match self {
40            MediaType::Image => write!(f, "MediaType::Image"),
41            MediaType::Video => write!(f, "MediaType::Video"),
42            MediaType::Audio => write!(f, "MediaType::Audio"),
43        }
44    }
45}
46
47impl<'src> MediaBlock<'src> {
48    pub(crate) fn parse(
49        metadata: &BlockMetadata<'src>,
50        parser: &mut Parser,
51    ) -> MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>> {
52        let line = metadata.block_start.take_normalized_line();
53
54        // Line must end with `]`; otherwise, it's not a block macro.
55        if !line.item.ends_with(']') {
56            return MatchAndWarnings {
57                item: None,
58                warnings: vec![],
59            };
60        }
61
62        let Some(name) = line.item.take_ident() else {
63            return MatchAndWarnings {
64                item: None,
65                warnings: vec![],
66            };
67        };
68
69        let type_ = match name.item.data() {
70            "image" => MediaType::Image,
71            "video" => MediaType::Video,
72            "audio" => MediaType::Audio,
73            _ => {
74                return MatchAndWarnings {
75                    item: None,
76                    warnings: vec![],
77                };
78            }
79        };
80
81        let Some(colons) = name.after.take_prefix("::") else {
82            return MatchAndWarnings {
83                item: None,
84                warnings: vec![Warning {
85                    source: name.after,
86                    warning: WarningType::MacroMissingDoubleColon,
87                }],
88            };
89        };
90
91        // The target field must exist and be non-empty.
92        let target = colons.after.take_while(|c| c != '[');
93
94        if target.item.is_empty() {
95            return MatchAndWarnings {
96                item: None,
97                warnings: vec![Warning {
98                    source: target.after,
99                    warning: WarningType::MediaMacroMissingTarget,
100                }],
101            };
102        }
103
104        let Some(open_brace) = target.after.take_prefix("[") else {
105            return MatchAndWarnings {
106                item: None,
107                warnings: vec![Warning {
108                    source: target.after,
109                    warning: WarningType::MacroMissingAttributeList,
110                }],
111            };
112        };
113
114        let attrlist = open_brace.after.slice(0..open_brace.after.len() - 1);
115        // Note that we already checked that this line ends with a close brace.
116
117        let macro_attrlist = Attrlist::parse(attrlist, parser, AttrlistContext::Inline);
118
119        let source: Span = metadata.source.trim_remainder(line.after);
120        let source = source.slice(0..source.trim().len());
121
122        MatchAndWarnings {
123            item: Some(MatchedItem {
124                item: Self {
125                    type_,
126                    target: target.item,
127                    macro_attrlist: macro_attrlist.item.item,
128                    source,
129                    title_source: metadata.title_source,
130                    title: metadata.title.clone(),
131                    anchor: metadata.anchor,
132                    anchor_reftext: None,
133                    attrlist: metadata.attrlist.clone(),
134                },
135
136                after: line.after.discard_empty_lines(),
137            }),
138            warnings: macro_attrlist.warnings,
139        }
140    }
141
142    /// Return a [`Span`] describing the macro name.
143    pub fn type_(&self) -> MediaType {
144        self.type_
145    }
146
147    /// Return a [`Span`] describing the macro target.
148    pub fn target(&'src self) -> Option<&'src Span<'src>> {
149        Some(&self.target)
150    }
151
152    /// Return the macro's attribute list.
153    ///
154    /// **IMPORTANT:** This is the list of attributes _within_ the macro block
155    /// definition itself.
156    ///
157    /// See also [`attrlist()`] for attributes that can be defined before the
158    /// macro invocation.
159    ///
160    /// [`attrlist()`]: Self::attrlist()
161    pub fn macro_attrlist(&'src self) -> &'src Attrlist<'src> {
162        &self.macro_attrlist
163    }
164}
165
166impl<'src> IsBlock<'src> for MediaBlock<'src> {
167    fn content_model(&self) -> ContentModel {
168        ContentModel::Empty
169    }
170
171    fn raw_context(&self) -> CowStr<'src> {
172        match self.type_ {
173            MediaType::Audio => "audio",
174            MediaType::Image => "image",
175            MediaType::Video => "video",
176        }
177        .into()
178    }
179
180    fn title_source(&'src self) -> Option<Span<'src>> {
181        self.title_source
182    }
183
184    fn title(&self) -> Option<&str> {
185        self.title.as_deref()
186    }
187
188    fn anchor(&'src self) -> Option<Span<'src>> {
189        self.anchor
190    }
191
192    fn anchor_reftext(&'src self) -> Option<Span<'src>> {
193        self.anchor_reftext
194    }
195
196    fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
197        self.attrlist.as_ref()
198    }
199}
200
201impl<'src> HasSpan<'src> for MediaBlock<'src> {
202    fn span(&self) -> Span<'src> {
203        self.source
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    #![allow(clippy::unwrap_used)]
210
211    use std::ops::Deref;
212
213    use pretty_assertions_sorted::assert_eq;
214
215    use crate::{
216        Parser,
217        blocks::{ContentModel, IsBlock, MediaType, metadata::BlockMetadata},
218        content::SubstitutionGroup,
219        tests::prelude::*,
220        warnings::WarningType,
221    };
222
223    #[test]
224    fn impl_clone() {
225        // Silly test to mark the #[derive(...)] line as covered.
226        let mut parser = Parser::default();
227
228        let b1 =
229            crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::foo.jpg[]"), &mut parser)
230                .unwrap_if_no_warnings()
231                .unwrap()
232                .item;
233
234        let b2 = b1.clone();
235        assert_eq!(b1, b2);
236    }
237
238    #[test]
239    fn err_empty_source() {
240        let mut parser = Parser::default();
241        assert!(
242            crate::blocks::MediaBlock::parse(&BlockMetadata::new(""), &mut parser)
243                .unwrap_if_no_warnings()
244                .is_none()
245        );
246    }
247
248    #[test]
249    fn err_only_spaces() {
250        let mut parser = Parser::default();
251        assert!(
252            crate::blocks::MediaBlock::parse(&BlockMetadata::new("    "), &mut parser)
253                .unwrap_if_no_warnings()
254                .is_none()
255        );
256    }
257
258    #[test]
259    fn err_macro_name_not_ident() {
260        let mut parser = Parser::default();
261        let maw = crate::blocks::MediaBlock::parse(
262            &BlockMetadata::new("98xyz::bar[blah,blap]"),
263            &mut parser,
264        );
265
266        assert!(maw.item.is_none());
267        assert!(maw.warnings.is_empty());
268    }
269
270    #[test]
271    fn err_missing_double_colon() {
272        let mut parser = Parser::default();
273        let maw = crate::blocks::MediaBlock::parse(
274            &BlockMetadata::new("image:bar[blah,blap]"),
275            &mut parser,
276        );
277
278        assert!(maw.item.is_none());
279
280        assert_eq!(
281            maw.warnings,
282            vec![Warning {
283                source: Span {
284                    data: ":bar[blah,blap]",
285                    line: 1,
286                    col: 6,
287                    offset: 5,
288                },
289                warning: WarningType::MacroMissingDoubleColon,
290            }]
291        );
292    }
293
294    #[test]
295    fn err_missing_macro_attrlist() {
296        let mut parser = Parser::default();
297        let maw = crate::blocks::MediaBlock::parse(
298            &BlockMetadata::new("image::barblah,blap]"),
299            &mut parser,
300        );
301
302        assert!(maw.item.is_none());
303
304        assert_eq!(
305            maw.warnings,
306            vec![Warning {
307                source: Span {
308                    data: "",
309                    line: 1,
310                    col: 21,
311                    offset: 20,
312                },
313                warning: WarningType::MacroMissingAttributeList,
314            }]
315        );
316    }
317
318    #[test]
319    fn err_unknown_type() {
320        let mut parser = Parser::default();
321        assert!(
322            crate::blocks::MediaBlock::parse(&BlockMetadata::new("imagex::bar[]"), &mut parser)
323                .unwrap_if_no_warnings()
324                .is_none()
325        );
326    }
327
328    #[test]
329    fn err_no_attr_list() {
330        let mut parser = Parser::default();
331        assert!(
332            crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar"), &mut parser)
333                .unwrap_if_no_warnings()
334                .is_none()
335        );
336    }
337
338    #[test]
339    fn err_attr_list_not_closed() {
340        let mut parser = Parser::default();
341        assert!(
342            crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar[blah"), &mut parser)
343                .unwrap_if_no_warnings()
344                .is_none()
345        );
346    }
347
348    #[test]
349    fn err_unexpected_after_attr_list() {
350        let mut parser = Parser::default();
351        assert!(
352            crate::blocks::MediaBlock::parse(
353                &BlockMetadata::new("image::bar[blah]bonus"),
354                &mut parser
355            )
356            .unwrap_if_no_warnings()
357            .is_none()
358        );
359    }
360
361    #[test]
362    fn simplest_block_macro() {
363        let mut parser = Parser::default();
364
365        let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::[]"), &mut parser);
366        assert!(mi.item.is_none());
367
368        assert_eq!(
369            mi.warnings,
370            vec![Warning {
371                source: Span {
372                    data: "[]",
373                    line: 1,
374                    col: 8,
375                    offset: 7,
376                },
377                warning: WarningType::MediaMacroMissingTarget,
378            }]
379        );
380    }
381
382    #[test]
383    fn has_target() {
384        let mut parser = Parser::default();
385
386        let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar[]"), &mut parser)
387            .unwrap_if_no_warnings()
388            .unwrap();
389
390        assert_eq!(
391            mi.item,
392            MediaBlock {
393                type_: MediaType::Image,
394                target: Span {
395                    data: "bar",
396                    line: 1,
397                    col: 8,
398                    offset: 7,
399                },
400                macro_attrlist: Attrlist {
401                    attributes: &[],
402                    anchor: None,
403                    source: Span {
404                        data: "",
405                        line: 1,
406                        col: 12,
407                        offset: 11,
408                    }
409                },
410                source: Span {
411                    data: "image::bar[]",
412                    line: 1,
413                    col: 1,
414                    offset: 0,
415                },
416                title_source: None,
417                title: None,
418                anchor: None,
419                anchor_reftext: None,
420                attrlist: None,
421            }
422        );
423
424        assert_eq!(
425            mi.after,
426            Span {
427                data: "",
428                line: 1,
429                col: 13,
430                offset: 12
431            }
432        );
433
434        assert_eq!(mi.item.content_model(), ContentModel::Empty);
435        assert_eq!(mi.item.raw_context().deref(), "image");
436        assert!(mi.item.nested_blocks().next().is_none());
437        assert!(mi.item.title_source().is_none());
438        assert!(mi.item.title().is_none());
439        assert!(mi.item.anchor().is_none());
440        assert!(mi.item.anchor_reftext().is_none());
441        assert!(mi.item.attrlist().is_none());
442        assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
443    }
444
445    #[test]
446    fn has_target_and_attrlist() {
447        let mut parser = Parser::default();
448
449        let mi =
450            crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar[blah]"), &mut parser)
451                .unwrap_if_no_warnings()
452                .unwrap();
453
454        assert_eq!(
455            mi.item,
456            MediaBlock {
457                type_: MediaType::Image,
458                target: Span {
459                    data: "bar",
460                    line: 1,
461                    col: 8,
462                    offset: 7,
463                },
464                macro_attrlist: Attrlist {
465                    attributes: &[ElementAttribute {
466                        name: None,
467                        shorthand_items: &["blah"],
468                        value: "blah"
469                    }],
470                    anchor: None,
471                    source: Span {
472                        data: "blah",
473                        line: 1,
474                        col: 12,
475                        offset: 11,
476                    }
477                },
478                source: Span {
479                    data: "image::bar[blah]",
480                    line: 1,
481                    col: 1,
482                    offset: 0,
483                },
484                title_source: None,
485                title: None,
486                anchor: None,
487                anchor_reftext: None,
488                attrlist: None,
489            }
490        );
491
492        assert_eq!(
493            mi.after,
494            Span {
495                data: "",
496                line: 1,
497                col: 17,
498                offset: 16
499            }
500        );
501    }
502
503    #[test]
504    fn audio() {
505        let mut parser = Parser::default();
506
507        let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("audio::bar[]"), &mut parser)
508            .unwrap_if_no_warnings()
509            .unwrap();
510
511        assert_eq!(
512            mi.item,
513            MediaBlock {
514                type_: MediaType::Audio,
515                target: Span {
516                    data: "bar",
517                    line: 1,
518                    col: 8,
519                    offset: 7,
520                },
521                macro_attrlist: Attrlist {
522                    attributes: &[],
523                    anchor: None,
524                    source: Span {
525                        data: "",
526                        line: 1,
527                        col: 12,
528                        offset: 11,
529                    }
530                },
531                source: Span {
532                    data: "audio::bar[]",
533                    line: 1,
534                    col: 1,
535                    offset: 0,
536                },
537                title_source: None,
538                title: None,
539                anchor: None,
540                anchor_reftext: None,
541                attrlist: None,
542            }
543        );
544
545        assert_eq!(
546            mi.after,
547            Span {
548                data: "",
549                line: 1,
550                col: 13,
551                offset: 12
552            }
553        );
554
555        assert_eq!(mi.item.content_model(), ContentModel::Empty);
556        assert_eq!(mi.item.raw_context().deref(), "audio");
557        assert!(mi.item.nested_blocks().next().is_none());
558        assert!(mi.item.title_source().is_none());
559        assert!(mi.item.title().is_none());
560        assert!(mi.item.anchor().is_none());
561        assert!(mi.item.anchor_reftext().is_none());
562        assert!(mi.item.attrlist().is_none());
563        assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
564    }
565
566    #[test]
567    fn video() {
568        let mut parser = Parser::default();
569
570        let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("video::bar[]"), &mut parser)
571            .unwrap_if_no_warnings()
572            .unwrap();
573
574        assert_eq!(
575            mi.item,
576            MediaBlock {
577                type_: MediaType::Video,
578                target: Span {
579                    data: "bar",
580                    line: 1,
581                    col: 8,
582                    offset: 7,
583                },
584                macro_attrlist: Attrlist {
585                    attributes: &[],
586                    anchor: None,
587                    source: Span {
588                        data: "",
589                        line: 1,
590                        col: 12,
591                        offset: 11,
592                    }
593                },
594                source: Span {
595                    data: "video::bar[]",
596                    line: 1,
597                    col: 1,
598                    offset: 0,
599                },
600                title_source: None,
601                title: None,
602                anchor: None,
603                anchor_reftext: None,
604                attrlist: None,
605            }
606        );
607
608        assert_eq!(
609            mi.after,
610            Span {
611                data: "",
612                line: 1,
613                col: 13,
614                offset: 12
615            }
616        );
617
618        assert_eq!(mi.item.content_model(), ContentModel::Empty);
619        assert_eq!(mi.item.raw_context().deref(), "video");
620        assert!(mi.item.nested_blocks().next().is_none());
621        assert!(mi.item.title_source().is_none());
622        assert!(mi.item.title().is_none());
623        assert!(mi.item.anchor().is_none());
624        assert!(mi.item.anchor_reftext().is_none());
625        assert!(mi.item.attrlist().is_none());
626        assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
627    }
628
629    #[test]
630    fn err_duplicate_comma() {
631        let mut parser = Parser::default();
632        let maw = crate::blocks::MediaBlock::parse(
633            &BlockMetadata::new("image::bar[blah,,blap]"),
634            &mut parser,
635        );
636
637        let mi = maw.item.unwrap().clone();
638
639        assert_eq!(
640            mi.item,
641            MediaBlock {
642                type_: MediaType::Image,
643                target: Span {
644                    data: "bar",
645                    line: 1,
646                    col: 8,
647                    offset: 7,
648                },
649                macro_attrlist: Attrlist {
650                    attributes: &[
651                        ElementAttribute {
652                            name: None,
653                            shorthand_items: &["blah"],
654                            value: "blah"
655                        },
656                        ElementAttribute {
657                            name: None,
658                            shorthand_items: &[],
659                            value: "blap"
660                        }
661                    ],
662                    anchor: None,
663                    source: Span {
664                        data: "blah,,blap",
665                        line: 1,
666                        col: 12,
667                        offset: 11,
668                    }
669                },
670                source: Span {
671                    data: "image::bar[blah,,blap]",
672                    line: 1,
673                    col: 1,
674                    offset: 0,
675                },
676                title_source: None,
677                title: None,
678                anchor: None,
679                anchor_reftext: None,
680                attrlist: None,
681            }
682        );
683
684        assert_eq!(
685            mi.after,
686            Span {
687                data: "",
688                line: 1,
689                col: 23,
690                offset: 22
691            }
692        );
693
694        assert_eq!(
695            maw.warnings,
696            vec![Warning {
697                source: Span {
698                    data: "blah,,blap",
699                    line: 1,
700                    col: 12,
701                    offset: 11,
702                },
703                warning: WarningType::EmptyAttributeValue,
704            }]
705        );
706    }
707
708    mod media_type {
709        mod impl_debug {
710            use pretty_assertions_sorted::assert_eq;
711
712            use crate::blocks::MediaType;
713
714            #[test]
715            fn image() {
716                let media_type = MediaType::Image;
717                let debug_output = format!("{:?}", media_type);
718                assert_eq!(debug_output, "MediaType::Image");
719            }
720
721            #[test]
722            fn video() {
723                let media_type = MediaType::Video;
724                let debug_output = format!("{:?}", media_type);
725                assert_eq!(debug_output, "MediaType::Video");
726            }
727
728            #[test]
729            fn audio() {
730                let media_type = MediaType::Audio;
731                let debug_output = format!("{:?}", media_type);
732                assert_eq!(debug_output, "MediaType::Audio");
733            }
734        }
735    }
736}