use serde::{
Serialize,
ser::{SerializeMap, Serializer},
};
use super::{
caption::Caption,
inlines::InlineNode,
location::Location,
section::{SectionKind, SectionNumber},
title::Title,
};
pub const UNNUMBERED_SECTION_STYLES: &[&str] = &[
"preface",
"abstract",
"dedication",
"colophon",
"bibliography",
"glossary",
"index",
"appendix",
];
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
#[non_exhaustive]
pub struct Anchor<'a> {
pub id: &'a str,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub xreflabel: Option<&'a str>,
pub location: Location,
#[serde(skip)]
pub(crate) bibliography: bool,
#[serde(skip)]
pub(crate) bibliography_label: Option<Box<BibliographyLabel<'a>>>,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct BibliographyLabel<'a> {
pub(crate) source: Option<&'a str>,
pub(crate) content: Vec<InlineNode<'a>>,
}
impl<'a> Anchor<'a> {
#[must_use]
pub fn new(id: &'a str, location: Location) -> Self {
Self {
id,
xreflabel: None,
location,
bibliography: false,
bibliography_label: None,
}
}
#[must_use]
pub fn with_xreflabel(mut self, xreflabel: Option<&'a str>) -> Self {
self.xreflabel = xreflabel;
self
}
#[must_use]
pub fn is_bibliography(&self) -> bool {
self.bibliography
}
#[must_use]
pub fn bibliography_label(&self) -> Option<&[InlineNode<'a>]> {
self.bibliography
.then_some(self.bibliography_label.as_deref())
.flatten()
.map(|label| label.content.as_slice())
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct TocEntry<'a> {
pub id: &'a str,
pub title: Title<'a>,
pub level: u8,
pub xreflabel: Option<&'a str>,
pub kind: SectionKind,
number: Option<SectionNumber>,
pub location: Location,
}
impl<'a> TocEntry<'a> {
pub(crate) fn for_section(
id: &'a str,
title: Title<'a>,
level: u8,
xreflabel: Option<&'a str>,
kind: SectionKind,
location: Location,
) -> Self {
Self {
id,
title,
level,
xreflabel,
kind,
number: None,
location,
}
}
pub(super) fn set_number(&mut self, number: Option<SectionNumber>) {
self.number = number;
}
#[must_use]
pub fn number(&self) -> Option<&str> {
self.number.as_ref().map(SectionNumber::as_str)
}
}
impl Serialize for TocEntry<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_map(None)?;
state.serialize_entry("id", &self.id)?;
state.serialize_entry("title", &self.title)?;
state.serialize_entry("level", &self.level)?;
if self.xreflabel.is_some() {
state.serialize_entry("xreflabel", &self.xreflabel)?;
}
if let Some(style) = self.kind.as_style() {
state.serialize_entry("style", style)?;
}
state.end()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct SectionReference {
pub(crate) name: &'static str,
pub(super) number: Option<SectionNumber>,
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct Reference<'a> {
pub xreflabel: Option<Vec<InlineNode<'a>>>,
pub title: Option<Title<'a>>,
pub location: Location,
pub caption: Option<Caption<'a>>,
pub(crate) section: Option<SectionReference>,
pub(crate) bibliography: bool,
pub(crate) automatic_citation: bool,
}
impl Reference<'_> {
#[must_use]
pub fn section_name(&self) -> Option<&str> {
self.section.as_ref().map(|section| section.name)
}
#[must_use]
pub fn section_number(&self) -> Option<&str> {
self.section
.as_ref()?
.number
.as_ref()
.map(SectionNumber::as_str)
}
#[must_use]
pub fn is_bibliography(&self) -> bool {
self.bibliography
}
#[must_use]
pub fn has_automatic_citation(&self) -> bool {
self.automatic_citation
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn section_reference_queries_borrow_shared_numbers() -> Result<(), Box<dyn std::error::Error>> {
let parsed = crate::parse(
include_str!("../../fixtures/tests/section_reference_metadata.adoc"),
&crate::Options::default(),
)?;
let document = parsed.document();
for id in ["part", "chapter", "section", "appendix"] {
let reference = document.references.get(id).ok_or("missing reference")?;
let cloned = reference.clone();
let number = reference.section_number().ok_or("missing number")?;
let cloned_number = cloned.section_number().ok_or("missing cloned number")?;
let entry = document
.toc_entries
.iter()
.find(|entry| entry.id == id)
.ok_or("missing TOC entry")?;
assert!(std::ptr::eq(number, cloned_number), "{id}");
if let Some(heading_number) = entry.number() {
assert!(std::ptr::eq(number, heading_number), "{id}");
}
}
let plain = document.references.get("plain").ok_or("missing section")?;
assert_eq!(plain.section_name(), Some("section"));
assert_eq!(plain.section_number(), None);
let block = document.references.get("block").ok_or("missing block")?;
assert_eq!(block.section_name(), None);
assert_eq!(block.section_number(), None);
Ok(())
}
fn toc_entry(kind: SectionKind) -> TocEntry<'static> {
TocEntry::for_section(
"_intro",
Title::default(),
1,
None,
kind,
Location::default(),
)
}
#[test]
fn toc_entry_serializes_special_style() -> Result<(), serde_json::Error> {
let json = serde_json::to_value(toc_entry(SectionKind::Preface))?;
assert_eq!(json.get("style").and_then(|v| v.as_str()), Some("preface"));
Ok(())
}
#[test]
fn toc_entry_omits_style_for_normal_section() -> Result<(), serde_json::Error> {
let json = serde_json::to_value(toc_entry(SectionKind::Normal))?;
assert!(json.get("style").is_none());
Ok(())
}
}