use super::document::Document;
use super::node::{Block, Inline};
use super::shortcode::Shortcode;
pub fn find_first_block_image(doc: &Document) -> Option<&Inline> {
find_first_image_in_blocks(&doc.blocks)
}
fn find_first_image_in_blocks(blocks: &[Block]) -> Option<&Inline> {
for block in blocks {
if let Some(img) = find_first_image_in_block(block) {
return Some(img);
}
}
None
}
fn find_first_image_in_block(block: &Block) -> Option<&Inline> {
match block {
Block::Figure { image, .. } => Some(image),
Block::Paragraph(inlines) => find_first_image_in_inlines(inlines),
Block::List { items, .. } => {
for item in items {
if let Some(img) = find_first_image_in_blocks(item) {
return Some(img);
}
}
None
}
Block::BlockQuote(children) => find_first_image_in_blocks(children),
Block::Callout { children, .. } => find_first_image_in_blocks(children),
Block::Table { header, rows, .. } => {
for cell in header {
if let Some(img) = find_first_image_in_inlines(cell) {
return Some(img);
}
}
for row in rows {
for cell in row {
if let Some(img) = find_first_image_in_inlines(cell) {
return Some(img);
}
}
}
None
}
Block::Shortcode(sc) => find_first_image_in_shortcode(sc),
Block::LinkCard { children, .. } => find_first_image_in_blocks(children),
Block::Heading { children, .. } => find_first_image_in_inlines(children),
Block::CodeBlock { .. } | Block::ThematicBreak | Block::Other(_) => None,
}
}
fn find_first_image_in_inlines(inlines: &[Inline]) -> Option<&Inline> {
for inline in inlines {
match inline {
Inline::Image { .. } => return Some(inline),
Inline::Link { children, .. }
| Inline::Emphasis(children)
| Inline::Strong(children) => {
if let Some(img) = find_first_image_in_inlines(children) {
return Some(img);
}
}
Inline::Text(_) | Inline::Code(_) | Inline::LineBreak | Inline::Other(_) => {}
}
}
None
}
fn find_first_image_in_shortcode(sc: &Shortcode) -> Option<&Inline> {
match sc {
Shortcode::Subscribe(_) | Shortcode::Buttons(_) | Shortcode::Recent(_) => None,
Shortcode::Gallery(args) => {
let _ = args;
None
}
Shortcode::Hero(args) => {
find_first_image_in_blocks(&args.overlay)
}
Shortcode::Grid(args) => {
for cell in &args.cells {
if let Some(img) = find_first_image_in_blocks(cell) {
return Some(img);
}
}
None
}
}
}
#[cfg(test)]
mod tests {
use super::super::node::Inline;
use super::super::shortcode::{GridShortcode, HeroShortcode};
use super::super::url::{Url, UrlKind};
use super::*;
fn img(src: &str) -> Inline {
Inline::Image {
src: Url::resolved(src, UrlKind::Asset),
alt: String::new(),
title: None,
is_wikilink: false,
wikilink_pothole: None,
}
}
fn img_block(src: &str) -> Block {
Block::Figure {
image: img(src),
caption: None,
width: None,
align: None,
class_names: Vec::new(),
img_style: None,
}
}
fn p_with_text(text: &str) -> Block {
Block::Paragraph(vec![Inline::Text(text.into())])
}
#[test]
fn returns_none_on_empty_doc() {
let doc = Document::new();
assert!(find_first_block_image(&doc).is_none());
}
#[test]
fn returns_none_when_no_image_anywhere() {
let doc = Document::from_blocks(vec![
p_with_text("plain prose"),
Block::Heading {
level: 2,
children: vec![Inline::Text("Title".into())],
id: None,
},
]);
assert!(find_first_block_image(&doc).is_none());
}
#[test]
fn finds_image_inside_figure() {
let doc = Document::from_blocks(vec![img_block("photo.jpg")]);
match find_first_block_image(&doc) {
Some(Inline::Image { src, .. }) => {
let Url::Resolved(r) = src else {
panic!("expected Resolved, got {src:?}")
};
assert_eq!(r.href, "photo.jpg");
}
other => panic!("expected Image, got {other:?}"),
}
}
#[test]
fn finds_image_inside_paragraph() {
let doc = Document::from_blocks(vec![Block::Paragraph(vec![
Inline::Text("see ".into()),
img("inline.png"),
])]);
match find_first_block_image(&doc) {
Some(Inline::Image { src, .. }) => {
let Url::Resolved(r) = src else {
panic!("expected Resolved, got {src:?}")
};
assert_eq!(r.href, "inline.png");
}
_ => panic!("expected Image"),
}
}
#[test]
fn finds_image_inside_emphasis_or_link() {
let doc = Document::from_blocks(vec![Block::Paragraph(vec![Inline::Emphasis(vec![img(
"nested.png",
)])])]);
match find_first_block_image(&doc) {
Some(Inline::Image { src, .. }) => {
let Url::Resolved(r) = src else {
panic!("expected Resolved, got {src:?}")
};
assert_eq!(r.href, "nested.png");
}
_ => panic!("expected Image"),
}
}
#[test]
fn picks_first_image_in_document_order() {
let doc = Document::from_blocks(vec![
p_with_text("intro"),
img_block("first.jpg"),
img_block("second.jpg"),
]);
match find_first_block_image(&doc) {
Some(Inline::Image { src, .. }) => {
let Url::Resolved(r) = src else {
panic!("expected Resolved, got {src:?}")
};
assert_eq!(r.href, "first.jpg");
}
_ => panic!("expected first.jpg"),
}
}
#[test]
fn finds_image_inside_grid_cell() {
let cell = vec![img_block("grid.png")];
let doc = Document::from_blocks(vec![Block::Shortcode(Shortcode::Grid(GridShortcode {
columns: 1,
ratio: None,
classes: String::new(),
cells: vec![cell],
width: None,
}))]);
match find_first_block_image(&doc) {
Some(Inline::Image { src, .. }) => {
let Url::Resolved(r) = src else {
panic!("expected Resolved, got {src:?}")
};
assert_eq!(r.href, "grid.png");
}
_ => panic!("expected grid.png"),
}
}
#[test]
fn finds_image_inside_blockquote() {
let doc = Document::from_blocks(vec![Block::BlockQuote(vec![img_block("quoted.jpg")])]);
assert!(find_first_block_image(&doc).is_some());
}
#[test]
fn finds_image_inside_list_item() {
let doc = Document::from_blocks(vec![Block::List {
ordered: false,
start: None,
items: vec![vec![img_block("list.png")]],
item_source_lines: vec![],
}]);
assert!(find_first_block_image(&doc).is_some());
}
#[test]
fn hero_overlay_image_is_findable() {
let overlay = vec![img_block("overlay.jpg")];
let doc = Document::from_blocks(vec![Block::Shortcode(Shortcode::Hero(HeroShortcode {
image: Some(Url::resolved("hero.jpg", UrlKind::Asset)),
attrs: String::new(),
classes: String::new(),
overlay,
overlay_text: String::new(),
width: None,
}))]);
match find_first_block_image(&doc) {
Some(Inline::Image { src, .. }) => {
let Url::Resolved(r) = src else {
panic!("expected Resolved, got {src:?}")
};
assert_eq!(r.href, "overlay.jpg");
}
_ => panic!("expected overlay image"),
}
}
}