1use std::fmt::Display;
4use std::str::FromStr;
5
6use serde::{
7 Serialize,
8 ser::{SerializeMap, Serializer},
9};
10
11use crate::{Error, Positioning, SourceLocation};
12
13use super::location::{Location, Position};
14use super::metadata::BlockMetadata;
15use super::title::Title;
16
17#[derive(Clone, Debug, PartialEq)]
50pub enum Source {
51 Path(std::path::PathBuf),
53 Url(SourceUrl),
55 Name(String),
57}
58
59#[derive(Clone, Debug)]
67pub struct SourceUrl {
68 url: url::Url,
69 original: String,
70}
71
72impl SourceUrl {
73 pub fn new(input: &str) -> Result<Self, url::ParseError> {
79 let url = url::Url::parse(input)?;
80 Ok(Self {
81 url,
82 original: input.to_string(),
83 })
84 }
85
86 #[must_use]
88 pub fn url(&self) -> &url::Url {
89 &self.url
90 }
91}
92
93impl std::ops::Deref for SourceUrl {
94 type Target = url::Url;
95 fn deref(&self) -> &Self::Target {
96 &self.url
97 }
98}
99
100impl PartialEq for SourceUrl {
101 fn eq(&self, other: &Self) -> bool {
102 self.url == other.url
103 }
104}
105
106impl Display for SourceUrl {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 write!(f, "{}", self.original)
109 }
110}
111
112impl Source {
113 #[must_use]
118 pub fn get_filename(&self) -> Option<&str> {
119 match self {
120 Source::Path(path) => path.file_name().and_then(|os_str| os_str.to_str()),
121 Source::Url(url) => url
122 .path_segments()
123 .and_then(std::iter::Iterator::last)
124 .filter(|s| !s.is_empty()),
125 Source::Name(name) => Some(name.as_str()),
126 }
127 }
128}
129
130impl FromStr for Source {
131 type Err = Error;
132
133 fn from_str(value: &str) -> Result<Self, Self::Err> {
134 if value.starts_with("http://")
136 || value.starts_with("https://")
137 || value.starts_with("ftp://")
138 || value.starts_with("irc://")
139 || value.starts_with("mailto:")
140 {
141 SourceUrl::new(value).map(Source::Url).map_err(|e| {
142 Error::Parse(
143 Box::new(SourceLocation {
144 file: None,
145 positioning: Positioning::Position(Position::default()),
146 }),
147 format!("invalid URL: {e}"),
148 )
149 })
150 } else if value.contains('/') || value.contains('\\') || value.contains('.') {
151 Ok(Source::Path(std::path::PathBuf::from(value)))
154 } else {
155 Ok(Source::Name(value.to_string()))
157 }
158 }
159}
160
161impl Display for Source {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 match self {
164 Source::Path(path) => write!(f, "{}", path.display()),
165 Source::Url(url) => write!(f, "{url}"),
166 Source::Name(name) => write!(f, "{name}"),
167 }
168 }
169}
170
171impl Serialize for Source {
172 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
173 where
174 S: Serializer,
175 {
176 let mut state = serializer.serialize_map(None)?;
177 match self {
178 Source::Path(path) => {
179 state.serialize_entry("type", "path")?;
180 state.serialize_entry("value", &path.display().to_string())?;
181 }
182 Source::Url(url) => {
183 state.serialize_entry("type", "url")?;
184 state.serialize_entry("value", &url.to_string())?;
185 }
186 Source::Name(name) => {
187 state.serialize_entry("type", "name")?;
188 state.serialize_entry("value", name)?;
189 }
190 }
191 state.end()
192 }
193}
194
195#[derive(Clone, Debug, PartialEq)]
197#[non_exhaustive]
198pub struct Audio {
199 pub title: Title,
200 pub source: Source,
201 pub metadata: BlockMetadata,
202 pub location: Location,
203}
204
205impl Audio {
206 #[must_use]
208 pub fn new(source: Source, location: Location) -> Self {
209 Self {
210 title: Title::default(),
211 source,
212 metadata: BlockMetadata::default(),
213 location,
214 }
215 }
216
217 #[must_use]
219 pub fn with_title(mut self, title: Title) -> Self {
220 self.title = title;
221 self
222 }
223
224 #[must_use]
226 pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
227 self.metadata = metadata;
228 self
229 }
230}
231
232#[derive(Clone, Debug, PartialEq)]
234#[non_exhaustive]
235pub struct Video {
236 pub title: Title,
237 pub sources: Vec<Source>,
238 pub metadata: BlockMetadata,
239 pub location: Location,
240}
241
242impl Video {
243 #[must_use]
245 pub fn new(sources: Vec<Source>, location: Location) -> Self {
246 Self {
247 title: Title::default(),
248 sources,
249 metadata: BlockMetadata::default(),
250 location,
251 }
252 }
253
254 #[must_use]
256 pub fn with_title(mut self, title: Title) -> Self {
257 self.title = title;
258 self
259 }
260
261 #[must_use]
263 pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
264 self.metadata = metadata;
265 self
266 }
267}
268
269#[derive(Clone, Debug, PartialEq)]
271#[non_exhaustive]
272pub struct Image {
273 pub title: Title,
274 pub source: Source,
275 pub metadata: BlockMetadata,
276 pub location: Location,
277}
278
279impl Image {
280 #[must_use]
282 pub fn new(source: Source, location: Location) -> Self {
283 Self {
284 title: Title::default(),
285 source,
286 metadata: BlockMetadata::default(),
287 location,
288 }
289 }
290
291 #[must_use]
293 pub fn with_title(mut self, title: Title) -> Self {
294 self.title = title;
295 self
296 }
297
298 #[must_use]
300 pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
301 self.metadata = metadata;
302 self
303 }
304}
305
306impl Serialize for Audio {
307 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
308 where
309 S: Serializer,
310 {
311 let mut state = serializer.serialize_map(None)?;
312 state.serialize_entry("name", "audio")?;
313 state.serialize_entry("type", "block")?;
314 state.serialize_entry("form", "macro")?;
315 if !self.metadata.is_default() {
316 state.serialize_entry("metadata", &self.metadata)?;
317 }
318 if !self.title.is_empty() {
319 state.serialize_entry("title", &self.title)?;
320 }
321 state.serialize_entry("source", &self.source)?;
322 state.serialize_entry("location", &self.location)?;
323 state.end()
324 }
325}
326
327impl Serialize for Image {
328 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
329 where
330 S: Serializer,
331 {
332 let mut state = serializer.serialize_map(None)?;
333 state.serialize_entry("name", "image")?;
334 state.serialize_entry("type", "block")?;
335 state.serialize_entry("form", "macro")?;
336 if !self.metadata.is_default() {
337 state.serialize_entry("metadata", &self.metadata)?;
338 }
339 if !self.title.is_empty() {
340 state.serialize_entry("title", &self.title)?;
341 }
342 state.serialize_entry("source", &self.source)?;
343 state.serialize_entry("location", &self.location)?;
344 state.end()
345 }
346}
347
348impl Serialize for Video {
349 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
350 where
351 S: Serializer,
352 {
353 let mut state = serializer.serialize_map(None)?;
354 state.serialize_entry("name", "video")?;
355 state.serialize_entry("type", "block")?;
356 state.serialize_entry("form", "macro")?;
357 if !self.metadata.is_default() {
358 state.serialize_entry("metadata", &self.metadata)?;
359 }
360 if !self.title.is_empty() {
361 state.serialize_entry("title", &self.title)?;
362 }
363 if !self.sources.is_empty() {
364 state.serialize_entry("sources", &self.sources)?;
365 }
366 state.serialize_entry("location", &self.location)?;
367 state.end()
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374
375 #[test]
376 fn source_display_preserves_trailing_slash() -> Result<(), Error> {
377 let source = Source::from_str("http://example.com/")?;
379 assert_eq!(source.to_string(), "http://example.com/");
380 Ok(())
381 }
382
383 #[test]
384 fn source_display_no_trailing_slash_when_absent() -> Result<(), Error> {
385 let source = Source::from_str("http://example.com")?;
387 assert_eq!(source.to_string(), "http://example.com");
388 Ok(())
389 }
390
391 #[test]
392 fn source_display_preserves_path_trailing_slash() -> Result<(), Error> {
393 let source = Source::from_str("http://example.com/foo/")?;
394 assert_eq!(source.to_string(), "http://example.com/foo/");
395 Ok(())
396 }
397
398 #[test]
399 fn source_display_preserves_path_without_trailing_slash() -> Result<(), Error> {
400 let source = Source::from_str("http://example.com/foo")?;
401 assert_eq!(source.to_string(), "http://example.com/foo");
402 Ok(())
403 }
404
405 #[test]
406 fn source_display_preserves_query_without_path() -> Result<(), Error> {
407 let source = Source::from_str("https://example.com?a=1&b=2")?;
409 assert_eq!(source.to_string(), "https://example.com?a=1&b=2");
410 Ok(())
411 }
412
413 #[test]
414 fn source_display_preserves_trailing_slash_with_query() -> Result<(), Error> {
415 let source = Source::from_str("https://example.com/?a=1&b=2")?;
416 assert_eq!(source.to_string(), "https://example.com/?a=1&b=2");
417 Ok(())
418 }
419}