use std::path::Path;
use prov::Mapping;
use crate::page::html_escape;
pub fn payload_of(frontmatter: &Mapping) -> Option<&str> {
let content = frontmatter.get("content").and_then(|v| v.as_str())?;
let flagged = frontmatter
.get("attachment")
.and_then(|v| v.as_bool())
.unwrap_or(false);
(flagged || prov::document::is_opaque_payload(Path::new(content))).then_some(content)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Kind {
Image,
Video,
Audio,
Document,
Other,
}
fn kind_of(payload: &str) -> Kind {
match Path::new(payload)
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_ascii_lowercase())
.as_deref()
{
Some(
"png" | "jpg" | "jpeg" | "gif" | "webp" | "svg" | "avif" | "bmp" | "heic" | "heif",
) => Kind::Image,
Some("mp4" | "webm" | "mov" | "m4v" | "ogv") => Kind::Video,
Some("mp3" | "m4a" | "aac" | "ogg" | "oga" | "wav" | "flac" | "opus") => Kind::Audio,
Some("pdf") => Kind::Document,
_ => Kind::Other,
}
}
pub fn render(title: &str, payload: &str) -> (String, String) {
let href = html_escape(payload);
let name = html_escape(
Path::new(payload)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(payload),
);
let alt = html_escape(title);
let download = format!(
r#"<p class="attachment-download"><a href="{href}" download>Download {name}</a></p>"#
);
let html = match kind_of(payload) {
Kind::Image => format!(
r#"<figure class="attachment attachment-image"><img src="{href}" alt="{alt}"></figure>"#
),
Kind::Video => format!(
r#"<figure class="attachment attachment-video"><video controls src="{href}"></video></figure>
{download}"#
),
Kind::Audio => format!(
r#"<figure class="attachment attachment-audio"><audio controls src="{href}"></audio></figure>
{download}"#
),
Kind::Document => format!(
r#"<figure class="attachment attachment-document"><iframe src="{href}" title="{alt}"></iframe></figure>
{download}"#
),
Kind::Other => download,
};
let markdown = match kind_of(payload) {
Kind::Image => format!("\n"),
_ => format!("[{title}]({payload})\n"),
};
(html, markdown)
}
#[cfg(test)]
mod tests {
use super::*;
use prov::Value;
fn fm(pairs: &[(&str, Value)]) -> Mapping {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect()
}
#[test]
fn a_flagged_sidecar_and_an_opaque_content_are_both_attachments() {
let flagged = fm(&[
("content", Value::String("notes.bin".into())),
("attachment", Value::Bool(true)),
]);
assert_eq!(payload_of(&flagged), Some("notes.bin"));
let opaque = fm(&[("content", Value::String("scan.pdf".into()))]);
assert_eq!(payload_of(&opaque), Some("scan.pdf"));
let prose = fm(&[("content", Value::String("body.md".into()))]);
assert_eq!(payload_of(&prose), None);
assert_eq!(payload_of(&fm(&[])), None);
}
#[test]
fn each_kind_embeds_as_itself() {
let (img, md) = render("Loon Lake", "loon lake.jpg");
assert!(
img.contains(r#"<img src="loon lake.jpg" alt="Loon Lake">"#),
"{img}"
);
assert!(
!img.contains("download"),
"a picture needs no download link"
);
assert_eq!(md, "\n");
let (pdf, md) = render("Scan", "scan.pdf");
assert!(
pdf.contains(r#"<iframe src="scan.pdf" title="Scan">"#),
"{pdf}"
);
assert!(
pdf.contains(r#"<a href="scan.pdf" download>Download scan.pdf</a>"#),
"{pdf}"
);
assert_eq!(md, "[Scan](scan.pdf)\n");
let (video, _) = render("Clip", "clip.mp4");
assert!(
video.contains(r#"<video controls src="clip.mp4">"#),
"{video}"
);
let (audio, _) = render("Voice", "voice.m4a");
assert!(
audio.contains(r#"<audio controls src="voice.m4a">"#),
"{audio}"
);
let (other, _) = render("Archive", "backup.zip");
assert_eq!(
other,
r#"<p class="attachment-download"><a href="backup.zip" download>Download backup.zip</a></p>"#
);
}
#[test]
fn the_reference_is_escaped_for_an_attribute() {
let (html, _) = render("A & B", r#"a"b.png"#);
assert!(html.contains(r#"src="a"b.png""#), "{html}");
assert!(html.contains(r#"alt="A & B""#), "{html}");
}
}