acdc_parser/model/
media.rs

1//! Media types for `AsciiDoc` documents (images, audio, video).
2
3use std::fmt::Display;
4use std::str::FromStr;
5
6use serde::{
7    Deserialize, Serialize,
8    de::{self, Deserializer, MapAccess, Visitor},
9    ser::{SerializeMap, Serializer},
10};
11
12use crate::{Error, Positioning, SourceLocation};
13
14use super::inlines::InlineNode;
15use super::location::{Location, Position};
16use super::metadata::BlockMetadata;
17use super::title::Title;
18
19/// A `Source` represents the source of content (images, audio, video, etc.).
20///
21/// This type distinguishes between filesystem paths, URLs, and simple names (like icon names).
22#[derive(Clone, Debug, PartialEq)]
23pub enum Source {
24    /// A filesystem path
25    Path(std::path::PathBuf),
26    /// A URL
27    Url(url::Url),
28    /// A simple name (used for example in menu macros or icon names)
29    Name(String),
30}
31
32impl Source {
33    /// Get the filename from the source.
34    ///
35    /// For paths, this returns the file name component. For URLs, it returns the last path
36    /// segment. For names, it returns the name itself.
37    #[must_use]
38    pub fn get_filename(&self) -> Option<&str> {
39        match self {
40            Source::Path(path) => path.file_name().and_then(|os_str| os_str.to_str()),
41            Source::Url(url) => url
42                .path_segments()
43                .and_then(std::iter::Iterator::last)
44                .filter(|s| !s.is_empty()),
45            Source::Name(name) => Some(name.as_str()),
46        }
47    }
48}
49
50impl FromStr for Source {
51    type Err = Error;
52
53    fn from_str(value: &str) -> Result<Self, Self::Err> {
54        // Try to parse as URL first
55        if value.starts_with("http://")
56            || value.starts_with("https://")
57            || value.starts_with("ftp://")
58            || value.starts_with("irc://")
59            || value.starts_with("mailto:")
60        {
61            url::Url::parse(value).map(Source::Url).map_err(|e| {
62                Error::Parse(
63                    Box::new(SourceLocation {
64                        file: None,
65                        positioning: Positioning::Position(Position::default()),
66                    }),
67                    format!("invalid URL: {e}"),
68                )
69            })
70        } else if value.contains('/') || value.contains('\\') || value.contains('.') {
71            // Contains path separators - treat as filesystem path or contains a dot which
72            // might indicate a filename with extension
73            Ok(Source::Path(std::path::PathBuf::from(value)))
74        } else {
75            // Contains special characters or spaces - treat as a name
76            Ok(Source::Name(value.to_string()))
77        }
78    }
79}
80
81impl Display for Source {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        match self {
84            Source::Path(path) => write!(f, "{}", path.display()),
85            Source::Url(url) => {
86                // The url crate normalizes domain-only URLs by adding a trailing slash
87                // (e.g., "https://example.com" -> "https://example.com/").
88                // Strip it to match asciidoctor's output behavior.
89                let url_str = url.as_str();
90                if url.path() == "/" && !url_str.ends_with("://") {
91                    write!(f, "{}", url_str.trim_end_matches('/'))
92                } else {
93                    write!(f, "{url}")
94                }
95            }
96            Source::Name(name) => write!(f, "{name}"),
97        }
98    }
99}
100
101impl Serialize for Source {
102    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
103    where
104        S: Serializer,
105    {
106        let mut state = serializer.serialize_map(None)?;
107        match self {
108            Source::Path(path) => {
109                state.serialize_entry("type", "path")?;
110                state.serialize_entry("value", &path.display().to_string())?;
111            }
112            Source::Url(url) => {
113                state.serialize_entry("type", "url")?;
114                state.serialize_entry("value", url.as_str())?;
115            }
116            Source::Name(name) => {
117                state.serialize_entry("type", "name")?;
118                state.serialize_entry("value", name)?;
119            }
120        }
121        state.end()
122    }
123}
124
125impl<'de> Deserialize<'de> for Source {
126    fn deserialize<D>(deserializer: D) -> Result<Source, D::Error>
127    where
128        D: Deserializer<'de>,
129    {
130        struct SourceVisitor;
131
132        impl<'de> Visitor<'de> for SourceVisitor {
133            type Value = Source;
134
135            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
136                formatter.write_str("a Source object with type and value fields")
137            }
138
139            fn visit_map<V>(self, mut map: V) -> Result<Source, V::Error>
140            where
141                V: MapAccess<'de>,
142            {
143                let mut source_type: Option<String> = None;
144                let mut value: Option<String> = None;
145
146                while let Some(key) = map.next_key::<String>()? {
147                    match key.as_str() {
148                        "type" => {
149                            if source_type.is_some() {
150                                return Err(de::Error::duplicate_field("type"));
151                            }
152                            source_type = Some(map.next_value()?);
153                        }
154                        "value" => {
155                            if value.is_some() {
156                                return Err(de::Error::duplicate_field("value"));
157                            }
158                            value = Some(map.next_value()?);
159                        }
160                        _ => {
161                            let _ = map.next_value::<de::IgnoredAny>()?;
162                        }
163                    }
164                }
165
166                let source_type = source_type.ok_or_else(|| de::Error::missing_field("type"))?;
167                let value = value.ok_or_else(|| de::Error::missing_field("value"))?;
168
169                match source_type.as_str() {
170                    "path" => Ok(Source::Path(std::path::PathBuf::from(value))),
171                    "url" => url::Url::parse(&value)
172                        .map(Source::Url)
173                        .map_err(|e| de::Error::custom(format!("invalid URL: {e}"))),
174                    "name" => Ok(Source::Name(value)),
175                    _ => Err(de::Error::custom(format!(
176                        "unexpected source type: {source_type}"
177                    ))),
178                }
179            }
180        }
181
182        deserializer.deserialize_map(SourceVisitor)
183    }
184}
185
186/// An `Audio` represents an audio block in a document.
187#[derive(Clone, Debug, PartialEq)]
188#[non_exhaustive]
189pub struct Audio {
190    pub title: Title,
191    pub source: Source,
192    pub metadata: BlockMetadata,
193    pub location: Location,
194}
195
196impl Audio {
197    /// Create a new audio with the given source and location.
198    #[must_use]
199    pub fn new(source: Source, location: Location) -> Self {
200        Self {
201            title: Title::default(),
202            source,
203            metadata: BlockMetadata::default(),
204            location,
205        }
206    }
207
208    /// Set the title.
209    #[must_use]
210    pub fn with_title(mut self, title: Title) -> Self {
211        self.title = title;
212        self
213    }
214
215    /// Set the metadata.
216    #[must_use]
217    pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
218        self.metadata = metadata;
219        self
220    }
221}
222
223/// A `Video` represents a video block in a document.
224#[derive(Clone, Debug, PartialEq)]
225#[non_exhaustive]
226pub struct Video {
227    pub title: Title,
228    pub sources: Vec<Source>,
229    pub metadata: BlockMetadata,
230    pub location: Location,
231}
232
233impl Video {
234    /// Create a new video with the given sources and location.
235    #[must_use]
236    pub fn new(sources: Vec<Source>, location: Location) -> Self {
237        Self {
238            title: Title::default(),
239            sources,
240            metadata: BlockMetadata::default(),
241            location,
242        }
243    }
244
245    /// Set the title.
246    #[must_use]
247    pub fn with_title(mut self, title: Title) -> Self {
248        self.title = title;
249        self
250    }
251
252    /// Set the metadata.
253    #[must_use]
254    pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
255        self.metadata = metadata;
256        self
257    }
258}
259
260/// An `Image` represents an image block in a document.
261#[derive(Clone, Debug, PartialEq)]
262#[non_exhaustive]
263pub struct Image {
264    pub title: Title,
265    pub source: Source,
266    pub metadata: BlockMetadata,
267    pub location: Location,
268}
269
270impl Image {
271    /// Create a new image with the given source and location.
272    #[must_use]
273    pub fn new(source: Source, location: Location) -> Self {
274        Self {
275            title: Title::default(),
276            source,
277            metadata: BlockMetadata::default(),
278            location,
279        }
280    }
281
282    /// Set the title.
283    #[must_use]
284    pub fn with_title(mut self, title: Title) -> Self {
285        self.title = title;
286        self
287    }
288
289    /// Set the metadata.
290    #[must_use]
291    pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
292        self.metadata = metadata;
293        self
294    }
295}
296
297impl Serialize for Audio {
298    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
299    where
300        S: Serializer,
301    {
302        let mut state = serializer.serialize_map(None)?;
303        state.serialize_entry("name", "audio")?;
304        state.serialize_entry("type", "block")?;
305        state.serialize_entry("form", "macro")?;
306        if !self.metadata.is_default() {
307            state.serialize_entry("metadata", &self.metadata)?;
308        }
309        if !self.title.is_empty() {
310            state.serialize_entry("title", &self.title)?;
311        }
312        state.serialize_entry("source", &self.source)?;
313        state.serialize_entry("location", &self.location)?;
314        state.end()
315    }
316}
317
318impl Serialize for Image {
319    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
320    where
321        S: Serializer,
322    {
323        let mut state = serializer.serialize_map(None)?;
324        state.serialize_entry("name", "image")?;
325        state.serialize_entry("type", "block")?;
326        state.serialize_entry("form", "macro")?;
327        if !self.metadata.is_default() {
328            state.serialize_entry("metadata", &self.metadata)?;
329        }
330        if !self.title.is_empty() {
331            state.serialize_entry("title", &self.title)?;
332        }
333        state.serialize_entry("source", &self.source)?;
334        state.serialize_entry("location", &self.location)?;
335        state.end()
336    }
337}
338
339impl Serialize for Video {
340    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
341    where
342        S: Serializer,
343    {
344        let mut state = serializer.serialize_map(None)?;
345        state.serialize_entry("name", "video")?;
346        state.serialize_entry("type", "block")?;
347        state.serialize_entry("form", "macro")?;
348        if !self.metadata.is_default() {
349            state.serialize_entry("metadata", &self.metadata)?;
350        }
351        if !self.title.is_empty() {
352            state.serialize_entry("title", &self.title)?;
353        }
354        if !self.sources.is_empty() {
355            state.serialize_entry("sources", &self.sources)?;
356        }
357        state.serialize_entry("location", &self.location)?;
358        state.end()
359    }
360}
361
362impl<'de> Deserialize<'de> for Audio {
363    fn deserialize<D>(deserializer: D) -> Result<Audio, D::Error>
364    where
365        D: Deserializer<'de>,
366    {
367        #[derive(Deserialize)]
368        #[serde(field_identifier, rename_all = "lowercase")]
369        enum Field {
370            Metadata,
371            Title,
372            Source,
373            Location,
374        }
375
376        struct AudioVisitor;
377
378        impl<'de> Visitor<'de> for AudioVisitor {
379            type Value = Audio;
380
381            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
382                formatter.write_str("struct Audio")
383            }
384
385            fn visit_map<V>(self, mut map: V) -> Result<Audio, V::Error>
386            where
387                V: MapAccess<'de>,
388            {
389                let mut metadata = None;
390                let mut title: Option<Vec<InlineNode>> = None;
391                let mut source = None;
392                let mut location = None;
393
394                while let Some(key) = map.next_key()? {
395                    match key {
396                        Field::Metadata => {
397                            metadata = Some(map.next_value()?);
398                        }
399                        Field::Title => {
400                            title = Some(map.next_value()?);
401                        }
402                        Field::Source => {
403                            source = Some(map.next_value()?);
404                        }
405                        Field::Location => {
406                            location = Some(map.next_value()?);
407                        }
408                    }
409                }
410
411                Ok(Audio {
412                    title: title.unwrap_or_default().into(),
413                    source: source.ok_or_else(|| serde::de::Error::missing_field("source"))?,
414                    metadata: metadata.unwrap_or_default(),
415                    location: location
416                        .ok_or_else(|| serde::de::Error::missing_field("location"))?,
417                })
418            }
419        }
420
421        deserializer.deserialize_struct(
422            "Audio",
423            &["metadata", "title", "source", "location"],
424            AudioVisitor,
425        )
426    }
427}
428
429impl<'de> Deserialize<'de> for Image {
430    fn deserialize<D>(deserializer: D) -> Result<Image, D::Error>
431    where
432        D: Deserializer<'de>,
433    {
434        #[derive(Deserialize)]
435        #[serde(field_identifier, rename_all = "lowercase")]
436        enum Field {
437            Metadata,
438            Title,
439            Source,
440            Location,
441        }
442
443        struct ImageVisitor;
444
445        impl<'de> Visitor<'de> for ImageVisitor {
446            type Value = Image;
447
448            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
449                formatter.write_str("struct Image")
450            }
451
452            fn visit_map<V>(self, mut map: V) -> Result<Image, V::Error>
453            where
454                V: MapAccess<'de>,
455            {
456                let mut metadata = None;
457                let mut title: Option<Vec<InlineNode>> = None;
458                let mut source = None;
459                let mut location = None;
460
461                while let Some(key) = map.next_key()? {
462                    match key {
463                        Field::Metadata => {
464                            metadata = Some(map.next_value()?);
465                        }
466                        Field::Title => {
467                            title = Some(map.next_value()?);
468                        }
469                        Field::Source => {
470                            source = Some(map.next_value()?);
471                        }
472                        Field::Location => {
473                            location = Some(map.next_value()?);
474                        }
475                    }
476                }
477
478                Ok(Image {
479                    title: title.unwrap_or_default().into(),
480                    source: source.ok_or_else(|| serde::de::Error::missing_field("source"))?,
481                    metadata: metadata.unwrap_or_default(),
482                    location: location
483                        .ok_or_else(|| serde::de::Error::missing_field("location"))?,
484                })
485            }
486        }
487
488        deserializer.deserialize_struct(
489            "Image",
490            &["metadata", "title", "source", "location"],
491            ImageVisitor,
492        )
493    }
494}
495
496// Video uses "sources" (plural)
497impl<'de> Deserialize<'de> for Video {
498    fn deserialize<D>(deserializer: D) -> Result<Video, D::Error>
499    where
500        D: Deserializer<'de>,
501    {
502        #[derive(Deserialize)]
503        #[serde(field_identifier, rename_all = "lowercase")]
504        enum Field {
505            Metadata,
506            Title,
507            Sources,
508            Location,
509        }
510
511        struct VideoVisitor;
512
513        impl<'de> Visitor<'de> for VideoVisitor {
514            type Value = Video;
515
516            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
517                formatter.write_str("struct Video")
518            }
519
520            fn visit_map<V>(self, mut map: V) -> Result<Video, V::Error>
521            where
522                V: MapAccess<'de>,
523            {
524                let mut metadata = None;
525                let mut title: Option<Vec<InlineNode>> = None;
526                let mut sources = None;
527                let mut location = None;
528
529                while let Some(key) = map.next_key()? {
530                    match key {
531                        Field::Metadata => metadata = Some(map.next_value()?),
532                        Field::Title => title = Some(map.next_value()?),
533                        Field::Sources => sources = Some(map.next_value()?),
534                        Field::Location => location = Some(map.next_value()?),
535                    }
536                }
537
538                Ok(Video {
539                    title: title.unwrap_or_default().into(),
540                    sources: sources.unwrap_or_default(),
541                    metadata: metadata.unwrap_or_default(),
542                    location: location
543                        .ok_or_else(|| serde::de::Error::missing_field("location"))?,
544                })
545            }
546        }
547
548        deserializer.deserialize_struct(
549            "Video",
550            &["metadata", "title", "sources", "location"],
551            VideoVisitor,
552        )
553    }
554}