lattice_plugin_host/
media_source.rs1use std::path::{Path, PathBuf};
8
9use lattice_mode::media_source::{AsyncMediaSource, MediaBlockRequest, MediaFuture};
10
11use crate::PluginId;
12use crate::boundary_decoration::project_decoration_context;
13use crate::media_task::MediaClient;
14
15#[derive(Clone, Debug)]
17pub struct WasmMediaSource {
18 client: MediaClient,
19}
20
21impl AsyncMediaSource for WasmMediaSource {
22 fn source_id(&self) -> u64 {
23 self.plugin_id().0 as u64
24 }
25
26 fn produce(
27 &self,
28 buffer_id: u64,
29 path: Option<PathBuf>,
30 line_count: u32,
31 text: String,
32 ) -> MediaFuture<'_> {
33 Box::pin(async move {
34 self.media_blocks(buffer_id, path.as_deref(), line_count, text)
35 .await
36 })
37 }
38}
39
40impl WasmMediaSource {
41 pub fn new(client: MediaClient) -> Self {
42 Self { client }
43 }
44
45 pub fn plugin_id(&self) -> PluginId {
46 self.client.id()
47 }
48
49 pub async fn media_blocks(
62 &self,
63 buffer_id: u64,
64 path: Option<&Path>,
65 line_count: u32,
66 text: String,
67 ) -> Result<Vec<MediaBlockRequest>, String> {
68 let ctx = project_decoration_context(buffer_id, path, line_count);
69 let wit: Vec<crate::media_task::MediaBlock> = match self.client.produce(ctx, text).await {
70 Ok(inner) => inner?,
71 Err(host_err) => return Err(format!("media plugin: {host_err}")),
72 };
73 let base = path.and_then(|p| p.parent()).map(Path::to_path_buf);
77 Ok(wit
78 .into_iter()
79 .filter_map(|b| resolve_block(b, base.as_deref()))
80 .collect())
81 }
82}
83
84fn resolve_block(
89 b: crate::media_task::MediaBlock,
90 base: Option<&Path>,
91) -> Option<MediaBlockRequest> {
92 if b.path.trim().is_empty() {
93 tracing::warn!("media block with an empty path; dropped");
94 return None;
95 }
96 let raw = lattice_core::home::expand_tilde_path(Path::new(&b.path));
103 let resolved = if raw.has_root() {
107 raw
108 } else {
109 match base {
110 Some(dir) => dir.join(raw),
111 None => {
115 tracing::warn!(path = %b.path, "relative media path in a pathless buffer; dropped");
116 return None;
117 }
118 }
119 };
120 Some(MediaBlockRequest {
121 anchor_line: b.anchor_line,
122 path: resolved,
123 alt: b.alt.filter(|a| !a.trim().is_empty()),
124 fit: match b.fit {
125 crate::media_task::MediaFit::Contain => lattice_cells::MediaFit::Contain,
126 crate::media_task::MediaFit::Width => lattice_cells::MediaFit::Width,
127 },
128 })
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::media_task::{MediaBlock as WitBlock, MediaFit as WitFit};
135
136 fn wit(path: &str) -> WitBlock {
137 WitBlock {
138 anchor_line: 3,
139 path: path.to_string(),
140 alt: None,
141 fit: WitFit::Contain,
142 }
143 }
144
145 #[test]
148 fn a_relative_path_resolves_against_the_buffers_directory() {
149 let base = Path::new("/home/u/notes");
150 let got = resolve_block(wit("img/diagram.png"), Some(base)).expect("resolves");
151 assert_eq!(got.path, Path::new("/home/u/notes/img/diagram.png"));
152 }
153
154 #[test]
158 fn a_tilde_path_expands_rather_than_being_joined_onto_the_buffer_dir() {
159 let home = dirs::home_dir().expect("a home directory");
160 let got = resolve_block(wit("~/pics/shot.png"), Some(Path::new("/home/u/notes")))
161 .expect("resolves");
162 assert_eq!(got.path, home.join("pics/shot.png"));
163 }
164
165 #[test]
168 fn a_tilde_user_path_is_not_resolved_against_our_home() {
169 let got = resolve_block(wit("~someone/x.png"), Some(Path::new("/home/u/notes")))
170 .expect("resolves");
171 assert_eq!(got.path, Path::new("/home/u/notes/~someone/x.png"));
172 }
173
174 #[test]
175 fn an_absolute_path_is_left_alone() {
176 let got = resolve_block(wit("/tmp/x.png"), Some(Path::new("/home/u"))).unwrap();
177 assert_eq!(got.path, Path::new("/tmp/x.png"));
178 }
179
180 #[test]
183 fn a_relative_path_in_a_pathless_buffer_is_dropped() {
184 assert!(resolve_block(wit("diagram.png"), None).is_none());
185 }
186
187 #[test]
188 fn an_empty_path_is_dropped_rather_than_resolved() {
189 assert!(resolve_block(wit(" "), Some(Path::new("/a"))).is_none());
190 }
191
192 #[test]
195 fn a_blank_alt_becomes_none_so_the_filename_fallback_applies() {
196 let mut b = wit("/x.png");
197 b.alt = Some(" ".into());
198 assert_eq!(resolve_block(b, None).unwrap().alt, None);
199 }
200}