use std::collections::HashMap;
use crate::{content::FootnoteDeferred, internal::debug::DebugHashMapFrom};
#[derive(Clone, Eq, PartialEq)]
pub struct Catalog {
pub(crate) refs: HashMap<String, RefEntry>,
pub(crate) reftext_to_id: HashMap<String, String>,
pub(crate) footnotes: Vec<Footnote>,
}
impl Default for Catalog {
fn default() -> Self {
Self::new()
}
}
impl Catalog {
pub(crate) fn new() -> Self {
Self {
refs: HashMap::new(),
reftext_to_id: HashMap::new(),
footnotes: Vec::new(),
}
}
pub(crate) fn register_ref(
&mut self,
id: &str,
reftext: Option<&str>,
ref_type: RefType,
) -> Result<(), DuplicateIdError> {
if self.refs.contains_key(id) {
return Err(DuplicateIdError(id.to_string()));
}
let entry = RefEntry {
id: id.to_string(),
reftext: reftext.map(|s| s.to_owned()),
ref_type,
};
self.refs.insert(id.to_string(), entry);
if let Some(reftext) = reftext {
self.reftext_to_id
.entry(reftext.to_string())
.or_insert_with(|| id.to_string());
}
Ok(())
}
pub(crate) fn generate_and_register_unique_id(
&mut self,
base_id: &str,
reftext: Option<&str>,
ref_type: RefType,
) -> String {
let unique_id = if !self.contains_id(base_id) {
base_id.to_string()
} else {
let mut counter = 2;
loop {
let candidate = format!("{}-{}", base_id, counter);
if !self.contains_id(&candidate) {
break candidate;
}
counter += 1;
}
};
let entry = RefEntry {
id: unique_id.clone(),
reftext: reftext.map(|s| s.to_owned()),
ref_type,
};
self.refs.insert(unique_id.clone(), entry);
if let Some(reftext) = reftext {
self.reftext_to_id
.entry(reftext.to_string())
.or_insert_with(|| unique_id.clone());
}
unique_id
}
pub fn get_ref(&self, id: &str) -> Option<&RefEntry> {
self.refs.get(id)
}
pub fn contains_id(&self, id: &str) -> bool {
self.refs.contains_key(id)
}
pub fn resolve_id(&self, reftext: &str) -> Option<String> {
self.reftext_to_id.get(reftext).cloned()
}
pub fn ids(&self) -> impl Iterator<Item = &str> {
self.refs.keys().map(String::as_str)
}
pub fn entries(&self) -> impl Iterator<Item = (&str, &RefEntry)> {
self.refs.iter().map(|(id, entry)| (id.as_str(), entry))
}
pub fn len(&self) -> usize {
self.refs.len()
}
pub fn is_empty(&self) -> bool {
self.refs.is_empty()
}
pub fn footnotes(&self) -> &[Footnote] {
&self.footnotes
}
pub(crate) fn register_footnote(&mut self, footnote: Footnote) {
self.footnotes.push(footnote);
}
pub(crate) fn footnote_with_id(&self, id: &str) -> Option<&Footnote> {
self.footnotes.iter().find(|f| f.id.as_deref() == Some(id))
}
pub(crate) fn take_footnotes(&mut self) -> Vec<Footnote> {
std::mem::take(&mut self.footnotes)
}
pub(crate) fn restore_footnotes(&mut self, footnotes: Vec<Footnote>) {
self.footnotes = footnotes;
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct Footnote {
pub index: String,
pub id: Option<String>,
pub text: String,
pub(crate) deferred: Option<Box<FootnoteDeferred>>,
}
impl Footnote {
pub(crate) fn resolve_references(
&mut self,
resolver: &dyn crate::parser::ReferenceResolver,
renderer: &dyn crate::parser::InlineSubstitutionRenderer,
warnings: &mut Vec<crate::parser::ReferenceWarning>,
) {
if let Some(deferred) = self.deferred.as_mut() {
deferred.resolve(resolver, warnings);
self.text = deferred.render(renderer);
}
}
}
impl std::fmt::Debug for Footnote {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = f.debug_struct("Footnote");
s.field("index", &self.index);
s.field("id", &self.id);
s.field("text", &self.text);
if let Some(deferred) = self.deferred.as_ref() {
s.field("deferred", deferred);
}
s.finish()
}
}
impl std::fmt::Debug for Catalog {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("Catalog")
.field("refs", &DebugHashMapFrom(&self.refs))
.field("reftext_to_id", &DebugHashMapFrom(&self.reftext_to_id))
.field("footnotes", &self.footnotes)
.finish()
}
}
#[derive(Clone, PartialEq, Eq)]
pub enum RefType {
Anchor,
Section,
Bibliography,
}
impl std::fmt::Debug for RefType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Anchor => f.write_str("RefType::Anchor"),
Self::Section => f.write_str("RefType::Section"),
Self::Bibliography => f.write_str("RefType::Bibliography"),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RefEntry {
pub id: String,
pub reftext: Option<String>,
pub ref_type: RefType,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct DuplicateIdError(pub(crate) String);
impl std::fmt::Display for DuplicateIdError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ID '{}' already registered", self.0)
}
}
impl std::error::Error for DuplicateIdError {}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn new_catalog_is_empty() {
let catalog = Catalog::new();
assert!(catalog.is_empty());
assert_eq!(catalog.len(), 0);
}
#[test]
fn register_ref_success() {
let mut catalog = Catalog::new();
let result = catalog.register_ref("test-id", Some("Test Reference"), RefType::Anchor);
assert!(result.is_ok());
assert_eq!(catalog.len(), 1);
assert!(catalog.contains_id("test-id"));
}
#[test]
fn register_duplicate_id_fails() {
let mut catalog = Catalog::new();
catalog
.register_ref("test-id", Some("First"), RefType::Anchor)
.unwrap();
let result = catalog.register_ref("test-id", Some("Second"), RefType::Section);
let error = result.unwrap_err();
assert_eq!(error.0, "test-id");
}
#[test]
fn generate_and_register_unique_id() {
let mut catalog = Catalog::new();
let id1 = catalog.generate_and_register_unique_id(
"available",
Some("Available Ref"),
RefType::Anchor,
);
assert_eq!(id1, "available");
assert!(catalog.contains_id("available"));
assert_eq!(
catalog.resolve_id("Available Ref"),
Some("available".to_string())
);
catalog
.register_ref("taken", None, RefType::Anchor)
.unwrap();
catalog
.register_ref("taken-2", None, RefType::Anchor)
.unwrap();
let id2 = catalog.generate_and_register_unique_id("taken", None, RefType::Section);
assert_eq!(id2, "taken-3");
assert!(catalog.contains_id("taken-3"));
}
#[test]
fn get_ref() {
let mut catalog = Catalog::new();
catalog
.register_ref("test-id", Some("Test Reference"), RefType::Bibliography)
.unwrap();
let entry = catalog.get_ref("test-id").unwrap();
assert_eq!(entry.id, "test-id");
assert_eq!(entry.reftext, Some("Test Reference".to_string()));
assert_eq!(entry.ref_type, RefType::Bibliography);
assert!(catalog.get_ref("nonexistent").is_none());
}
#[test]
fn enumerate_ids_and_entries() {
let mut catalog = Catalog::new();
catalog
.register_ref("intro", Some("Introduction"), RefType::Section)
.unwrap();
catalog
.register_ref("fig-1", None, RefType::Anchor)
.unwrap();
let mut ids: Vec<&str> = catalog.ids().collect();
ids.sort_unstable();
assert_eq!(ids, vec!["fig-1", "intro"]);
let entries: Vec<(&str, &RefEntry)> = catalog.entries().collect();
assert_eq!(entries.len(), 2);
let (fig_id, fig_entry) = entries.iter().find(|(id, _)| *id == "fig-1").unwrap();
assert_eq!(*fig_id, "fig-1");
assert_eq!(fig_entry.id, "fig-1");
assert_eq!(fig_entry.reftext, None);
assert_eq!(fig_entry.ref_type, RefType::Anchor);
let (_, intro_entry) = entries.iter().find(|(id, _)| *id == "intro").unwrap();
assert_eq!(intro_entry.reftext, Some("Introduction".to_string()));
assert_eq!(intro_entry.ref_type, RefType::Section);
}
#[test]
fn resolve_id() {
let mut catalog = Catalog::new();
catalog
.register_ref("anchor1", Some("Reference Text"), RefType::Anchor)
.unwrap();
catalog
.register_ref("anchor2", Some("Another Reference"), RefType::Section)
.unwrap();
assert_eq!(
catalog.resolve_id("Reference Text"),
Some("anchor1".to_string())
);
assert_eq!(
catalog.resolve_id("Another Reference"),
Some("anchor2".to_string())
);
assert_eq!(catalog.resolve_id("Nonexistent"), None);
}
#[test]
fn resolve_id_first_wins_on_duplicates() {
let mut catalog = Catalog::new();
catalog
.register_ref("first", Some("Same Text"), RefType::Anchor)
.unwrap();
catalog
.register_ref("second", Some("Same Text"), RefType::Section)
.unwrap();
assert_eq!(catalog.resolve_id("Same Text"), Some("first".to_string()));
}
#[test]
fn duplicate_id_error_impl_display() {
let did_error = DuplicateIdError("foo".to_string());
assert_eq!(did_error.to_string(), "ID 'foo' already registered");
}
#[test]
fn ref_type_impl_debug() {
assert_eq!(format!("{:#?}", RefType::Anchor), "RefType::Anchor");
assert_eq!(format!("{:#?}", RefType::Section), "RefType::Section");
assert_eq!(
format!("{:#?}", RefType::Bibliography),
"RefType::Bibliography"
);
}
}