use std::fmt::Display;
use std::str::FromStr;
use serde::{
Serialize,
ser::{SerializeMap, Serializer},
};
use crate::{Error, Positioning, SourceLocation};
use super::location::{Location, Position};
use super::metadata::BlockMetadata;
use super::title::Title;
#[derive(Clone, Debug, PartialEq)]
pub enum Source {
Path(std::path::PathBuf),
Url(SourceUrl),
Name(String),
}
#[derive(Clone, Debug)]
pub struct SourceUrl {
url: url::Url,
original: String,
}
impl SourceUrl {
pub fn new(input: &str) -> Result<Self, url::ParseError> {
let url = url::Url::parse(input)?;
Ok(Self {
url,
original: input.to_string(),
})
}
#[must_use]
pub fn url(&self) -> &url::Url {
&self.url
}
}
impl std::ops::Deref for SourceUrl {
type Target = url::Url;
fn deref(&self) -> &Self::Target {
&self.url
}
}
impl PartialEq for SourceUrl {
fn eq(&self, other: &Self) -> bool {
self.url == other.url
}
}
impl Display for SourceUrl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.original)
}
}
impl Source {
#[must_use]
pub fn get_filename(&self) -> Option<&str> {
match self {
Source::Path(path) => path.file_name().and_then(|os_str| os_str.to_str()),
Source::Url(url) => url
.path_segments()
.and_then(std::iter::Iterator::last)
.filter(|s| !s.is_empty()),
Source::Name(name) => Some(name.as_str()),
}
}
}
impl FromStr for Source {
type Err = Error;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if value.starts_with("http://")
|| value.starts_with("https://")
|| value.starts_with("ftp://")
|| value.starts_with("irc://")
|| value.starts_with("mailto:")
{
SourceUrl::new(value).map(Source::Url).map_err(|e| {
Error::Parse(
Box::new(SourceLocation {
file: None,
positioning: Positioning::Position(Position::default()),
}),
format!("invalid URL: {e}"),
)
})
} else if value.contains('/') || value.contains('\\') || value.contains('.') {
Ok(Source::Path(std::path::PathBuf::from(value)))
} else {
Ok(Source::Name(value.to_string()))
}
}
}
impl Display for Source {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Source::Path(path) => write!(f, "{}", path.display()),
Source::Url(url) => write!(f, "{url}"),
Source::Name(name) => write!(f, "{name}"),
}
}
}
impl Serialize for Source {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_map(None)?;
match self {
Source::Path(path) => {
state.serialize_entry("type", "path")?;
state.serialize_entry("value", &path.display().to_string())?;
}
Source::Url(url) => {
state.serialize_entry("type", "url")?;
state.serialize_entry("value", &url.to_string())?;
}
Source::Name(name) => {
state.serialize_entry("type", "name")?;
state.serialize_entry("value", name)?;
}
}
state.end()
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct Audio {
pub title: Title,
pub source: Source,
pub metadata: BlockMetadata,
pub location: Location,
}
impl Audio {
#[must_use]
pub fn new(source: Source, location: Location) -> Self {
Self {
title: Title::default(),
source,
metadata: BlockMetadata::default(),
location,
}
}
#[must_use]
pub fn with_title(mut self, title: Title) -> Self {
self.title = title;
self
}
#[must_use]
pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
self.metadata = metadata;
self
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct Video {
pub title: Title,
pub sources: Vec<Source>,
pub metadata: BlockMetadata,
pub location: Location,
}
impl Video {
#[must_use]
pub fn new(sources: Vec<Source>, location: Location) -> Self {
Self {
title: Title::default(),
sources,
metadata: BlockMetadata::default(),
location,
}
}
#[must_use]
pub fn with_title(mut self, title: Title) -> Self {
self.title = title;
self
}
#[must_use]
pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
self.metadata = metadata;
self
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct Image {
pub title: Title,
pub source: Source,
pub metadata: BlockMetadata,
pub location: Location,
}
impl Image {
#[must_use]
pub fn new(source: Source, location: Location) -> Self {
Self {
title: Title::default(),
source,
metadata: BlockMetadata::default(),
location,
}
}
#[must_use]
pub fn with_title(mut self, title: Title) -> Self {
self.title = title;
self
}
#[must_use]
pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
self.metadata = metadata;
self
}
}
impl Serialize for Audio {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_map(None)?;
state.serialize_entry("name", "audio")?;
state.serialize_entry("type", "block")?;
state.serialize_entry("form", "macro")?;
if !self.metadata.is_default() {
state.serialize_entry("metadata", &self.metadata)?;
}
if !self.title.is_empty() {
state.serialize_entry("title", &self.title)?;
}
state.serialize_entry("source", &self.source)?;
state.serialize_entry("location", &self.location)?;
state.end()
}
}
impl Serialize for Image {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_map(None)?;
state.serialize_entry("name", "image")?;
state.serialize_entry("type", "block")?;
state.serialize_entry("form", "macro")?;
if !self.metadata.is_default() {
state.serialize_entry("metadata", &self.metadata)?;
}
if !self.title.is_empty() {
state.serialize_entry("title", &self.title)?;
}
state.serialize_entry("source", &self.source)?;
state.serialize_entry("location", &self.location)?;
state.end()
}
}
impl Serialize for Video {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_map(None)?;
state.serialize_entry("name", "video")?;
state.serialize_entry("type", "block")?;
state.serialize_entry("form", "macro")?;
if !self.metadata.is_default() {
state.serialize_entry("metadata", &self.metadata)?;
}
if !self.title.is_empty() {
state.serialize_entry("title", &self.title)?;
}
if !self.sources.is_empty() {
state.serialize_entry("sources", &self.sources)?;
}
state.serialize_entry("location", &self.location)?;
state.end()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn source_display_preserves_trailing_slash() -> Result<(), Error> {
let source = Source::from_str("http://example.com/")?;
assert_eq!(source.to_string(), "http://example.com/");
Ok(())
}
#[test]
fn source_display_no_trailing_slash_when_absent() -> Result<(), Error> {
let source = Source::from_str("http://example.com")?;
assert_eq!(source.to_string(), "http://example.com");
Ok(())
}
#[test]
fn source_display_preserves_path_trailing_slash() -> Result<(), Error> {
let source = Source::from_str("http://example.com/foo/")?;
assert_eq!(source.to_string(), "http://example.com/foo/");
Ok(())
}
#[test]
fn source_display_preserves_path_without_trailing_slash() -> Result<(), Error> {
let source = Source::from_str("http://example.com/foo")?;
assert_eq!(source.to_string(), "http://example.com/foo");
Ok(())
}
#[test]
fn source_display_preserves_query_without_path() -> Result<(), Error> {
let source = Source::from_str("https://example.com?a=1&b=2")?;
assert_eq!(source.to_string(), "https://example.com?a=1&b=2");
Ok(())
}
#[test]
fn source_display_preserves_trailing_slash_with_query() -> Result<(), Error> {
let source = Source::from_str("https://example.com/?a=1&b=2")?;
assert_eq!(source.to_string(), "https://example.com/?a=1&b=2");
Ok(())
}
}