plates_render/
attachment.rs1use std::path::Path;
21
22use prov::Mapping;
23
24use crate::page::html_escape;
25
26pub fn payload_of(frontmatter: &Mapping) -> Option<&str> {
34 let content = frontmatter.get("content").and_then(|v| v.as_str())?;
35 let flagged = frontmatter
36 .get("attachment")
37 .and_then(|v| v.as_bool())
38 .unwrap_or(false);
39 (flagged || prov::document::is_opaque_payload(Path::new(content))).then_some(content)
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44enum Kind {
45 Image,
46 Video,
47 Audio,
48 Document,
49 Other,
50}
51
52fn kind_of(payload: &str) -> Kind {
53 match Path::new(payload)
54 .extension()
55 .and_then(|e| e.to_str())
56 .map(|e| e.to_ascii_lowercase())
57 .as_deref()
58 {
59 Some(
60 "png" | "jpg" | "jpeg" | "gif" | "webp" | "svg" | "avif" | "bmp" | "heic" | "heif",
61 ) => Kind::Image,
62 Some("mp4" | "webm" | "mov" | "m4v" | "ogv") => Kind::Video,
63 Some("mp3" | "m4a" | "aac" | "ogg" | "oga" | "wav" | "flac" | "opus") => Kind::Audio,
64 Some("pdf") => Kind::Document,
65 _ => Kind::Other,
66 }
67}
68
69pub fn render(title: &str, payload: &str) -> (String, String) {
74 let href = html_escape(payload);
75 let name = html_escape(
76 Path::new(payload)
77 .file_name()
78 .and_then(|n| n.to_str())
79 .unwrap_or(payload),
80 );
81 let alt = html_escape(title);
82 let download = format!(
83 r#"<p class="attachment-download"><a href="{href}" download>Download {name}</a></p>"#
84 );
85 let html = match kind_of(payload) {
86 Kind::Image => format!(
87 r#"<figure class="attachment attachment-image"><img src="{href}" alt="{alt}"></figure>"#
88 ),
89 Kind::Video => format!(
90 r#"<figure class="attachment attachment-video"><video controls src="{href}"></video></figure>
91{download}"#
92 ),
93 Kind::Audio => format!(
94 r#"<figure class="attachment attachment-audio"><audio controls src="{href}"></audio></figure>
95{download}"#
96 ),
97 Kind::Document => format!(
98 r#"<figure class="attachment attachment-document"><iframe src="{href}" title="{alt}"></iframe></figure>
99{download}"#
100 ),
101 Kind::Other => download,
102 };
103 let markdown = match kind_of(payload) {
104 Kind::Image => format!("\n"),
105 _ => format!("[{title}]({payload})\n"),
106 };
107 (html, markdown)
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use prov::Value;
114
115 fn fm(pairs: &[(&str, Value)]) -> Mapping {
116 pairs
117 .iter()
118 .map(|(k, v)| (k.to_string(), v.clone()))
119 .collect()
120 }
121
122 #[test]
123 fn a_flagged_sidecar_and_an_opaque_content_are_both_attachments() {
124 let flagged = fm(&[
125 ("content", Value::String("notes.bin".into())),
126 ("attachment", Value::Bool(true)),
127 ]);
128 assert_eq!(payload_of(&flagged), Some("notes.bin"));
129 let opaque = fm(&[("content", Value::String("scan.pdf".into()))]);
130 assert_eq!(payload_of(&opaque), Some("scan.pdf"));
131 let prose = fm(&[("content", Value::String("body.md".into()))]);
133 assert_eq!(payload_of(&prose), None);
134 assert_eq!(payload_of(&fm(&[])), None);
135 }
136
137 #[test]
138 fn each_kind_embeds_as_itself() {
139 let (img, md) = render("Loon Lake", "loon lake.jpg");
140 assert!(
141 img.contains(r#"<img src="loon lake.jpg" alt="Loon Lake">"#),
142 "{img}"
143 );
144 assert!(
145 !img.contains("download"),
146 "a picture needs no download link"
147 );
148 assert_eq!(md, "\n");
149
150 let (pdf, md) = render("Scan", "scan.pdf");
151 assert!(
152 pdf.contains(r#"<iframe src="scan.pdf" title="Scan">"#),
153 "{pdf}"
154 );
155 assert!(
156 pdf.contains(r#"<a href="scan.pdf" download>Download scan.pdf</a>"#),
157 "{pdf}"
158 );
159 assert_eq!(md, "[Scan](scan.pdf)\n");
160
161 let (video, _) = render("Clip", "clip.mp4");
162 assert!(
163 video.contains(r#"<video controls src="clip.mp4">"#),
164 "{video}"
165 );
166 let (audio, _) = render("Voice", "voice.m4a");
167 assert!(
168 audio.contains(r#"<audio controls src="voice.m4a">"#),
169 "{audio}"
170 );
171
172 let (other, _) = render("Archive", "backup.zip");
173 assert_eq!(
174 other,
175 r#"<p class="attachment-download"><a href="backup.zip" download>Download backup.zip</a></p>"#
176 );
177 }
178
179 #[test]
180 fn the_reference_is_escaped_for_an_attribute() {
181 let (html, _) = render("A & B", r#"a"b.png"#);
182 assert!(html.contains(r#"src="a"b.png""#), "{html}");
183 assert!(html.contains(r#"alt="A & B""#), "{html}");
184 }
185}