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