use std::{borrow::Cow, num::NonZeroU32};
use super::{AttributeValue, Block, BlockMetadata, DelimitedBlockType, DocumentAttributes, Table};
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum Caption<'a> {
Numbered {
kind: CaptionKind,
label: Cow<'a, str>,
number: Option<NonZeroU32>,
},
Custom(Cow<'a, str>),
Unnumbered,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum CaptionKind {
Example,
Figure,
Listing,
Table,
}
impl CaptionKind {
const fn attribute_name(self) -> &'static str {
match self {
Self::Example => "example-caption",
Self::Figure => "figure-caption",
Self::Listing => "listing-caption",
Self::Table => "table-caption",
}
}
pub(crate) fn for_block(block: &Block<'_>) -> Option<Self> {
match block {
Block::DelimitedBlock(delimited) => {
Self::for_delimited(&delimited.inner, delimited.metadata.style)
}
Block::Paragraph(paragraph) => Self::for_style(paragraph.metadata.style),
Block::Image(_) => Some(Self::Figure),
Block::Section(_)
| Block::Admonition(_)
| Block::DiscreteHeader(_)
| Block::PageBreak(_)
| Block::ThematicBreak(_)
| Block::TableOfContents(_)
| Block::UnorderedList(_)
| Block::OrderedList(_)
| Block::CalloutList(_)
| Block::DescriptionList(_)
| Block::Audio(_)
| Block::Video(_)
| Block::DocumentAttribute(_)
| Block::Comment(_) => None,
}
}
#[must_use]
pub fn for_style(style: Option<&str>) -> Option<Self> {
match style {
Some("example") => Some(Self::Example),
Some("listing" | "source") => Some(Self::Listing),
_ => None,
}
}
#[must_use]
pub fn for_delimited(inner: &DelimitedBlockType<'_>, style: Option<&str>) -> Option<Self> {
match inner {
DelimitedBlockType::DelimitedExample(_) => Some(Self::Example),
DelimitedBlockType::DelimitedTable(_) => Some(Self::Table),
DelimitedBlockType::DelimitedListing(_) => {
Self::for_verbatim(style, Some(Self::Listing))
}
DelimitedBlockType::DelimitedLiteral(_) => Self::for_verbatim(style, None),
DelimitedBlockType::DelimitedOpen(_) => Self::for_style(style),
DelimitedBlockType::DelimitedSidebar(_)
| DelimitedBlockType::DelimitedQuote(_)
| DelimitedBlockType::DelimitedVerse(_)
| DelimitedBlockType::DelimitedPass(_)
| DelimitedBlockType::DelimitedComment(_)
| DelimitedBlockType::DelimitedStem(_) => None,
}
}
fn for_verbatim(style: Option<&str>, default: Option<Self>) -> Option<Self> {
match style {
Some("listing" | "source") => Some(Self::Listing),
Some("literal") => None,
_ => default,
}
}
}
enum CaptionSource<'value, 'a> {
Custom(Option<&'value Cow<'a, str>>),
Numbered(Option<&'value Cow<'a, str>>),
None,
}
fn caption_source<'value, 'a>(
metadata: &'value BlockMetadata<'a>,
attributes: &'value DocumentAttributes<'a>,
kind: CaptionKind,
) -> CaptionSource<'value, 'a> {
if kind == CaptionKind::Example && metadata.options.contains(&"collapsible") {
return CaptionSource::Custom(None);
}
match metadata.attributes.get("caption") {
Some(AttributeValue::String(value)) => return CaptionSource::Custom(Some(value)),
Some(AttributeValue::Bool(_) | AttributeValue::None) | None => {}
}
match attributes.stored_text("caption") {
Some(value) => return CaptionSource::Custom(Some(value)),
None if attributes.contains_key("caption") => return CaptionSource::Custom(None),
None => {}
}
match attributes.stored_text(kind.attribute_name()) {
Some(value) => CaptionSource::Numbered(Some(value)),
None if attributes.contains_key(kind.attribute_name()) => CaptionSource::Numbered(None),
None => CaptionSource::None,
}
}
fn borrowed_label<'a>(value: Option<&Cow<'a, str>>) -> Cow<'a, str> {
value.cloned().unwrap_or(Cow::Borrowed(""))
}
fn owned_label(value: Option<&Cow<'_, str>>) -> Cow<'static, str> {
value.map_or(Cow::Borrowed(""), |value| Cow::Owned(value.to_string()))
}
impl<'a> Caption<'a> {
#[must_use]
pub fn resolve(
metadata: &BlockMetadata<'a>,
attributes: &DocumentAttributes<'a>,
kind: CaptionKind,
) -> Self {
match caption_source(metadata, attributes, kind) {
CaptionSource::Custom(value) => Self::Custom(borrowed_label(value)),
CaptionSource::Numbered(value) => Self::Numbered {
kind,
label: borrowed_label(value),
number: None,
},
CaptionSource::None => Self::Unnumbered,
}
}
}
impl Caption<'static> {
#[must_use]
pub fn resolve_owned(
metadata: &BlockMetadata<'_>,
attributes: &DocumentAttributes<'_>,
kind: CaptionKind,
) -> Self {
match caption_source(metadata, attributes, kind) {
CaptionSource::Custom(value) => Self::Custom(owned_label(value)),
CaptionSource::Numbered(value) => Self::Numbered {
kind,
label: owned_label(value),
number: None,
},
CaptionSource::None => Self::Unnumbered,
}
}
}
#[derive(Debug, Default)]
struct CaptionCounters {
example: u32,
figure: u32,
listing: u32,
table: u32,
}
impl CaptionCounters {
fn next(&mut self, kind: CaptionKind) -> Option<NonZeroU32> {
let counter = match kind {
CaptionKind::Example => &mut self.example,
CaptionKind::Figure => &mut self.figure,
CaptionKind::Listing => &mut self.listing,
CaptionKind::Table => &mut self.table,
};
*counter = counter.saturating_add(1);
NonZeroU32::new(*counter)
}
}
pub(crate) fn renumber_captions(blocks: &mut [Block<'_>]) {
let mut counters = CaptionCounters::default();
renumber_blocks(blocks, &mut counters);
}
pub(crate) fn highest_caption_number(blocks: &[Block<'_>], kind: CaptionKind) -> u32 {
let mut highest = 0;
visit_captions(blocks, &mut |caption| {
if let Caption::Numbered {
kind: caption_kind,
number: Some(number),
..
} = caption
&& *caption_kind == kind
{
highest = highest.max(number.get());
}
});
highest
}
fn visit_captions(blocks: &[Block<'_>], visit: &mut impl FnMut(&Caption<'_>)) {
for block in blocks {
if let Some(caption) = block
.metadata()
.and_then(|metadata| metadata.caption.as_ref())
{
visit(caption);
}
match block {
Block::Section(section) => visit_captions(§ion.content, visit),
Block::Admonition(admonition) => visit_captions(&admonition.blocks, visit),
Block::UnorderedList(list) => {
for item in &list.items {
visit_captions(&item.blocks, visit);
}
}
Block::OrderedList(list) => {
for item in &list.items {
visit_captions(&item.blocks, visit);
}
}
Block::CalloutList(list) => {
for item in &list.items {
visit_captions(&item.blocks, visit);
}
}
Block::DescriptionList(list) => {
for item in &list.items {
visit_captions(&item.description, visit);
}
}
Block::DelimitedBlock(delimited) => match &delimited.inner {
DelimitedBlockType::DelimitedExample(blocks)
| DelimitedBlockType::DelimitedOpen(blocks)
| DelimitedBlockType::DelimitedSidebar(blocks)
| DelimitedBlockType::DelimitedQuote(blocks) => visit_captions(blocks, visit),
DelimitedBlockType::DelimitedTable(table) => {
for row in table
.header
.iter()
.chain(table.rows.iter())
.chain(table.footer.iter())
{
for column in &row.columns {
visit_captions(&column.content, visit);
}
}
}
DelimitedBlockType::DelimitedListing(_)
| DelimitedBlockType::DelimitedLiteral(_)
| DelimitedBlockType::DelimitedVerse(_)
| DelimitedBlockType::DelimitedPass(_)
| DelimitedBlockType::DelimitedComment(_)
| DelimitedBlockType::DelimitedStem(_) => {}
},
Block::Paragraph(_)
| Block::DiscreteHeader(_)
| Block::PageBreak(_)
| Block::ThematicBreak(_)
| Block::TableOfContents(_)
| Block::Image(_)
| Block::Audio(_)
| Block::Video(_)
| Block::DocumentAttribute(_)
| Block::Comment(_) => {}
}
}
}
fn renumber_blocks(blocks: &mut [Block<'_>], counters: &mut CaptionCounters) {
for block in blocks {
renumber_block(block, counters);
}
}
fn renumber_block(block: &mut Block<'_>, counters: &mut CaptionCounters) {
renumber_children(block, counters);
let titled = block.title().is_some();
if let Some(metadata) = block.metadata_mut()
&& let Some(Caption::Numbered { kind, number, .. }) = &mut metadata.caption
{
*number = titled.then(|| counters.next(*kind)).flatten();
}
}
fn renumber_children(block: &mut Block<'_>, counters: &mut CaptionCounters) {
match block {
Block::Section(section) => renumber_blocks(&mut section.content, counters),
Block::Admonition(admonition) => renumber_blocks(&mut admonition.blocks, counters),
Block::UnorderedList(list) => {
for item in &mut list.items {
renumber_blocks(&mut item.blocks, counters);
}
}
Block::OrderedList(list) => {
for item in &mut list.items {
renumber_blocks(&mut item.blocks, counters);
}
}
Block::CalloutList(list) => {
for item in &mut list.items {
renumber_blocks(&mut item.blocks, counters);
}
}
Block::DescriptionList(list) => {
for item in &mut list.items {
renumber_blocks(&mut item.description, counters);
}
}
Block::DelimitedBlock(delimited) => match &mut delimited.inner {
DelimitedBlockType::DelimitedExample(blocks)
| DelimitedBlockType::DelimitedOpen(blocks)
| DelimitedBlockType::DelimitedSidebar(blocks)
| DelimitedBlockType::DelimitedQuote(blocks) => renumber_blocks(blocks, counters),
DelimitedBlockType::DelimitedTable(table) => renumber_table(table, counters),
DelimitedBlockType::DelimitedListing(_)
| DelimitedBlockType::DelimitedLiteral(_)
| DelimitedBlockType::DelimitedVerse(_)
| DelimitedBlockType::DelimitedPass(_)
| DelimitedBlockType::DelimitedComment(_)
| DelimitedBlockType::DelimitedStem(_) => {}
},
Block::Paragraph(_)
| Block::DiscreteHeader(_)
| Block::PageBreak(_)
| Block::ThematicBreak(_)
| Block::TableOfContents(_)
| Block::Image(_)
| Block::Audio(_)
| Block::Video(_)
| Block::DocumentAttribute(_)
| Block::Comment(_) => {}
}
}
fn renumber_table(table: &mut Table<'_>, counters: &mut CaptionCounters) {
for row in table
.header
.iter_mut()
.chain(table.rows.iter_mut())
.chain(table.footer.iter_mut())
{
for column in &mut row.columns {
renumber_blocks(&mut column.content, counters);
}
}
}