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