use super::document::Document;
use super::node::{Block, Inline};
use super::shortcode::Shortcode;
use std::collections::HashMap;
pub fn find_first_block_image(doc: &Document) -> Option<&Inline> {
let hoisted = super::footnotes::hoisted_definition_bodies(&doc.blocks);
find_first_image_in_blocks(&doc.blocks, Notes::Hoisted, &hoisted).or_else(|| {
let index = super::footnotes::FootnoteIndex::build(&doc.blocks);
index.entries().iter().find_map(|(_, label)| {
super::footnotes::FootnoteIndex::definition(&doc.blocks, label).and_then(
|children| find_first_image_in_blocks(children, Notes::Hoisted, &hoisted),
)
})
})
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Notes {
Hoisted,
InPlace,
}
fn find_first_image_in_blocks<'a>(
blocks: &'a [Block],
notes: Notes,
hoisted: &HashMap<String, usize>,
) -> Option<&'a Inline> {
for block in blocks {
if let Some(img) = find_first_image_in_block(block, notes, hoisted) {
return Some(img);
}
}
None
}
fn find_first_image_in_block<'a>(
block: &'a Block,
notes: Notes,
hoisted: &HashMap<String, usize>,
) -> Option<&'a 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, notes, hoisted) {
return Some(img);
}
}
None
}
Block::BlockQuote(children) => find_first_image_in_blocks(children, notes, hoisted),
Block::Callout { children, .. } => find_first_image_in_blocks(children, notes, hoisted),
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, Notes::InPlace, hoisted),
Block::LinkCard { children, .. } => find_first_image_in_blocks(children, notes, hoisted),
Block::Heading { children, .. } => find_first_image_in_inlines(children),
Block::FootnoteDefinition { label, children } => match notes {
Notes::Hoisted
if hoisted.get(label.as_str()) == Some(&(children.as_ptr() as usize)) =>
{
None
}
_ => find_first_image_in_blocks(children, notes, hoisted),
},
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)
| Inline::Strikethrough(children) => {
if let Some(img) = find_first_image_in_inlines(children) {
return Some(img);
}
}
Inline::Text(_)
| Inline::Code(_)
| Inline::LineBreak
| Inline::FootnoteRef(_)
| Inline::TaskMarker(_)
| Inline::Other(_) => {}
}
}
None
}
fn find_first_image_in_shortcode<'a>(
sc: &'a Shortcode,
notes: Notes,
hoisted: &HashMap<String, usize>,
) -> Option<&'a Inline> {
match sc {
Shortcode::Subscribe(_) | Shortcode::Buttons(_) | Shortcode::Recent(_) | Shortcode::Apply(_) => None,
Shortcode::Gallery(args) => {
let _ = args;
None
}
Shortcode::Hero(args) => {
find_first_image_in_blocks(&args.overlay, notes, hoisted)
}
Shortcode::Grid(args) => {
for cell in &args.cells {
if let Some(img) = find_first_image_in_blocks(cell, notes, hoisted) {
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 a_body_image_outranks_one_hoisted_into_the_endnotes() {
let doc = super::super::parse(
"Intro paragraph[^a].\n\n[^a]: \n\nLater section.\n\n\n",
);
match find_first_block_image(&doc) {
Some(Inline::Image { src, .. }) => assert_eq!(
src,
&Url::unresolved("p.png"),
"the endnote's image won the cover over the body's"
),
other => panic!("expected an image, got {other:?}"),
}
}
#[test]
fn a_repeated_labels_second_definition_is_body_matter_not_endnote_matter() {
let doc = super::super::parse(
"Para one[^1].\n\n[^1]: see \n\nPara two[^1].\n\n[^1]: see \n",
);
match find_first_block_image(&doc) {
Some(Inline::Image { src, .. }) => assert_eq!(
src,
&Url::unresolved("c2.png"),
"picked the hoisted first definition's image over the one that \
actually renders in the body"
),
other => panic!("expected an image, got {other:?}"),
}
}
#[test]
fn a_repeat_whose_first_definition_is_nested_is_still_body_matter() {
let doc = super::super::parse(
"Body[^a][^b].\n\n[^a]: OUT\n\n > [^b]: IN \n\n[^b]: REPEAT \n",
);
match find_first_block_image(&doc) {
Some(Inline::Image { src, .. }) => assert_eq!(
src,
&Url::unresolved("r.png"),
"picked an endnote-only image over the repeat's body image"
),
other => panic!("expected the repeat's body image, got {other:?}"),
}
}
#[test]
fn with_no_body_image_the_first_endnote_image_wins_not_the_first_written() {
let doc =
super::super::parse("Body[^b][^a].\n\n[^a]: \n\n[^b]: \n");
match find_first_block_image(&doc) {
Some(Inline::Image { src, .. }) => assert_eq!(
src,
&Url::unresolved("b.png"),
"picked the first-written definition's image over the first-rendered note's"
),
other => panic!("expected an image, got {other:?}"),
}
}
#[test]
fn an_image_that_lives_only_in_a_note_is_still_found() {
let doc = super::super::parse("Intro[^a].\n\n[^a]: \n");
match find_first_block_image(&doc) {
Some(Inline::Image { src, .. }) => assert_eq!(src, &Url::unresolved("d.png")),
other => panic!("expected the note's image, got {other:?}"),
}
}
#[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)),
extra_images: Vec::new(),
attrs: String::new(),
classes: String::new(),
overlay,
overlay_text: String::new(),
width: None,
mobile: None,
caption: String::new(),
}))]);
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"),
}
}
}