use super::*;
use crate::asset_snapshot::VariantKindSet;
fn snapshot_with(
path: &str,
dims: Option<(u32, u32)>,
color: Option<&str>,
lqip: Option<&str>,
webp: bool,
) -> AssetSnapshot {
let mut s = AssetSnapshot::new();
let key = PathBuf::from(path);
if let Some(d) = dims {
s.dimensions.insert(key.clone(), d);
}
if let Some(c) = color {
s.dominant_color.insert(key.clone(), c.to_string());
}
if let Some(l) = lqip {
s.lqip.insert(key.clone(), l.to_string());
}
if webp {
let stem = crate::asset_snapshot::path_strip_extension(&key);
s.variants.insert(
stem,
VariantKindSet {
webp: true,
avif: false,
},
);
}
s
}
fn snapshot_dims(path: &str, w: u32, h: u32) -> AssetSnapshot {
snapshot_with(path, Some((w, h)), None, None, false)
}
#[test]
fn probe_paths_percent_decodes_src() {
let snap = snapshot_dims("assets/Europe - A Prophecy/e-006.jpg", 4515, 6158);
assert_eq!(
lookup_dims(&snap, "../../assets/Europe%20-%20A%20Prophecy/e-006.jpg"),
Some((4515, 6158))
);
}
#[test]
fn cover_slug_url_emits_real_dimensions_not_fallback() {
let mut snap = snapshot_dims("assets/Europe - A Prophecy/e-006.jpg", 4515, 6158);
snap.dimensions.insert(
PathBuf::from("assets/europe-a-prophecy/e-006.jpg"),
(4515, 6158),
);
let html = synthesize_image_html(
"/assets/europe-a-prophecy/e-006.jpg",
"cover",
&snap,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert!(
html.contains(r#"width="4515" height="6158""#),
"expected real dims, got: {html}"
);
assert!(
!html.contains(r#"width="800" height="600""#),
"800x600 fallback fired: {html}"
);
}
#[test]
fn picture_wrap_for_raster_original_no_lqip() {
let s = snapshot_dims("photo.jpg", 800, 600);
let html = synthesize_image_html(
"photo.jpg",
"A cat",
&s,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert_eq!(
html,
r#"<picture><source srcset="photo.webp" type="image/webp"><img src="photo.jpg" width="800" height="600" loading="lazy" alt="A cat" /></picture>"#
);
}
#[test]
fn picture_wrap_for_raster_original_with_lqip() {
let s = snapshot_with(
"photo.jpg",
Some((800, 600)),
None,
Some("data:image/jpeg;base64,abc"),
false,
);
let html = synthesize_image_html(
"photo.jpg",
"A cat",
&s,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert_eq!(
html,
r#"<picture><source srcset="photo.webp" type="image/webp"><img src="photo.jpg" width="800" height="600" loading="lazy" style="background-image:url(data:image/jpeg;base64,abc);background-size:cover" alt="A cat" /></picture>"#
);
}
#[test]
fn bare_img_with_dominant_color_no_lqip() {
let s = snapshot_with("photo.jpg", Some((800, 600)), Some("#aabbcc"), None, false);
let html = synthesize_image_html(
"photo.jpg",
"Cat",
&s,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert!(
html.contains(r#"style="background-color:#aabbcc""#),
"Got: {html}"
);
}
#[test]
fn picture_wrap_when_webp_exists() {
let s = snapshot_with("photo.jpg", Some((800, 600)), None, None, true);
let html = synthesize_image_html(
"photo.jpg",
"Cat",
&s,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert!(
html.starts_with(r#"<picture><source srcset="photo.webp" type="image/webp"><img"#),
"Got: {html}"
);
assert!(html.ends_with(r#"alt="Cat" /></picture>"#), "Got: {html}");
}
#[test]
fn picture_srcset_uses_to_webp_of_src_not_stored_variant() {
let s = snapshot_with("../assets/photo.jpg", Some((800, 600)), None, None, true);
let html = synthesize_image_html(
"../assets/photo.jpg",
"",
&s,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert!(
html.contains(r#"srcset="../assets/photo.webp""#),
"srcset should inherit src's prefix; got: {html}"
);
}
#[test]
fn eager_swaps_loading_attr_and_adds_fetchpriority() {
let s = snapshot_dims("photo.jpg", 1, 1);
let html = synthesize_image_html(
"photo.jpg",
"Hero",
&s,
ImageContext::Hero,
&ImageRenderOptions {
eager: true,
..Default::default()
},
);
assert!(
html.contains(r#"loading="eager" fetchpriority="high""#),
"Got: {html}"
);
assert!(!html.contains(r#"loading="lazy""#), "Got: {html}");
}
#[test]
fn favicon_is_bare_16x16_with_class() {
let s = AssetSnapshot::new();
let html = synthesize_image_html(
"https://example.com/favicon.ico",
"",
&s,
ImageContext::Favicon,
&ImageRenderOptions {
class: Some("link-preview-favicon"),
..Default::default()
},
);
assert_eq!(
html,
r#"<img class="link-preview-favicon" src="https://example.com/favicon.ico" width="16" height="16" alt="">"#
);
}
#[test]
fn synthesize_site_logo_basic_shape() {
let html = synthesize_image_html(
"assets/logo.png",
"",
&AssetSnapshot::new(),
ImageContext::SiteLogo,
&ImageRenderOptions::default(),
);
assert_eq!(
html,
r#"<img class="site-logo" src="assets/logo.png" alt="" aria-hidden="true">"#
);
}
#[test]
fn synthesize_site_logo_escapes_src() {
let html = synthesize_image_html(
r#"logo "with quotes".png"#,
"",
&AssetSnapshot::new(),
ImageContext::SiteLogo,
&ImageRenderOptions::default(),
);
assert!(
html.contains(r#"logo "with quotes".png"#),
"got: {html}"
);
}
#[test]
fn synthesize_site_logo_does_not_emit_picture_or_lqip() {
let mut snap = AssetSnapshot::new();
snap.lqip
.insert("logo.png".into(), "data:image/jpeg;base64,xxx".into());
let html = synthesize_image_html(
"logo.png",
"moss",
&snap,
ImageContext::SiteLogo,
&ImageRenderOptions::default(),
);
assert!(
!html.contains("<picture"),
"logo should not get picture wrap: {html}"
);
assert!(
!html.contains("background-image"),
"logo should not get LQIP: {html}"
);
assert!(
!html.contains("loading="),
"logo above-the-fold, no lazy-load: {html}"
);
}
#[test]
fn alt_with_quotes_is_escaped() {
let s = snapshot_dims("photo.jpg", 1, 1);
let html = synthesize_image_html(
"photo.jpg",
r#"Quote: "hi""#,
&s,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert!(
html.contains(r#"alt="Quote: "hi"""#),
"Got: {html}"
);
}
#[test]
fn extra_attrs_passed_through_verbatim() {
let s = snapshot_dims("hero.jpg", 1920, 1080);
let html = synthesize_image_html(
"hero.jpg",
"",
&s,
ImageContext::Hero,
&ImageRenderOptions {
eager: true,
extra_attrs: Some(r#"data-cover-fit="cover""#),
..Default::default()
},
);
assert!(html.contains(r#"data-cover-fit="cover""#), "Got: {html}");
let alt_pos = html.find("alt=").unwrap();
let extra_pos = html.find("data-cover-fit=").unwrap();
assert!(extra_pos > alt_pos, "extra attrs should come after alt");
}
#[test]
fn lqip_style_suppressed_when_extra_attrs_has_style() {
let s = snapshot_with(
"hero.jpg",
Some((1920, 1080)),
None,
Some("data:image/jpeg;base64,abc"),
false,
);
let html = synthesize_image_html(
"hero.jpg",
"",
&s,
ImageContext::Hero,
&ImageRenderOptions {
eager: true,
extra_attrs: Some(r#"style="object-fit:cover;object-position:50% 50%""#),
..Default::default()
},
);
assert_eq!(
html.matches("style=").count(),
1,
"expected exactly one style= attribute; got: {html}"
);
assert!(
html.contains(r#"style="object-fit:cover"#),
"caller-supplied style must survive; got: {html}"
);
assert!(
!html.contains(r#"background-image:url(data:"#),
"LQIP must be suppressed when extra_attrs carries style=; got: {html}"
);
}
fn empty_extras() -> std::collections::BTreeMap<String, String> {
std::collections::BTreeMap::new()
}
#[test]
fn markdown_standalone_no_caption_wraps_in_figure() {
let s = snapshot_dims("photo.jpg", 800, 600);
let extras = empty_extras();
let html = synthesize_image_html(
"photo.jpg",
"Alt text",
&s,
ImageContext::MarkdownStandalone {
caption: None,
width: None,
align: None,
class_names: &[],
extra_attrs: &extras,
},
&ImageRenderOptions::default(),
);
assert!(html.starts_with(r#"<figure class="moss-image">"#));
assert!(html.ends_with("</figure>"));
assert!(!html.contains("<figcaption>"));
assert!(html.contains("<img"));
}
#[test]
fn markdown_standalone_with_caption_adds_figcaption() {
let s = snapshot_dims("photo.jpg", 800, 600);
let extras = empty_extras();
let html = synthesize_image_html(
"photo.jpg",
"Alt text",
&s,
ImageContext::MarkdownStandalone {
caption: Some("A nice photo"),
width: None,
align: None,
class_names: &[],
extra_attrs: &extras,
},
&ImageRenderOptions::default(),
);
assert!(html.starts_with(r#"<figure class="moss-image">"#));
assert!(html.contains("<figcaption>A nice photo</figcaption>"));
assert!(html.ends_with("</figure>"));
}
#[test]
fn markdown_standalone_caption_is_html_escaped() {
let s = snapshot_dims("photo.jpg", 800, 600);
let extras = empty_extras();
let html = synthesize_image_html(
"photo.jpg",
"",
&s,
ImageContext::MarkdownStandalone {
caption: Some(r#"Q&A "best" of <em>2024</em>"#),
width: None,
align: None,
class_names: &[],
extra_attrs: &extras,
},
&ImageRenderOptions::default(),
);
assert!(
html.contains(
"<figcaption>Q&A "best" of <em>2024</em></figcaption>"
),
"caption text must be HTML-escaped at the boundary; got: {html}"
);
}
#[test]
fn markdown_standalone_wraps_picture_when_webp_present() {
let s = snapshot_with("photo.jpg", Some((1200, 800)), None, None, true);
let extras = empty_extras();
let html = synthesize_image_html(
"photo.jpg",
"Alt",
&s,
ImageContext::MarkdownStandalone {
caption: Some("Cap"),
width: None,
align: None,
class_names: &[],
extra_attrs: &extras,
},
&ImageRenderOptions::default(),
);
let fig_idx = html.find(r#"<figure class="moss-image">"#).expect("figure");
let pic_idx = html.find("<picture>").expect("picture");
let src_idx = html.find("<source").expect("source");
let img_idx = html.find("<img").expect("img");
let cap_idx = html.find("<figcaption>").expect("figcaption");
let fig_close = html.find("</figure>").expect("figure close");
assert!(fig_idx < pic_idx, "<picture> must be inside <figure>");
assert!(pic_idx < src_idx);
assert!(src_idx < img_idx);
assert!(img_idx < cap_idx, "<figcaption> follows <picture>");
assert!(cap_idx < fig_close);
}
#[test]
fn markdown_inline_does_not_wrap_in_figure() {
let s = snapshot_dims("photo.jpg", 800, 600);
let html = synthesize_image_html(
"photo.jpg",
"Alt",
&s,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert!(!html.contains("<figure"));
assert!(!html.contains("<figcaption>"));
}
#[test]
fn markdown_standalone_width_screen_emits_data_width_on_figure() {
let s = snapshot_dims("photo.jpg", 800, 600);
let extras = empty_extras();
let html = synthesize_image_html(
"photo.jpg",
"",
&s,
ImageContext::MarkdownStandalone {
caption: None,
width: Some("screen"),
align: None,
class_names: &[],
extra_attrs: &extras,
},
&ImageRenderOptions::default(),
);
assert!(
html.starts_with(r#"<figure class="moss-image" data-width="screen">"#),
"data-width must sit on the figure wrapper; got: {html}"
);
assert!(
!html.contains(r#"<img"#) || !html[html.find("<img").unwrap()..].contains("data-width="),
"inner <img> must not carry data-width; got: {html}"
);
}
#[test]
fn markdown_standalone_width_wide_with_caption() {
let s = snapshot_dims("photo.jpg", 800, 600);
let extras = empty_extras();
let html = synthesize_image_html(
"photo.jpg",
"Alt",
&s,
ImageContext::MarkdownStandalone {
caption: Some("A nice photo"),
width: Some("wide"),
align: None,
class_names: &[],
extra_attrs: &extras,
},
&ImageRenderOptions::default(),
);
assert!(html.contains(r#"data-width="wide""#), "got: {html}");
assert!(
html.contains("<figcaption>A nice photo</figcaption>"),
"got: {html}"
);
}
#[test]
fn markdown_standalone_width_none_omits_data_width() {
let s = snapshot_dims("photo.jpg", 800, 600);
let extras = empty_extras();
let html = synthesize_image_html(
"photo.jpg",
"",
&s,
ImageContext::MarkdownStandalone {
caption: None,
width: None,
align: None,
class_names: &[],
extra_attrs: &extras,
},
&ImageRenderOptions::default(),
);
assert!(!html.contains("data-width="), "got: {html}");
}
#[test]
fn wrap_in_figure_width_emits_data_width_attribute() {
let html = wrap_in_figure(r#"<img src="x" />"#, None, Some("page"));
assert_eq!(
html,
r#"<figure class="moss-image" data-width="page"><img src="x" /></figure>"#
);
}
#[test]
fn wrap_in_figure_width_with_caption() {
let html = wrap_in_figure(r#"<img src="x" />"#, Some("hello"), Some("screen"));
assert_eq!(
html,
r#"<figure class="moss-image" data-width="screen"><img src="x" /><figcaption>hello</figcaption></figure>"#
);
}
#[test]
fn wrap_in_figure_no_width_no_attribute() {
let html = wrap_in_figure(r#"<img src="x" />"#, None, None);
assert_eq!(
html,
r#"<figure class="moss-image"><img src="x" /></figure>"#
);
}
#[test]
fn markdown_standalone_no_manifest_still_wraps_in_figure() {
let s = AssetSnapshot::new();
let extras = empty_extras();
let html = synthesize_image_html(
"photo.jpg",
"Alt",
&s,
ImageContext::MarkdownStandalone {
caption: Some("Cap"),
width: None,
align: None,
class_names: &[],
extra_attrs: &extras,
},
&ImageRenderOptions::default(),
);
assert!(html.starts_with(r#"<figure class="moss-image">"#));
assert!(html.contains("<img"));
assert!(html.contains("<figcaption>Cap</figcaption>"));
assert!(html.ends_with("</figure>"));
}
#[test]
fn missing_dimensions_fall_back_to_800x600() {
let s = AssetSnapshot::new();
let html = synthesize_image_html(
"ghost.jpg",
"",
&s,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert!(html.contains(r#"width="800" height="600""#), "Got: {html}");
}
#[test]
fn synthesize_tracking_pixel_basic() {
let html = synthesize_image_html(
"https://api.mosspub.com/pixel.gif?u=abc",
"",
&AssetSnapshot::new(),
ImageContext::TrackingPixel,
&ImageRenderOptions::default(),
);
assert_eq!(
html,
r#"<img src="https://api.mosspub.com/pixel.gif?u=abc" alt="" width="1" height="1" />"#
);
}
#[test]
fn synthesize_tracking_pixel_escapes_url() {
let html = synthesize_image_html(
r#"x.gif?u=a&b="c""#,
"",
&AssetSnapshot::new(),
ImageContext::TrackingPixel,
&ImageRenderOptions::default(),
);
assert!(html.contains("&"), "& must be escaped");
assert!(html.contains("""), "\" must be escaped");
}
#[test]
fn synthesize_tracking_pixel_no_lazy_no_lqip() {
let mut snap = AssetSnapshot::new();
snap.lqip
.insert("pixel.gif".into(), "data:image/jpeg;base64,xxx".into());
let html = synthesize_image_html(
"pixel.gif",
"",
&snap,
ImageContext::TrackingPixel,
&ImageRenderOptions::default(),
);
assert!(
!html.contains("loading=\"lazy\""),
"must NOT lazy-load (must fire on read)"
);
assert!(!html.contains("background-image"), "must NOT carry LQIP");
assert!(!html.contains("<picture"), "must NOT wrap in picture");
}
#[test]
fn synthesize_email_body_with_dims() {
let html = synthesize_image_html(
"https://media.example.com/photo.jpg",
"Cover",
&AssetSnapshot::new(),
ImageContext::EmailBody {
width: Some(600),
height: Some(400),
},
&ImageRenderOptions::default(),
);
assert!(html.contains(r#"width="600""#));
assert!(html.contains(r#"height="400""#));
assert!(html.contains(r#"style="display:block;max-width:100%;height:auto;""#));
}
#[test]
fn synthesize_email_body_without_dims() {
let html = synthesize_image_html(
"x.jpg",
"alt",
&AssetSnapshot::new(),
ImageContext::EmailBody {
width: None,
height: None,
},
&ImageRenderOptions::default(),
);
assert!(
!html.contains("width="),
"width should be omitted when None"
);
assert!(
!html.contains("height="),
"height should be omitted when None"
);
}
#[test]
fn synthesize_email_body_no_picture_no_data() {
let html = synthesize_image_html(
"photo.jpg",
"alt",
&AssetSnapshot::new(),
ImageContext::EmailBody {
width: None,
height: None,
},
&ImageRenderOptions::default(),
);
assert!(
!html.contains("<picture"),
"email images must not use <picture>"
);
assert!(!html.contains("<source"), "no <source>");
assert!(!html.contains("data-"), "no data-* (email clients strip)");
assert!(
!html.contains("loading="),
"email clients ignore loading attr"
);
assert!(
!html.contains("background-image"),
"email clients strip inline style URLs"
);
}
#[test]
fn synthesize_email_body_escapes() {
let html = synthesize_image_html(
r#"https://x.com/photo.jpg?a=1&b=2"#,
r#"alt with "quotes""#,
&AssetSnapshot::new(),
ImageContext::EmailBody {
width: None,
height: None,
},
&ImageRenderOptions::default(),
);
assert!(html.contains("&"), "& must be escaped");
assert!(html.contains("""), "\" must be escaped");
}
#[test]
fn synthesize_gallery_thumb_emits_picture_with_lazy() {
let p = ImageRenderOptions::default();
let mut snap = AssetSnapshot::new();
snap.dimensions
.insert(PathBuf::from("photo.jpg"), (1200, 800));
let out = synthesize_image_html("photo.jpg", "alt", &snap, ImageContext::GalleryThumb, &p);
assert!(out.contains("<picture"), "{out}");
assert!(
out.contains(
r#"srcset="photo.w800.webp 800w, photo.webp 1200w" type="image/webp" sizes="(min-width: 48rem) 33vw, 100vw""#
),
"{out}"
);
assert!(out.contains(r#"loading="lazy""#), "{out}");
assert!(out.contains(r#"width="1200""#), "{out}");
assert!(out.contains(r#"height="800""#), "{out}");
assert!(out.contains(r#"alt="alt""#), "{out}");
}
#[test]
fn synthesize_gallery_thumb_non_raster_bare_img() {
let p = ImageRenderOptions::default();
let mut snap = AssetSnapshot::new();
snap.dimensions.insert(PathBuf::from("icon.svg"), (64, 64));
let out = synthesize_image_html("icon.svg", "", &snap, ImageContext::GalleryThumb, &p);
assert!(!out.contains("<picture"), "{out}");
assert!(!out.contains("<source"), "{out}");
assert!(out.contains(r#"loading="lazy""#), "{out}");
assert!(out.contains(r#"width="64""#), "{out}");
}
#[test]
fn synthesize_gallery_thumb_threads_extra_attrs() {
let snap = snapshot_with(
"photo.jpg",
Some((1200, 800)),
None,
Some("data:image/jpeg;base64,abc"),
false,
);
let opts = ImageRenderOptions {
extra_attrs: Some(r#"style="object-position:50% 50%""#),
..Default::default()
};
let out = synthesize_image_html("photo.jpg", "", &snap, ImageContext::GalleryThumb, &opts);
assert_eq!(
out.matches("style=").count(),
1,
"expected exactly one style= attribute; got: {out}"
);
assert!(
out.contains(r#"style="object-position:50% 50%""#),
"caller-supplied style must survive; got: {out}"
);
}
#[test]
fn synthesize_hero_bare_basic_shape() {
let out = synthesize_image_html(
"cover.jpg",
"",
&AssetSnapshot::new(),
ImageContext::HeroBare,
&ImageRenderOptions::default(),
);
assert_eq!(out, r#"<img src="cover.jpg" alt="" />"#, "got: {}", out);
}
#[test]
fn ladder_srcset_emitted_for_wide_raster() {
let assets = snapshot_dims("photo.jpg", 2000, 1200);
let html = synthesize_image_html(
"photo.jpg",
"alt",
&assets,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert!(
html.contains(
r#"<source srcset="photo.w800.webp 800w, photo.w1600.webp 1600w, photo.webp 2000w" type="image/webp" sizes="(min-width: 48rem) 47.25rem, 100vw">"#
),
"got: {html}"
);
}
#[test]
fn base_descriptor_caps_at_deploy_max_edge() {
let assets = snapshot_dims("photo.jpg", 4000, 3000);
let html = synthesize_image_html(
"photo.jpg",
"",
&assets,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert!(html.contains("photo.webp 2400w"), "got: {html}");
}
#[test]
fn no_ladder_below_first_rung_keeps_legacy_shape() {
let assets = snapshot_dims("photo.jpg", 800, 600);
let html = synthesize_image_html(
"photo.jpg",
"",
&assets,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert!(
html.contains(r#"<source srcset="photo.webp" type="image/webp">"#),
"got: {html}"
);
assert!(!html.contains("sizes="), "got: {html}");
}
#[test]
fn unknown_dims_keep_legacy_shape() {
let html = synthesize_image_html(
"photo.jpg",
"",
&AssetSnapshot::new(),
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert!(
html.contains(r#"<source srcset="photo.webp" type="image/webp">"#),
"got: {html}"
);
assert!(!html.contains("sizes="), "got: {html}");
}
#[test]
fn portrait_below_first_rung_keeps_legacy_shape() {
let assets = snapshot_dims("photo.jpg", 1200, 3600);
let html = synthesize_image_html(
"photo.jpg",
"",
&assets,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert!(
html.contains(r#"<source srcset="photo.webp" type="image/webp">"#),
"got: {html}"
);
assert!(!html.contains("sizes="), "got: {html}");
}
#[test]
fn portrait_base_descriptor_uses_post_resize_width() {
let assets = snapshot_dims("photo.jpg", 3024, 4032);
let html = synthesize_image_html(
"photo.jpg",
"",
&assets,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert!(
html.contains(r#"srcset="photo.w800.webp 800w, photo.w1600.webp 1600w, photo.webp 1800w""#),
"got: {html}"
);
}
#[test]
fn folder_card_cover_uses_card_sizes() {
let assets = snapshot_dims("cover.jpg", 2000, 1200);
let html = synthesize_image_html(
"cover.jpg",
"",
&assets,
ImageContext::FolderCardCover,
&ImageRenderOptions::default(),
);
assert!(
html.contains(r#"sizes="(min-width: 48rem) 24rem, 100vw""#),
"got: {html}"
);
}
#[test]
fn percent_encoded_src_gets_encoded_rung_urls() {
let assets = snapshot_dims("a b/photo.jpg", 2000, 1200);
let html = synthesize_image_html(
"a%20b/photo.jpg",
"",
&assets,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert!(
html.contains(
r#"srcset="a%20b/photo.w800.webp 800w, a%20b/photo.w1600.webp 1600w, a%20b/photo.webp 2000w""#
),
"got: {html}"
);
}
#[test]
fn comma_named_raster_encodes_srcset_commas_but_not_base_src() {
let assets = snapshot_dims("a,b.jpg", 2000, 1200);
let html = synthesize_image_html(
"a,b.jpg",
"cat",
&assets,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert_eq!(
html,
r#"<picture><source srcset="a%2Cb.w800.webp 800w, a%2Cb.w1600.webp 1600w, a%2Cb.webp 2000w" type="image/webp" sizes="(min-width: 48rem) 47.25rem, 100vw"><img src="a,b.jpg" width="2000" height="1200" loading="lazy" alt="cat" /></picture>"#
);
}
#[test]
fn comma_named_webp_source_encodes_img_srcset_commas_but_not_base_src() {
let assets = snapshot_dims("a,b.webp", 2000, 1200);
let html = synthesize_image_html(
"a,b.webp",
"cat",
&assets,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert_eq!(
html,
r#"<img src="a,b.webp" srcset="a%2Cb.w800.webp 800w, a%2Cb.w1600.webp 1600w, a%2Cb.webp 2000w" sizes="(min-width: 48rem) 47.25rem, 100vw" width="2000" height="1200" loading="lazy" alt="cat" />"#
);
}
#[test]
fn comma_named_raster_with_no_ladder_encodes_single_url_srcset() {
let assets = AssetSnapshot::new(); let html = synthesize_image_html(
"a,b.jpg",
"cat",
&assets,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert_eq!(
html,
r#"<picture><source srcset="a%2Cb.webp" type="image/webp"><img src="a,b.jpg" width="800" height="600" loading="lazy" alt="cat" /></picture>"#
);
let srcset_start = html.find("srcset=\"").unwrap() + "srcset=\"".len();
let srcset_end = html[srcset_start..].find('"').unwrap() + srcset_start;
assert!(
!html[srcset_start..srcset_end].contains(','),
"srcset candidate must not contain a literal comma; got: {html}"
);
assert!(html.contains(r#"src="a,b.jpg""#), "got: {html}");
}
#[test]
fn no_comma_filename_srcset_is_not_over_encoded() {
let assets = snapshot_dims("photo.jpg", 2000, 1200);
let html = synthesize_image_html(
"photo.jpg",
"cat",
&assets,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert!(!html.contains("%2C"), "no comma to encode; got: {html}");
assert_eq!(
html,
r#"<picture><source srcset="photo.w800.webp 800w, photo.w1600.webp 1600w, photo.webp 2000w" type="image/webp" sizes="(min-width: 48rem) 47.25rem, 100vw"><img src="photo.jpg" width="2000" height="1200" loading="lazy" alt="cat" /></picture>"#
);
}
#[test]
fn hero_context_uses_full_bleed_sizes() {
let assets = snapshot_dims("hero.jpg", 2400, 985);
let html = synthesize_image_html(
"hero.jpg",
"",
&assets,
ImageContext::Hero,
&ImageRenderOptions::default(),
);
assert!(html.contains(r#"sizes="100vw""#), "got: {html}");
}
#[test]
fn markdown_standalone_width_wide_uses_wide_band_sizes() {
let s = snapshot_dims("photo.jpg", 2000, 1200);
let extras = empty_extras();
let html = synthesize_image_html(
"photo.jpg",
"",
&s,
ImageContext::MarkdownStandalone {
caption: None,
width: Some("wide"),
align: None,
class_names: &[],
extra_attrs: &extras,
},
&ImageRenderOptions::default(),
);
assert!(
html.starts_with(r#"<figure class="moss-image" data-width="wide">"#),
"got: {html}"
);
assert!(
html.contains(r#"sizes="(min-width: 48rem) min(63rem, 100vw), 100vw""#),
"wide declares the wide escape band; got: {html}"
);
}
#[test]
fn markdown_standalone_width_page_uses_page_band_sizes() {
let s = snapshot_dims("photo.jpg", 2000, 1200);
let extras = empty_extras();
let html = synthesize_image_html(
"photo.jpg",
"",
&s,
ImageContext::MarkdownStandalone {
caption: None,
width: Some("page"),
align: None,
class_names: &[],
extra_attrs: &extras,
},
&ImageRenderOptions::default(),
);
assert!(
html.contains(r#"sizes="(min-width: 48rem) min(1200px, 100vw), 100vw""#),
"page declares the page escape band; got: {html}"
);
}
#[test]
fn explicit_sizes_option_overrides_context() {
let s = snapshot_dims("photo.jpg", 2000, 1200);
let html = synthesize_image_html(
"photo.jpg",
"",
&s,
ImageContext::MarkdownInline,
&ImageRenderOptions {
sizes: Some("(min-width: 48rem) calc(min(1200px, 100vw) / 3), 100vw"),
..Default::default()
},
);
assert!(
html.contains(r#"sizes="(min-width: 48rem) calc(min(1200px, 100vw) / 3), 100vw""#),
"options.sizes must override the context default; got: {html}"
);
}
#[test]
fn markdown_standalone_width_screen_uses_full_bleed_sizes() {
let s = snapshot_dims("photo.jpg", 2000, 1200);
let extras = empty_extras();
let html = synthesize_image_html(
"photo.jpg",
"",
&s,
ImageContext::MarkdownStandalone {
caption: None,
width: Some("screen"),
align: None,
class_names: &[],
extra_attrs: &extras,
},
&ImageRenderOptions::default(),
);
assert!(
html.starts_with(r#"<figure class="moss-image" data-width="screen">"#),
"got: {html}"
);
assert!(html.contains(r#"sizes="100vw""#), "got: {html}");
}
#[test]
fn email_body_output_unchanged_by_ladder() {
let assets = snapshot_dims("photo.jpg", 2000, 1200);
let html = synthesize_image_html(
"photo.jpg",
"a",
&assets,
ImageContext::EmailBody {
width: Some(2000),
height: Some(1200),
},
&ImageRenderOptions::default(),
);
assert!(
!html.contains("srcset"),
"email must never carry srcset: {html}"
);
assert!(!html.contains("<picture"), "got: {html}");
}
#[test]
fn synthesize_hero_bare_with_style_via_extra_attrs() {
let out = synthesize_image_html(
"cover.jpg",
"",
&AssetSnapshot::new(),
ImageContext::HeroBare,
&ImageRenderOptions {
extra_attrs: Some(r#"style="object-fit:cover;object-position:50% 50%""#),
..Default::default()
},
);
assert_eq!(
out, r#"<img src="cover.jpg" alt="" style="object-fit:cover;object-position:50% 50%" />"#,
"got: {}",
out
);
}
#[test]
fn synthesize_hero_bare_with_lqip_in_snapshot_still_bare() {
let mut snap = AssetSnapshot::new();
snap.lqip
.insert("cover.jpg".into(), "data:image/jpeg;base64,xxx".into());
snap.dimensions.insert("cover.jpg".into(), (1920, 1080));
let out = synthesize_image_html(
"cover.jpg",
"",
&snap,
ImageContext::HeroBare,
&ImageRenderOptions::default(),
);
assert!(!out.contains("<picture"), "got: {}", out);
assert!(!out.contains("<source"), "got: {}", out);
assert!(!out.contains("background-image"), "got: {}", out);
assert!(!out.contains("width="), "got: {}", out);
assert!(!out.contains("height="), "got: {}", out);
assert!(!out.contains("loading="), "got: {}", out);
}
#[test]
fn synthesize_hero_bare_escapes_url() {
let out = synthesize_image_html(
r#"x.jpg?a=1&b="c""#,
"",
&AssetSnapshot::new(),
ImageContext::HeroBare,
&ImageRenderOptions::default(),
);
assert!(out.contains("&"), "& must be escaped; got: {}", out);
assert!(out.contains("""), "\" must be escaped; got: {}", out);
}
#[test]
fn synthesize_hero_bare_byte_shape_matches_legacy_fallback() {
let href = "covers/img.jpg";
let style_fragment = r#" style="object-fit:contain""#;
let legacy = format!(
"<img src=\"{}\" alt=\"\"{} />",
html_escape(href),
style_fragment,
);
let pr2 = synthesize_image_html(
href,
"",
&AssetSnapshot::new(),
ImageContext::HeroBare,
&ImageRenderOptions {
extra_attrs: Some(style_fragment.trim_start()),
..Default::default()
},
);
assert_eq!(
pr2, legacy,
"byte shape divergence: pr2={} legacy={}",
pr2, legacy
);
}
fn snapshot_dims_animated(path: &str, w: u32, h: u32, animated: bool) -> AssetSnapshot {
let mut s = snapshot_dims(path, w, h);
s.animated.insert(PathBuf::from(path), animated);
s
}
#[test]
fn webp_source_wide_emits_img_srcset_no_picture() {
let assets = snapshot_dims("photo.webp", 1866, 1866);
let html = synthesize_image_html(
"photo.webp",
"alt",
&assets,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert_eq!(
html,
r#"<img src="photo.webp" srcset="photo.w800.webp 800w, photo.w1600.webp 1600w, photo.webp 1866w" sizes="(min-width: 48rem) 47.25rem, 100vw" width="1866" height="1866" loading="lazy" alt="alt" />"#
);
assert!(
!html.contains("<picture"),
"webp source must NOT be wrapped in <picture>: {html}"
);
}
#[test]
fn webp_source_small_is_byte_identical_bare_img() {
let assets = snapshot_dims("photo.webp", 600, 400);
let html = synthesize_image_html(
"photo.webp",
"alt",
&assets,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert_eq!(
html,
r#"<img src="photo.webp" width="600" height="400" loading="lazy" alt="alt" />"#
);
}
#[test]
fn webp_source_animated_is_byte_identical_bare_img() {
let assets = snapshot_dims_animated("loop.webp", 1866, 1866, true);
let html = synthesize_image_html(
"loop.webp",
"alt",
&assets,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert_eq!(
html,
r#"<img src="loop.webp" width="1866" height="1866" loading="lazy" alt="alt" />"#
);
assert!(
!html.contains("srcset"),
"animated webp must not carry srcset: {html}"
);
}
#[test]
fn webp_source_explicit_non_animated_flag_ladders() {
let assets = snapshot_dims_animated("photo.webp", 2000, 1200, false);
let html = synthesize_image_html(
"photo.webp",
"",
&assets,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert!(
html.contains(r#"srcset="photo.w800.webp 800w, photo.w1600.webp 1600w, photo.webp 2000w""#),
"got: {html}"
);
}
#[test]
fn webp_source_card_context_uses_card_sizes() {
let assets = snapshot_dims("cover.webp", 2000, 1200);
let html = synthesize_image_html(
"cover.webp",
"",
&assets,
ImageContext::FolderCardCover,
&ImageRenderOptions::default(),
);
assert!(
html.contains(
r#"srcset="cover.w800.webp 800w, cover.w1600.webp 1600w, cover.webp 2000w" sizes="(min-width: 48rem) 24rem, 100vw""#
),
"got: {html}"
);
assert!(!html.contains("<picture"), "got: {html}");
}
#[test]
fn webp_source_gallery_context_uses_gallery_sizes() {
let assets = snapshot_dims("g.webp", 2000, 1200);
let html = synthesize_image_html(
"g.webp",
"",
&assets,
ImageContext::GalleryThumb,
&ImageRenderOptions::default(),
);
assert!(
html.contains(r#"sizes="(min-width: 48rem) 33vw, 100vw""#),
"got: {html}"
);
assert!(!html.contains("<picture"), "got: {html}");
}
#[test]
fn webp_source_hero_context_uses_full_bleed_sizes() {
let assets = snapshot_dims("hero.webp", 2400, 985);
let html = synthesize_image_html(
"hero.webp",
"",
&assets,
ImageContext::Hero,
&ImageRenderOptions::default(),
);
assert!(
html.contains(
r#"srcset="hero.w800.webp 800w, hero.w1600.webp 1600w, hero.webp 2400w" sizes="100vw""#
),
"got: {html}"
);
}
#[test]
fn webp_source_portrait_base_descriptor_uses_post_resize_width() {
let assets = snapshot_dims("photo.webp", 3024, 4032);
let html = synthesize_image_html(
"photo.webp",
"",
&assets,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert!(
html.contains(r#"srcset="photo.w800.webp 800w, photo.w1600.webp 1600w, photo.webp 1800w""#),
"got: {html}"
);
assert!(
html.contains(r#"width="3024" height="4032""#),
"got: {html}"
);
}
#[test]
fn webp_source_unknown_dims_is_bare_img() {
let html = synthesize_image_html(
"photo.webp",
"",
&AssetSnapshot::new(),
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert!(!html.contains("srcset"), "got: {html}");
assert!(!html.contains("<picture"), "got: {html}");
assert!(html.starts_with(r#"<img src="photo.webp""#), "got: {html}");
}
#[test]
fn webp_source_uppercase_ext_ladders_and_uses_src_case() {
let assets = snapshot_dims("photo.WEBP", 2000, 1200);
let html = synthesize_image_html(
"photo.WEBP",
"",
&assets,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert!(
!html.contains("<picture"),
"uppercase .WEBP must ladder as <img srcset>: {html}"
);
assert!(
html.contains(r#"srcset="photo.w800.webp 800w, photo.w1600.webp 1600w, photo.WEBP 2000w""#),
"got: {html}"
);
}
#[test]
fn webp_source_lqip_style_survives_on_laddered_img() {
let assets = snapshot_with(
"photo.webp",
Some((2000, 1200)),
None,
Some("data:image/jpeg;base64,abc"),
false,
);
let html = synthesize_image_html(
"photo.webp",
"",
&assets,
ImageContext::MarkdownInline,
&ImageRenderOptions::default(),
);
assert!(
html.contains(r#"srcset="photo.w800.webp 800w"#),
"got: {html}"
);
assert!(
html.contains(
r#"style="background-image:url(data:image/jpeg;base64,abc);background-size:cover""#
),
"LQIP must survive on the laddered webp <img>: {html}"
);
}
#[test]
fn webp_source_in_email_body_never_leaks_srcset() {
let assets = snapshot_dims("photo.webp", 2000, 1200);
let html = synthesize_image_html(
"photo.webp",
"alt",
&assets,
ImageContext::EmailBody {
width: Some(2000),
height: Some(1200),
},
&ImageRenderOptions::default(),
);
assert!(
!html.contains("srcset"),
"email webp must never carry srcset: {html}"
);
assert!(
!html.contains("<picture"),
"email webp must never wrap in <picture>: {html}"
);
assert!(
html.contains(r#"style="display:block;max-width:100%;height:auto;""#),
"email <img> shape preserved: {html}"
);
}