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    attrlist: Option<Attrlist<'src>>,
21}
22
23/// A media type may be one of three different types.
24#[derive(Clone, Copy, Eq, PartialEq)]
25pub enum MediaType {
26    /// Still image
27    Image,
28
29    /// Video
30    Video,
31
32    /// Audio
33    Audio,
34}
35
36impl std::fmt::Debug for MediaType {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        match self {
39            MediaType::Image => write!(f, "MediaType::Image"),
40            MediaType::Video => write!(f, "MediaType::Video"),
41            MediaType::Audio => write!(f, "MediaType::Audio"),
42        }
43    }
44}
45
46impl<'src> MediaBlock<'src> {
47    pub(crate) fn parse(
48        metadata: &BlockMetadata<'src>,
49        parser: &mut Parser,
50    ) -> MatchAndWarnings<'src, Option<MatchedItem<'src, Self>>> {
51        let line = metadata.block_start.take_normalized_line();
52
53        // Line must end with `]`; otherwise, it's not a block macro.
54        if !line.item.ends_with(']') {
55            return MatchAndWarnings {
56                item: None,
57                warnings: vec![],
58            };
59        }
60
61        let Some(name) = line.item.take_ident() else {
62            return MatchAndWarnings {
63                item: None,
64                warnings: vec![],
65            };
66        };
67
68        let type_ = match name.item.data() {
69            "image" => MediaType::Image,
70            "video" => MediaType::Video,
71            "audio" => MediaType::Audio,
72            _ => {
73                return MatchAndWarnings {
74                    item: None,
75                    warnings: vec![],
76                };
77            }
78        };
79
80        let Some(colons) = name.after.take_prefix("::") else {
81            return MatchAndWarnings {
82                item: None,
83                warnings: vec![Warning {
84                    source: name.after,
85                    warning: WarningType::MacroMissingDoubleColon,
86                }],
87            };
88        };
89
90        // The target field must exist and be non-empty.
91        let target = colons.after.take_while(|c| c != '[');
92
93        if target.item.is_empty() {
94            return MatchAndWarnings {
95                item: None,
96                warnings: vec![Warning {
97                    source: target.after,
98                    warning: WarningType::MediaMacroMissingTarget,
99                }],
100            };
101        }
102
103        let Some(open_brace) = target.after.take_prefix("[") else {
104            return MatchAndWarnings {
105                item: None,
106                warnings: vec![Warning {
107                    source: target.after,
108                    warning: WarningType::MacroMissingAttributeList,
109                }],
110            };
111        };
112
113        let attrlist = open_brace.after.slice(0..open_brace.after.len() - 1);
114        // Note that we already checked that this line ends with a close brace.
115
116        let macro_attrlist = Attrlist::parse(attrlist, parser, AttrlistContext::Inline);
117
118        let source: Span = metadata.source.trim_remainder(line.after);
119        let source = source.slice(0..source.trim().len());
120
121        MatchAndWarnings {
122            item: Some(MatchedItem {
123                item: Self {
124                    type_,
125                    target: target.item,
126                    macro_attrlist: macro_attrlist.item.item,
127                    source,
128                    title_source: metadata.title_source,
129                    title: metadata.title.clone(),
130                    anchor: metadata.anchor,
131                    attrlist: metadata.attrlist.clone(),
132                },
133
134                after: line.after.discard_empty_lines(),
135            }),
136            warnings: macro_attrlist.warnings,
137        }
138    }
139
140    /// Return a [`Span`] describing the macro name.
141    pub fn type_(&self) -> MediaType {
142        self.type_
143    }
144
145    /// Return a [`Span`] describing the macro target.
146    pub fn target(&'src self) -> Option<&'src Span<'src>> {
147        Some(&self.target)
148    }
149
150    /// Return the macro's attribute list.
151    ///
152    /// **IMPORTANT:** This is the list of attributes _within_ the macro block
153    /// definition itself.
154    ///
155    /// See also [`attrlist()`] for attributes that can be defined before the
156    /// macro invocation.
157    ///
158    /// [`attrlist()`]: Self::attrlist()
159    pub fn macro_attrlist(&'src self) -> &'src Attrlist<'src> {
160        &self.macro_attrlist
161    }
162}
163
164impl<'src> IsBlock<'src> for MediaBlock<'src> {
165    fn content_model(&self) -> ContentModel {
166        ContentModel::Empty
167    }
168
169    fn raw_context(&self) -> CowStr<'src> {
170        match self.type_ {
171            MediaType::Audio => "audio",
172            MediaType::Image => "image",
173            MediaType::Video => "video",
174        }
175        .into()
176    }
177
178    fn title_source(&'src self) -> Option<Span<'src>> {
179        self.title_source
180    }
181
182    fn title(&self) -> Option<&str> {
183        self.title.as_deref()
184    }
185
186    fn anchor(&'src self) -> Option<Span<'src>> {
187        self.anchor
188    }
189
190    fn attrlist(&'src self) -> Option<&'src Attrlist<'src>> {
191        self.attrlist.as_ref()
192    }
193}
194
195impl<'src> HasSpan<'src> for MediaBlock<'src> {
196    fn span(&self) -> Span<'src> {
197        self.source
198    }
199}