use crate::asset_paths::{to_mp4, to_thumb};
use crate::asset_snapshot::AssetSnapshot;
use crate::resolve::embed_renderer::html_escape_attr;
use crate::resolve::title_params::TitleParams;
use std::path::PathBuf;
pub const AMBIENT_PLAYBACK_ATTRS: &str = r#"muted loop playsinline preload="metadata""#;
pub fn synthesize_video_html(
params: &TitleParams,
src: &str,
assets: &AssetSnapshot,
) -> String {
let converted_src = to_mp4(src);
let thumb = to_thumb(src);
let (width_attr, height_attr) = if let (Some(w), Some(h)) = (params.get("width"), params.get("height")) {
(
format!(r#" width="{}""#, html_escape_attr(w)),
format!(r#" height="{}""#, html_escape_attr(h)),
)
} else if let Some(w) = params.get("width") {
(
format!(r#" width="{}""#, html_escape_attr(w)),
String::new(),
)
} else {
match assets.dims(&PathBuf::from(src)) {
Some((w, h)) if w > 0 && h > 0 => (
format!(r#" width="{}""#, w),
format!(r#" height="{}""#, h),
),
_ => (String::new(), String::new()),
}
};
let is_loop = params.get("loop").is_some();
let (playback, data_loop) = if is_loop {
(
format!("autoplay {}", AMBIENT_PLAYBACK_ATTRS),
r#" data-loop"#,
)
} else {
(r#"controls preload="metadata""#.to_string(), "")
};
format!(
r#"<video class="moss-embed moss-embed-video" data-type="video"{data_loop} src="{src}" data-placeholder-src="{orig}" poster="{thumb}" data-thumb-src="{thumb}" {playback}{w}{h}></video>"#,
data_loop = data_loop,
src = html_escape_attr(&converted_src),
orig = html_escape_attr(src),
thumb = html_escape_attr(&thumb),
playback = playback,
w = width_attr,
h = height_attr,
)
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_snapshot() -> AssetSnapshot {
AssetSnapshot::new()
}
fn params_with(kvs: &[(&str, &str)]) -> TitleParams {
let mut p = TitleParams::default();
for (k, v) in kvs {
p.insert(*k, *v);
}
p
}
#[test]
fn video_basic_shape() {
let p = params_with(&[("kind", "video")]);
let out = synthesize_video_html(&p, "clip.mp4", &empty_snapshot());
assert!(out.contains("<video"), "got: {}", out);
assert!(out.contains(r#"src="clip.mp4""#), "got: {}", out);
}
#[test]
fn video_emits_moss_embed_classes() {
let p = params_with(&[("kind", "video")]);
let out = synthesize_video_html(&p, "clip.mp4", &empty_snapshot());
assert!(
out.contains(r#"class="moss-embed moss-embed-video""#),
"got: {}",
out
);
}
#[test]
fn video_emits_closing_tag() {
let p = params_with(&[("kind", "video")]);
let out = synthesize_video_html(&p, "clip.mp4", &empty_snapshot());
assert!(out.ends_with("</video>"), "got: {}", out);
}
#[test]
fn video_emits_controls_on_default_path() {
let p = params_with(&[("kind", "video")]);
let out = synthesize_video_html(&p, "clip.mp4", &empty_snapshot());
assert!(out.contains(" controls"), "default path must emit controls, got: {}", out);
}
#[test]
fn video_emits_preload_metadata() {
let p = params_with(&[("kind", "video")]);
let out = synthesize_video_html(&p, "clip.mp4", &empty_snapshot());
assert!(out.contains(r#"preload="metadata""#), "got: {}", out);
}
#[test]
fn video_single_src_no_nested_source() {
let p = params_with(&[("kind", "video")]);
let out = synthesize_video_html(&p, "clip.mp4", &empty_snapshot());
assert!(!out.contains("<source"), "must not emit <source>: {}", out);
}
#[test]
fn video_mov_extension_swaps_to_mp4() {
let p = params_with(&[("kind", "video")]);
let out = synthesize_video_html(&p, "clip.mov", &empty_snapshot());
assert!(
out.contains(r#"src="clip.mp4""#),
"expected .mov swapped to .mp4, got: {}",
out
);
assert!(
!out.contains(r#" src="clip.mov""#),
"raw .mov must not appear in src= after swap, got: {}",
out
);
}
#[test]
fn video_uppercase_mov_extension_swaps_to_mp4() {
let p = params_with(&[("kind", "video")]);
let out = synthesize_video_html(&p, "clip.MOV", &empty_snapshot());
assert!(
out.contains(r#"src="clip.mp4""#),
"expected .MOV swapped to .mp4, got: {}",
out
);
}
#[test]
fn video_mp4_extension_pass_through() {
let p = params_with(&[("kind", "video")]);
let out = synthesize_video_html(&p, "../assets/clip.mp4", &empty_snapshot());
assert!(
out.contains(r#"src="../assets/clip.mp4""#),
"got: {}",
out
);
}
#[test]
fn video_webm_extension_pass_through() {
let p = params_with(&[("kind", "video")]);
let out = synthesize_video_html(&p, "clip.webm", &empty_snapshot());
assert!(out.contains(r#"src="clip.webm""#), "got: {}", out);
}
#[test]
fn video_emits_width_param_when_present() {
let p = params_with(&[("kind", "video"), ("width", "640px")]);
let out = synthesize_video_html(&p, "clip.mp4", &empty_snapshot());
assert!(out.contains(r#"width="640px""#), "got: {}", out);
}
#[test]
fn video_emits_height_param_when_present() {
let p = params_with(&[("kind", "video"), ("width", "640px"), ("height", "360px")]);
let out = synthesize_video_html(&p, "clip.mp4", &empty_snapshot());
assert!(out.contains(r#"width="640px""#), "got: {}", out);
assert!(out.contains(r#"height="360px""#), "got: {}", out);
}
#[test]
fn video_no_width_or_height_attrs_when_snapshot_empty() {
let p = params_with(&[("kind", "video")]);
let out = synthesize_video_html(&p, "clip.mp4", &empty_snapshot());
assert!(!out.contains("width="), "got: {}", out);
assert!(!out.contains("height="), "got: {}", out);
}
#[test]
fn video_src_is_html_escaped() {
let p = params_with(&[("kind", "video")]);
let out = synthesize_video_html(&p, "q&a.mp4", &empty_snapshot());
assert!(out.contains(r#"src="q&a.mp4""#), "got: {}", out);
}
#[test]
fn synth_video_mov_to_mp4() {
let p = params_with(&[("kind", "video")]);
let out = synthesize_video_html(&p, "clip.mov", &empty_snapshot());
assert!(
out.contains(r#"src="clip.mp4""#),
"expected src= to point at transcoded mp4, got: {}",
out
);
assert!(
out.contains(r#"data-placeholder-src="clip.mov""#),
"expected data-placeholder-src= to point at original mov, got: {}",
out
);
}
#[test]
fn synth_video_poster_and_thumb() {
let p = params_with(&[("kind", "video")]);
let out = synthesize_video_html(&p, "clip.mov", &empty_snapshot());
assert!(
out.contains(r#"poster="clip.thumb.jpg""#),
"expected poster= from to_thumb(src), got: {}",
out
);
assert!(
out.contains(r#"data-thumb-src="clip.thumb.jpg""#),
"expected data-thumb-src= from to_thumb(src), got: {}",
out
);
}
#[test]
fn synth_video_dims_from_snapshot() {
let p = params_with(&[("kind", "video")]);
let mut snap = AssetSnapshot::new();
snap.dimensions.insert(PathBuf::from("clip.mov"), (1920, 1080));
let out = synthesize_video_html(&p, "clip.mov", &snap);
assert!(out.contains(r#"width="1920""#), "got: {}", out);
assert!(out.contains(r#"height="1080""#), "got: {}", out);
}
#[test]
fn synth_video_omits_zero_dims() {
let p = params_with(&[("kind", "video")]);
let mut snap = AssetSnapshot::new();
snap.dimensions.insert(PathBuf::from("clip.mov"), (0, 0));
let out = synthesize_video_html(&p, "clip.mov", &snap);
assert!(
!out.contains("width="),
"(0, 0) sentinel must NOT produce width=, got: {}",
out
);
assert!(
!out.contains("height="),
"(0, 0) sentinel must NOT produce height=, got: {}",
out
);
}
#[test]
fn loop_keyword_emits_autoplay_muted_loop_playsinline_no_controls() {
let p = params_with(&[("kind", "video"), ("loop", "1")]);
let out = synthesize_video_html(&p, "clip.mp4", &empty_snapshot());
assert!(out.contains(" autoplay"), "missing autoplay, got: {}", out);
assert!(out.contains(" muted"), "missing muted, got: {}", out);
assert!(out.contains(" loop"), "missing loop, got: {}", out);
assert!(out.contains(" playsinline"), "missing playsinline, got: {}", out);
assert!(!out.contains(" controls"), "controls must be absent on loop branch, got: {}", out);
}
#[test]
fn loop_keyword_emits_data_type_and_data_loop() {
let p = params_with(&[("kind", "video"), ("loop", "1")]);
let out = synthesize_video_html(&p, "clip.mp4", &empty_snapshot());
assert!(out.contains(r#"data-type="video""#), "missing data-type=video, got: {}", out);
assert!(out.contains(" data-loop"), "missing data-loop attribute, got: {}", out);
}
#[test]
fn default_path_emits_controls() {
let p = params_with(&[("kind", "video")]);
let out = synthesize_video_html(&p, "clip.mp4", &empty_snapshot());
assert!(out.contains(" controls"), "default path must emit controls, got: {}", out);
}
#[test]
fn loop_with_size_emits_width_height_and_loop_set() {
let p = params_with(&[("kind", "video"), ("loop", "1"), ("width", "640px"), ("height", "360px")]);
let out = synthesize_video_html(&p, "clip.mp4", &empty_snapshot());
assert!(out.contains(r#"width="640px""#), "missing width, got: {}", out);
assert!(out.contains(r#"height="360px""#), "missing height, got: {}", out);
assert!(out.contains(" autoplay"), "missing autoplay, got: {}", out);
assert!(out.contains(" loop"), "missing loop, got: {}", out);
assert!(!out.contains(" controls"), "controls must be absent on loop branch, got: {}", out);
}
#[test]
fn data_type_video_emitted_on_default_branch() {
let p = params_with(&[("kind", "video")]);
let out = synthesize_video_html(&p, "clip.mp4", &empty_snapshot());
assert!(out.contains(r#"data-type="video""#), "missing data-type=video on default branch, got: {}", out);
}
}