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    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 crate::{
214        blocks::{ContentModel, MediaType, metadata::BlockMetadata},
215        tests::prelude::*,
216    };
217
218    #[test]
219    fn impl_clone() {
220        // Silly test to mark the #[derive(...)] line as covered.
221        let mut parser = Parser::default();
222
223        let b1 =
224            crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::foo.jpg[]"), &mut parser)
225                .unwrap_if_no_warnings()
226                .unwrap()
227                .item;
228
229        let b2 = b1.clone();
230        assert_eq!(b1, b2);
231    }
232
233    #[test]
234    fn err_empty_source() {
235        let mut parser = Parser::default();
236        assert!(
237            crate::blocks::MediaBlock::parse(&BlockMetadata::new(""), &mut parser)
238                .unwrap_if_no_warnings()
239                .is_none()
240        );
241    }
242
243    #[test]
244    fn err_only_spaces() {
245        let mut parser = Parser::default();
246        assert!(
247            crate::blocks::MediaBlock::parse(&BlockMetadata::new("    "), &mut parser)
248                .unwrap_if_no_warnings()
249                .is_none()
250        );
251    }
252
253    #[test]
254    fn err_macro_name_not_ident() {
255        let mut parser = Parser::default();
256        let maw = crate::blocks::MediaBlock::parse(
257            &BlockMetadata::new("98xyz::bar[blah,blap]"),
258            &mut parser,
259        );
260
261        assert!(maw.item.is_none());
262        assert!(maw.warnings.is_empty());
263    }
264
265    #[test]
266    fn err_missing_double_colon() {
267        let mut parser = Parser::default();
268        let maw = crate::blocks::MediaBlock::parse(
269            &BlockMetadata::new("image:bar[blah,blap]"),
270            &mut parser,
271        );
272
273        assert!(maw.item.is_none());
274
275        assert_eq!(
276            maw.warnings,
277            vec![Warning {
278                source: Span {
279                    data: ":bar[blah,blap]",
280                    line: 1,
281                    col: 6,
282                    offset: 5,
283                },
284                warning: WarningType::MacroMissingDoubleColon,
285            }]
286        );
287    }
288
289    #[test]
290    fn err_missing_macro_attrlist() {
291        let mut parser = Parser::default();
292        let maw = crate::blocks::MediaBlock::parse(
293            &BlockMetadata::new("image::barblah,blap]"),
294            &mut parser,
295        );
296
297        assert!(maw.item.is_none());
298
299        assert_eq!(
300            maw.warnings,
301            vec![Warning {
302                source: Span {
303                    data: "",
304                    line: 1,
305                    col: 21,
306                    offset: 20,
307                },
308                warning: WarningType::MacroMissingAttributeList,
309            }]
310        );
311    }
312
313    #[test]
314    fn err_unknown_type() {
315        let mut parser = Parser::default();
316        assert!(
317            crate::blocks::MediaBlock::parse(&BlockMetadata::new("imagex::bar[]"), &mut parser)
318                .unwrap_if_no_warnings()
319                .is_none()
320        );
321    }
322
323    #[test]
324    fn err_no_attr_list() {
325        let mut parser = Parser::default();
326        assert!(
327            crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar"), &mut parser)
328                .unwrap_if_no_warnings()
329                .is_none()
330        );
331    }
332
333    #[test]
334    fn err_attr_list_not_closed() {
335        let mut parser = Parser::default();
336        assert!(
337            crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar[blah"), &mut parser)
338                .unwrap_if_no_warnings()
339                .is_none()
340        );
341    }
342
343    #[test]
344    fn err_unexpected_after_attr_list() {
345        let mut parser = Parser::default();
346        assert!(
347            crate::blocks::MediaBlock::parse(
348                &BlockMetadata::new("image::bar[blah]bonus"),
349                &mut parser
350            )
351            .unwrap_if_no_warnings()
352            .is_none()
353        );
354    }
355
356    #[test]
357    fn simplest_block_macro() {
358        let mut parser = Parser::default();
359
360        let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::[]"), &mut parser);
361        assert!(mi.item.is_none());
362
363        assert_eq!(
364            mi.warnings,
365            vec![Warning {
366                source: Span {
367                    data: "[]",
368                    line: 1,
369                    col: 8,
370                    offset: 7,
371                },
372                warning: WarningType::MediaMacroMissingTarget,
373            }]
374        );
375    }
376
377    #[test]
378    fn has_target() {
379        let mut parser = Parser::default();
380
381        let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar[]"), &mut parser)
382            .unwrap_if_no_warnings()
383            .unwrap();
384
385        assert_eq!(
386            mi.item,
387            MediaBlock {
388                type_: MediaType::Image,
389                target: Span {
390                    data: "bar",
391                    line: 1,
392                    col: 8,
393                    offset: 7,
394                },
395                macro_attrlist: Attrlist {
396                    attributes: &[],
397                    anchor: None,
398                    source: Span {
399                        data: "",
400                        line: 1,
401                        col: 12,
402                        offset: 11,
403                    }
404                },
405                source: Span {
406                    data: "image::bar[]",
407                    line: 1,
408                    col: 1,
409                    offset: 0,
410                },
411                title_source: None,
412                title: None,
413                anchor: None,
414                anchor_reftext: None,
415                attrlist: None,
416            }
417        );
418
419        assert_eq!(
420            mi.after,
421            Span {
422                data: "",
423                line: 1,
424                col: 13,
425                offset: 12
426            }
427        );
428
429        assert_eq!(mi.item.content_model(), ContentModel::Empty);
430        assert_eq!(mi.item.raw_context().deref(), "image");
431        assert!(mi.item.nested_blocks().next().is_none());
432        assert!(mi.item.title_source().is_none());
433        assert!(mi.item.title().is_none());
434        assert!(mi.item.anchor().is_none());
435        assert!(mi.item.anchor_reftext().is_none());
436        assert!(mi.item.attrlist().is_none());
437        assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
438    }
439
440    #[test]
441    fn has_target_and_attrlist() {
442        let mut parser = Parser::default();
443
444        let mi =
445            crate::blocks::MediaBlock::parse(&BlockMetadata::new("image::bar[blah]"), &mut parser)
446                .unwrap_if_no_warnings()
447                .unwrap();
448
449        assert_eq!(
450            mi.item,
451            MediaBlock {
452                type_: MediaType::Image,
453                target: Span {
454                    data: "bar",
455                    line: 1,
456                    col: 8,
457                    offset: 7,
458                },
459                macro_attrlist: Attrlist {
460                    attributes: &[ElementAttribute {
461                        name: None,
462                        shorthand_items: &["blah"],
463                        value: "blah"
464                    }],
465                    anchor: None,
466                    source: Span {
467                        data: "blah",
468                        line: 1,
469                        col: 12,
470                        offset: 11,
471                    }
472                },
473                source: Span {
474                    data: "image::bar[blah]",
475                    line: 1,
476                    col: 1,
477                    offset: 0,
478                },
479                title_source: None,
480                title: None,
481                anchor: None,
482                anchor_reftext: None,
483                attrlist: None,
484            }
485        );
486
487        assert_eq!(
488            mi.after,
489            Span {
490                data: "",
491                line: 1,
492                col: 17,
493                offset: 16
494            }
495        );
496    }
497
498    #[test]
499    fn audio() {
500        let mut parser = Parser::default();
501
502        let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("audio::bar[]"), &mut parser)
503            .unwrap_if_no_warnings()
504            .unwrap();
505
506        assert_eq!(
507            mi.item,
508            MediaBlock {
509                type_: MediaType::Audio,
510                target: Span {
511                    data: "bar",
512                    line: 1,
513                    col: 8,
514                    offset: 7,
515                },
516                macro_attrlist: Attrlist {
517                    attributes: &[],
518                    anchor: None,
519                    source: Span {
520                        data: "",
521                        line: 1,
522                        col: 12,
523                        offset: 11,
524                    }
525                },
526                source: Span {
527                    data: "audio::bar[]",
528                    line: 1,
529                    col: 1,
530                    offset: 0,
531                },
532                title_source: None,
533                title: None,
534                anchor: None,
535                anchor_reftext: None,
536                attrlist: None,
537            }
538        );
539
540        assert_eq!(
541            mi.after,
542            Span {
543                data: "",
544                line: 1,
545                col: 13,
546                offset: 12
547            }
548        );
549
550        assert_eq!(mi.item.content_model(), ContentModel::Empty);
551        assert_eq!(mi.item.raw_context().deref(), "audio");
552        assert!(mi.item.nested_blocks().next().is_none());
553        assert!(mi.item.title_source().is_none());
554        assert!(mi.item.title().is_none());
555        assert!(mi.item.anchor().is_none());
556        assert!(mi.item.anchor_reftext().is_none());
557        assert!(mi.item.attrlist().is_none());
558        assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
559    }
560
561    #[test]
562    fn video() {
563        let mut parser = Parser::default();
564
565        let mi = crate::blocks::MediaBlock::parse(&BlockMetadata::new("video::bar[]"), &mut parser)
566            .unwrap_if_no_warnings()
567            .unwrap();
568
569        assert_eq!(
570            mi.item,
571            MediaBlock {
572                type_: MediaType::Video,
573                target: Span {
574                    data: "bar",
575                    line: 1,
576                    col: 8,
577                    offset: 7,
578                },
579                macro_attrlist: Attrlist {
580                    attributes: &[],
581                    anchor: None,
582                    source: Span {
583                        data: "",
584                        line: 1,
585                        col: 12,
586                        offset: 11,
587                    }
588                },
589                source: Span {
590                    data: "video::bar[]",
591                    line: 1,
592                    col: 1,
593                    offset: 0,
594                },
595                title_source: None,
596                title: None,
597                anchor: None,
598                anchor_reftext: None,
599                attrlist: None,
600            }
601        );
602
603        assert_eq!(
604            mi.after,
605            Span {
606                data: "",
607                line: 1,
608                col: 13,
609                offset: 12
610            }
611        );
612
613        assert_eq!(mi.item.content_model(), ContentModel::Empty);
614        assert_eq!(mi.item.raw_context().deref(), "video");
615        assert!(mi.item.nested_blocks().next().is_none());
616        assert!(mi.item.title_source().is_none());
617        assert!(mi.item.title().is_none());
618        assert!(mi.item.anchor().is_none());
619        assert!(mi.item.anchor_reftext().is_none());
620        assert!(mi.item.attrlist().is_none());
621        assert_eq!(mi.item.substitution_group(), SubstitutionGroup::Normal);
622    }
623
624    #[test]
625    fn err_duplicate_comma() {
626        let mut parser = Parser::default();
627        let maw = crate::blocks::MediaBlock::parse(
628            &BlockMetadata::new("image::bar[blah,,blap]"),
629            &mut parser,
630        );
631
632        let mi = maw.item.unwrap().clone();
633
634        assert_eq!(
635            mi.item,
636            MediaBlock {
637                type_: MediaType::Image,
638                target: Span {
639                    data: "bar",
640                    line: 1,
641                    col: 8,
642                    offset: 7,
643                },
644                macro_attrlist: Attrlist {
645                    attributes: &[
646                        ElementAttribute {
647                            name: None,
648                            shorthand_items: &["blah"],
649                            value: "blah"
650                        },
651                        ElementAttribute {
652                            name: None,
653                            shorthand_items: &[],
654                            value: "blap"
655                        }
656                    ],
657                    anchor: None,
658                    source: Span {
659                        data: "blah,,blap",
660                        line: 1,
661                        col: 12,
662                        offset: 11,
663                    }
664                },
665                source: Span {
666                    data: "image::bar[blah,,blap]",
667                    line: 1,
668                    col: 1,
669                    offset: 0,
670                },
671                title_source: None,
672                title: None,
673                anchor: None,
674                anchor_reftext: None,
675                attrlist: None,
676            }
677        );
678
679        assert_eq!(
680            mi.after,
681            Span {
682                data: "",
683                line: 1,
684                col: 23,
685                offset: 22
686            }
687        );
688
689        assert_eq!(
690            maw.warnings,
691            vec![Warning {
692                source: Span {
693                    data: "blah,,blap",
694                    line: 1,
695                    col: 12,
696                    offset: 11,
697                },
698                warning: WarningType::EmptyAttributeValue,
699            }]
700        );
701    }
702
703    mod media_type {
704        mod impl_debug {
705            use crate::blocks::MediaType;
706
707            #[test]
708            fn image() {
709                let media_type = MediaType::Image;
710                let debug_output = format!("{:?}", media_type);
711                assert_eq!(debug_output, "MediaType::Image");
712            }
713
714            #[test]
715            fn video() {
716                let media_type = MediaType::Video;
717                let debug_output = format!("{:?}", media_type);
718                assert_eq!(debug_output, "MediaType::Video");
719            }
720
721            #[test]
722            fn audio() {
723                let media_type = MediaType::Audio;
724                let debug_output = format!("{:?}", media_type);
725                assert_eq!(debug_output, "MediaType::Audio");
726            }
727        }
728    }
729}