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