1use std::{borrow::Cow, num::NonZeroU32};
9
10use super::{AttributeValue, Block, BlockMetadata, DelimitedBlockType, DocumentAttributes, Table};
11
12#[derive(Clone, Debug, PartialEq)]
14#[non_exhaustive]
15pub enum Caption<'a> {
16 Numbered {
24 kind: CaptionKind,
26 label: Cow<'a, str>,
28 number: Option<NonZeroU32>,
31 },
32 Custom(Cow<'a, str>),
34 Unnumbered,
36}
37
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40#[non_exhaustive]
41pub enum CaptionKind {
42 Example,
44 Figure,
46 Listing,
48 Table,
50}
51
52impl CaptionKind {
53 const fn attribute_name(self) -> &'static str {
54 match self {
55 Self::Example => "example-caption",
56 Self::Figure => "figure-caption",
57 Self::Listing => "listing-caption",
58 Self::Table => "table-caption",
59 }
60 }
61
62 pub(crate) fn for_block(block: &Block<'_>) -> Option<Self> {
69 match block {
70 Block::DelimitedBlock(delimited) => {
71 Self::for_delimited(&delimited.inner, delimited.metadata.style)
72 }
73 Block::Paragraph(paragraph) => Self::for_style(paragraph.metadata.style),
74 Block::Image(_) => Some(Self::Figure),
75 Block::Section(_)
76 | Block::Admonition(_)
77 | Block::DiscreteHeader(_)
78 | Block::PageBreak(_)
79 | Block::ThematicBreak(_)
80 | Block::TableOfContents(_)
81 | Block::UnorderedList(_)
82 | Block::OrderedList(_)
83 | Block::CalloutList(_)
84 | Block::DescriptionList(_)
85 | Block::Audio(_)
86 | Block::Video(_)
87 | Block::DocumentAttribute(_)
88 | Block::Comment(_) => None,
89 }
90 }
91
92 #[must_use]
98 pub fn for_style(style: Option<&str>) -> Option<Self> {
99 match style {
100 Some("example") => Some(Self::Example),
101 Some("listing" | "source") => Some(Self::Listing),
102 _ => None,
103 }
104 }
105
106 #[must_use]
111 pub fn for_delimited(inner: &DelimitedBlockType<'_>, style: Option<&str>) -> Option<Self> {
112 match inner {
113 DelimitedBlockType::DelimitedExample(_) => Some(Self::Example),
115 DelimitedBlockType::DelimitedTable(_) => Some(Self::Table),
116 DelimitedBlockType::DelimitedListing(_) => {
119 Self::for_verbatim(style, Some(Self::Listing))
120 }
121 DelimitedBlockType::DelimitedLiteral(_) => Self::for_verbatim(style, None),
122 DelimitedBlockType::DelimitedOpen(_) => Self::for_style(style),
124 DelimitedBlockType::DelimitedSidebar(_)
126 | DelimitedBlockType::DelimitedQuote(_)
127 | DelimitedBlockType::DelimitedVerse(_)
128 | DelimitedBlockType::DelimitedPass(_)
129 | DelimitedBlockType::DelimitedComment(_)
130 | DelimitedBlockType::DelimitedStem(_) => None,
131 }
132 }
133
134 fn for_verbatim(style: Option<&str>, default: Option<Self>) -> Option<Self> {
137 match style {
138 Some("listing" | "source") => Some(Self::Listing),
139 Some("literal") => None,
140 _ => default,
141 }
142 }
143}
144
145enum CaptionSource<'value, 'a> {
149 Custom(Option<&'value Cow<'a, str>>),
151 Numbered(Option<&'value Cow<'a, str>>),
153 None,
155}
156
157fn caption_source<'value, 'a>(
162 metadata: &'value BlockMetadata<'a>,
163 attributes: &'value DocumentAttributes<'a>,
164 kind: CaptionKind,
165) -> CaptionSource<'value, 'a> {
166 if kind == CaptionKind::Example && metadata.options.contains(&"collapsible") {
167 return CaptionSource::Custom(None);
168 }
169
170 match metadata.attributes.get("caption") {
174 Some(AttributeValue::String(value)) => return CaptionSource::Custom(Some(value)),
175 Some(AttributeValue::Bool(_) | AttributeValue::None) | None => {}
176 }
177 match attributes.stored_text("caption") {
178 Some(value) => return CaptionSource::Custom(Some(value)),
179 None if attributes.contains_key("caption") => return CaptionSource::Custom(None),
180 None => {}
181 }
182 match attributes.stored_text(kind.attribute_name()) {
183 Some(value) => CaptionSource::Numbered(Some(value)),
184 None if attributes.contains_key(kind.attribute_name()) => CaptionSource::Numbered(None),
185 None => CaptionSource::None,
186 }
187}
188
189fn borrowed_label<'a>(value: Option<&Cow<'a, str>>) -> Cow<'a, str> {
193 value.cloned().unwrap_or(Cow::Borrowed(""))
194}
195
196fn owned_label(value: Option<&Cow<'_, str>>) -> Cow<'static, str> {
197 value.map_or(Cow::Borrowed(""), |value| Cow::Owned(value.to_string()))
198}
199
200impl<'a> Caption<'a> {
201 #[must_use]
204 pub fn resolve(
205 metadata: &BlockMetadata<'a>,
206 attributes: &DocumentAttributes<'a>,
207 kind: CaptionKind,
208 ) -> Self {
209 match caption_source(metadata, attributes, kind) {
210 CaptionSource::Custom(value) => Self::Custom(borrowed_label(value)),
211 CaptionSource::Numbered(value) => Self::Numbered {
212 kind,
213 label: borrowed_label(value),
214 number: None,
215 },
216 CaptionSource::None => Self::Unnumbered,
217 }
218 }
219}
220
221impl Caption<'static> {
222 #[must_use]
226 pub fn resolve_owned(
227 metadata: &BlockMetadata<'_>,
228 attributes: &DocumentAttributes<'_>,
229 kind: CaptionKind,
230 ) -> Self {
231 match caption_source(metadata, attributes, kind) {
232 CaptionSource::Custom(value) => Self::Custom(owned_label(value)),
233 CaptionSource::Numbered(value) => Self::Numbered {
234 kind,
235 label: owned_label(value),
236 number: None,
237 },
238 CaptionSource::None => Self::Unnumbered,
239 }
240 }
241}
242
243#[derive(Debug, Default)]
245struct CaptionCounters {
246 example: u32,
247 figure: u32,
248 listing: u32,
249 table: u32,
250}
251
252impl CaptionCounters {
253 fn next(&mut self, kind: CaptionKind) -> Option<NonZeroU32> {
254 let counter = match kind {
255 CaptionKind::Example => &mut self.example,
256 CaptionKind::Figure => &mut self.figure,
257 CaptionKind::Listing => &mut self.listing,
258 CaptionKind::Table => &mut self.table,
259 };
260 *counter = counter.saturating_add(1);
261 NonZeroU32::new(*counter)
262 }
263}
264
265pub(crate) fn renumber_captions(blocks: &mut [Block<'_>]) {
272 let mut counters = CaptionCounters::default();
273 renumber_blocks(blocks, &mut counters);
274}
275
276pub(crate) fn highest_caption_number(blocks: &[Block<'_>], kind: CaptionKind) -> u32 {
280 let mut highest = 0;
281 visit_captions(blocks, &mut |caption| {
282 if let Caption::Numbered {
283 kind: caption_kind,
284 number: Some(number),
285 ..
286 } = caption
287 && *caption_kind == kind
288 {
289 highest = highest.max(number.get());
290 }
291 });
292 highest
293}
294
295fn visit_captions(blocks: &[Block<'_>], visit: &mut impl FnMut(&Caption<'_>)) {
296 for block in blocks {
297 if let Some(caption) = block
298 .metadata()
299 .and_then(|metadata| metadata.caption.as_ref())
300 {
301 visit(caption);
302 }
303 match block {
304 Block::Section(section) => visit_captions(§ion.content, visit),
305 Block::Admonition(admonition) => visit_captions(&admonition.blocks, visit),
306 Block::UnorderedList(list) => {
307 for item in &list.items {
308 visit_captions(&item.blocks, visit);
309 }
310 }
311 Block::OrderedList(list) => {
312 for item in &list.items {
313 visit_captions(&item.blocks, visit);
314 }
315 }
316 Block::CalloutList(list) => {
317 for item in &list.items {
318 visit_captions(&item.blocks, visit);
319 }
320 }
321 Block::DescriptionList(list) => {
322 for item in &list.items {
323 visit_captions(&item.description, visit);
324 }
325 }
326 Block::DelimitedBlock(delimited) => match &delimited.inner {
327 DelimitedBlockType::DelimitedExample(blocks)
328 | DelimitedBlockType::DelimitedOpen(blocks)
329 | DelimitedBlockType::DelimitedSidebar(blocks)
330 | DelimitedBlockType::DelimitedQuote(blocks) => visit_captions(blocks, visit),
331 DelimitedBlockType::DelimitedTable(table) => {
332 for row in table
333 .header
334 .iter()
335 .chain(table.rows.iter())
336 .chain(table.footer.iter())
337 {
338 for column in &row.columns {
339 visit_captions(&column.content, visit);
340 }
341 }
342 }
343 DelimitedBlockType::DelimitedListing(_)
344 | DelimitedBlockType::DelimitedLiteral(_)
345 | DelimitedBlockType::DelimitedVerse(_)
346 | DelimitedBlockType::DelimitedPass(_)
347 | DelimitedBlockType::DelimitedComment(_)
348 | DelimitedBlockType::DelimitedStem(_) => {}
349 },
350 Block::Paragraph(_)
351 | Block::DiscreteHeader(_)
352 | Block::PageBreak(_)
353 | Block::ThematicBreak(_)
354 | Block::TableOfContents(_)
355 | Block::Image(_)
356 | Block::Audio(_)
357 | Block::Video(_)
358 | Block::DocumentAttribute(_)
359 | Block::Comment(_) => {}
360 }
361 }
362}
363
364fn renumber_blocks(blocks: &mut [Block<'_>], counters: &mut CaptionCounters) {
365 for block in blocks {
366 renumber_block(block, counters);
367 }
368}
369
370fn renumber_block(block: &mut Block<'_>, counters: &mut CaptionCounters) {
371 renumber_children(block, counters);
372
373 let titled = block.title().is_some();
374 if let Some(metadata) = block.metadata_mut()
375 && let Some(Caption::Numbered { kind, number, .. }) = &mut metadata.caption
376 {
377 *number = titled.then(|| counters.next(*kind)).flatten();
378 }
379}
380
381fn renumber_children(block: &mut Block<'_>, counters: &mut CaptionCounters) {
385 match block {
386 Block::Section(section) => renumber_blocks(&mut section.content, counters),
387 Block::Admonition(admonition) => renumber_blocks(&mut admonition.blocks, counters),
388 Block::UnorderedList(list) => {
389 for item in &mut list.items {
390 renumber_blocks(&mut item.blocks, counters);
391 }
392 }
393 Block::OrderedList(list) => {
394 for item in &mut list.items {
395 renumber_blocks(&mut item.blocks, counters);
396 }
397 }
398 Block::CalloutList(list) => {
399 for item in &mut list.items {
400 renumber_blocks(&mut item.blocks, counters);
401 }
402 }
403 Block::DescriptionList(list) => {
404 for item in &mut list.items {
405 renumber_blocks(&mut item.description, counters);
406 }
407 }
408 Block::DelimitedBlock(delimited) => match &mut delimited.inner {
409 DelimitedBlockType::DelimitedExample(blocks)
410 | DelimitedBlockType::DelimitedOpen(blocks)
411 | DelimitedBlockType::DelimitedSidebar(blocks)
412 | DelimitedBlockType::DelimitedQuote(blocks) => renumber_blocks(blocks, counters),
413 DelimitedBlockType::DelimitedTable(table) => renumber_table(table, counters),
414 DelimitedBlockType::DelimitedListing(_)
415 | DelimitedBlockType::DelimitedLiteral(_)
416 | DelimitedBlockType::DelimitedVerse(_)
417 | DelimitedBlockType::DelimitedPass(_)
418 | DelimitedBlockType::DelimitedComment(_)
419 | DelimitedBlockType::DelimitedStem(_) => {}
420 },
421 Block::Paragraph(_)
422 | Block::DiscreteHeader(_)
423 | Block::PageBreak(_)
424 | Block::ThematicBreak(_)
425 | Block::TableOfContents(_)
426 | Block::Image(_)
427 | Block::Audio(_)
428 | Block::Video(_)
429 | Block::DocumentAttribute(_)
430 | Block::Comment(_) => {}
431 }
432}
433
434fn renumber_table(table: &mut Table<'_>, counters: &mut CaptionCounters) {
435 for row in table
436 .header
437 .iter_mut()
438 .chain(table.rows.iter_mut())
439 .chain(table.footer.iter_mut())
440 {
441 for column in &mut row.columns {
442 renumber_blocks(&mut column.content, counters);
443 }
444 }
445}