use crate::{
Parser, attributes::Attrlist, blocks::is_built_in_context, document::InterpretedValue,
};
pub(crate) fn caption_attribute_name(context: &str) -> Option<&'static str> {
match context {
"example" => Some("example-caption"),
"figure" => Some("figure-caption"),
"listing" => Some("listing-caption"),
"table" => Some("table-caption"),
_ => None,
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub(crate) struct Caption {
pub(crate) prefix: String,
pub(crate) number: Option<usize>,
}
pub(crate) fn assign_block_caption(
parser: &mut Parser,
raw_context: &str,
attrlist: Option<&Attrlist<'_>>,
has_title: bool,
) -> Option<Caption> {
let resolved_context = attrlist
.and_then(|attrlist| attrlist.block_style())
.filter(|style| is_built_in_context(style))
.unwrap_or(raw_context);
let explicit_caption =
if let Some(caption) = attrlist.and_then(|attrlist| attrlist.named_attribute("caption")) {
Some(caption.value().to_string())
} else if resolved_context == "example"
&& attrlist.is_some_and(|attrlist| attrlist.has_option("collapsible"))
{
Some(String::new())
} else {
None
};
assign_caption(
parser,
resolved_context,
has_title,
explicit_caption.as_deref(),
)
}
pub(crate) fn assign_caption(
parser: &mut Parser,
context: &str,
has_title: bool,
explicit_caption: Option<&str>,
) -> Option<Caption> {
if !has_title {
return None;
}
let attr_name = caption_attribute_name(context)?;
if let Some(value) = explicit_caption {
return if value.is_empty() {
None
} else {
Some(Caption {
prefix: value.to_string(),
number: None,
})
};
}
if let InterpretedValue::Value(value) = parser.attribute_value("caption")
&& !value.is_empty()
{
return Some(Caption {
prefix: value,
number: None,
});
}
match parser.attribute_value(attr_name) {
InterpretedValue::Value(label) if !label.is_empty() => {
let number = parser.increment_counter(&format!("{context}-number"));
Some(Caption {
prefix: format!("{label} {number}. "),
number: Some(number),
})
}
_ => None,
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use crate::{blocks::IsBlock, tests::prelude::*};
fn first_block_caption(input: &str) -> (Option<String>, Option<usize>) {
let doc = Parser::default().parse(input);
let block = doc.nested_blocks().next().expect("expected a block");
(block.caption().map(str::to_string), block.number())
}
#[test]
fn caption_attribute_name_lookup() {
assert_eq!(
super::caption_attribute_name("example"),
Some("example-caption")
);
assert_eq!(
super::caption_attribute_name("figure"),
Some("figure-caption")
);
assert_eq!(
super::caption_attribute_name("listing"),
Some("listing-caption")
);
assert_eq!(
super::caption_attribute_name("table"),
Some("table-caption")
);
assert_eq!(super::caption_attribute_name("sidebar"), None);
assert_eq!(super::caption_attribute_name("paragraph"), None);
}
#[test]
fn untitled_block_is_not_captioned() {
assert_eq!(first_block_caption("====\nbody.\n===="), (None, None));
}
#[test]
fn titled_example_is_numbered_by_default() {
assert_eq!(
first_block_caption(".Title\n====\nbody.\n===="),
(Some("Example 1. ".to_string()), Some(1))
);
}
#[test]
fn counter_is_per_context_and_document_wide() {
let doc =
Parser::default().parse(".One\n====\na\n====\n\n====\nb\n====\n\n.Two\n====\nc\n====");
let numbers: Vec<_> = doc.nested_blocks().map(|b| b.number()).collect();
assert_eq!(numbers, vec![Some(1), None, Some(2)]);
}
#[test]
fn custom_label_from_caption_attribute() {
assert_eq!(
first_block_caption(":example-caption: Exhibit\n\n.Title\n====\nbody.\n===="),
(Some("Exhibit 1. ".to_string()), Some(1))
);
}
#[test]
fn unset_caption_attribute_disables_numbering() {
assert_eq!(
first_block_caption(":!example-caption:\n\n.Title\n====\nbody.\n===="),
(None, None)
);
}
#[test]
fn explicit_caption_attribute_is_verbatim_and_unnumbered() {
assert_eq!(
first_block_caption("[caption=\"Sample: \"]\n.Title\n====\nbody.\n===="),
(Some("Sample: ".to_string()), None)
);
}
#[test]
fn empty_explicit_caption_suppresses_caption() {
assert_eq!(
first_block_caption("[caption=]\n.Title\n====\nbody.\n===="),
(None, None)
);
}
#[test]
fn document_caption_attribute_overrides_label() {
assert_eq!(
first_block_caption(":caption: Sample\n\n.Title\n====\nbody.\n===="),
(Some("Sample".to_string()), None)
);
assert_eq!(
first_block_caption(":caption: Sample\n\n.Title\nplain paragraph."),
(None, None)
);
}
#[test]
fn collapsible_example_is_unnumbered() {
let doc = Parser::default()
.parse("[%collapsible]\n====\nhidden\n====\n\n.Title\n====\nbody.\n====");
let numbers: Vec<_> = doc.nested_blocks().map(|b| b.number()).collect();
assert_eq!(numbers, vec![None, Some(1)]);
}
}