use super::super::document::BlockMeta;
use super::super::hooks::DefaultHooks;
use super::super::node::Inline;
use super::super::shortcode::HeroShortcode;
use super::super::url::{Url, UrlKind};
use super::*;
fn hero_block(image_url: Option<&str>, overlay: Vec<Block>) -> Block {
Block::Shortcode(Shortcode::Hero(HeroShortcode {
image: image_url.map(|u| Url::resolved(u, UrlKind::Asset)),
extra_images: Vec::new(),
attrs: String::new(),
classes: String::new(),
overlay,
overlay_text: String::new(),
width: None,
mobile: None,
caption: String::new(),
}))
}
#[test]
fn caption_is_read_from_every_hero_syntax() {
let cfg = ParseConfig::default();
let cap = "Cover: the memorial wall (photo: A. Photographer)";
let (attr_form, _, _) = parse_hero(&format!("{{image=cover.jpg caption=\"{cap}\"}}"), "", &cfg);
assert_eq!(attr_form.caption, cap);
assert_eq!(attr_form.overlay_text, "", "a caption is not overlay text");
let (directive_form, _, _) = parse_hero(&format!("./cover.jpg {{caption=\"{cap}\"}}"), "", &cfg);
assert_eq!(directive_form.caption, cap);
let (body_form, _, _) = parse_hero(&format!("{{caption=\"{cap}\"}}"), "![[cover.jpg]]\n", &cfg);
assert_eq!(body_form.caption, cap);
let (plain, _, _) = parse_hero("{image=cover.jpg}", "", &cfg);
assert_eq!(plain.caption, "", "no caption written, none invented");
}
#[test]
fn extract_hero_returns_none_when_no_hero_present() {
let mut doc = Document::from_blocks(vec![Block::Paragraph(vec![Inline::Text("plain".into())])]);
let hooks = DefaultHooks::new();
let extraction = extract_hero(&mut doc, &hooks);
assert!(extraction.is_none());
assert_eq!(doc.blocks.len(), 1, "doc unchanged when no hero");
}
#[test]
fn extract_hero_removes_top_level_hero_from_doc() {
let mut doc = Document::from_blocks(vec![
hero_block(Some("hero.jpg"), vec![]),
Block::Paragraph(vec![Inline::Text("body".into())]),
]);
let hooks = DefaultHooks::new();
let extraction = extract_hero(&mut doc, &hooks).expect("hero found");
assert!(!extraction.html.is_empty(), "hero rendered");
assert_eq!(extraction.image_url.as_deref(), Some("hero.jpg"));
assert_eq!(doc.blocks.len(), 1, "hero removed from doc");
assert!(matches!(&doc.blocks[0], Block::Paragraph(_)));
}
#[test]
fn extract_hero_captures_image_url_from_resolved_url() {
let mut doc = Document::from_blocks(vec![hero_block(Some("path/to/hero.webp"), vec![])]);
let hooks = DefaultHooks::new();
let extraction = extract_hero(&mut doc, &hooks).expect("hero found");
assert_eq!(extraction.image_url.as_deref(), Some("path/to/hero.webp"));
}
#[test]
fn extract_hero_image_url_none_when_no_hero_image() {
let mut doc = Document::from_blocks(vec![hero_block(None, vec![])]);
let hooks = DefaultHooks::new();
let extraction = extract_hero(&mut doc, &hooks).expect("hero found");
assert!(extraction.image_url.is_none());
}
#[test]
fn extract_hero_overlay_text_walks_typed_blocks() {
let overlay = vec![Block::Paragraph(vec![
Inline::Text("Hello, ".into()),
Inline::Strong(vec![Inline::Text("world".into())]),
])];
let mut doc = Document::from_blocks(vec![hero_block(None, overlay)]);
let hooks = DefaultHooks::new();
let extraction = extract_hero(&mut doc, &hooks).expect("hero found");
assert_eq!(extraction.overlay_text.as_deref(), Some("Hello, world"));
}
#[test]
fn extract_hero_overlay_text_skips_leading_heading() {
let overlay = vec![
Block::Heading {
level: 2,
children: vec![Inline::Text("Title".into())],
id: None,
},
Block::Paragraph(vec![Inline::Text("Description".into())]),
];
let mut doc = Document::from_blocks(vec![hero_block(None, overlay)]);
let hooks = DefaultHooks::new();
let extraction = extract_hero(&mut doc, &hooks).expect("hero found");
assert_eq!(extraction.overlay_text.as_deref(), Some("Description"));
}
#[test]
fn extract_hero_finds_only_first_hero_when_multiple_present() {
let mut doc = Document::from_blocks(vec![
hero_block(Some("a.jpg"), vec![]),
Block::Paragraph(vec![Inline::Text("middle".into())]),
hero_block(Some("b.jpg"), vec![]),
]);
let hooks = DefaultHooks::new();
let extraction = extract_hero(&mut doc, &hooks).expect("hero found");
assert_eq!(extraction.image_url.as_deref(), Some("a.jpg"));
assert_eq!(doc.blocks.len(), 2);
}
#[test]
fn extract_hero_passes_source_line_to_render_shortcode() {
let hero = hero_block(None, vec![]);
let meta_with_line = BlockMeta {
source_line: Some(3),
..BlockMeta::default()
};
let mut doc = Document::from_blocks_with_meta(vec![hero], vec![meta_with_line]);
let hooks = DefaultHooks::new();
let extraction = extract_hero(&mut doc, &hooks).expect("hero found");
assert!(
extraction.html.contains(r#"data-source-range="3-3""#),
"hoisted hero should carry data-source-range from block_meta, got: {}",
&extraction.html[..extraction.html.len().min(300)]
);
}
#[test]
fn extract_hero_source_line_none_when_meta_absent() {
let hero = hero_block(None, vec![]);
let mut doc = Document::from_blocks(vec![hero]); let hooks = DefaultHooks::new();
let extraction = extract_hero(&mut doc, &hooks).expect("hero found");
assert!(
!extraction.html.contains("data-source-range"),
"no source_line in meta → no data-source-range, got: {}",
&extraction.html[..extraction.html.len().min(300)]
);
}
#[test]
fn extract_hero_with_no_top_level_hero_in_nested_block() {
let mut doc = Document::from_blocks(vec![Block::BlockQuote(vec![hero_block(
Some("nested.jpg"),
vec![],
)])]);
let hooks = DefaultHooks::new();
let extraction = extract_hero(&mut doc, &hooks);
assert!(extraction.is_none(), "nested hero NOT extracted");
assert_eq!(doc.blocks.len(), 1);
}