use std::collections::{HashMap, HashSet};
use common::database::Store;
use common::types::EntityId;
#[derive(Debug, Default, Clone)]
pub struct Footnotes {
numbers: HashMap<String, usize>,
definitions: HashMap<String, EntityId>,
definition_frames: HashSet<EntityId>,
overrides: HashMap<String, String>,
}
impl Footnotes {
pub fn build(store: &Store) -> Self {
let frames = store.frames.read();
let mut definitions: HashMap<String, EntityId> = HashMap::new();
let mut definition_frames: HashSet<EntityId> = HashSet::new();
let mut definition_blocks: HashSet<EntityId> = HashSet::new();
for frame in frames.values() {
let Some(label) = &frame.footnote_label else {
continue;
};
definition_frames.insert(frame.id);
definitions.insert(label.clone(), frame.id);
for child in &frame.child_order {
if *child > 0 {
definition_blocks.insert(*child as EntityId);
}
}
}
drop(frames);
let mut ordered: Vec<(i64, EntityId)> = store
.blocks
.read()
.values()
.filter(|b| !definition_blocks.contains(&b.id))
.map(|b| (b.document_position, b.id))
.collect();
ordered.sort_unstable();
let refs = store.block_footnote_refs.read();
let mut numbers: HashMap<String, usize> = HashMap::new();
let mut next = 1usize;
for (_, block_id) in ordered {
let Some(anchors) = refs.get(&block_id) else {
continue;
};
let mut in_block: Vec<_> = anchors.iter().collect();
in_block.sort_by_key(|a| a.byte_offset);
for anchor in in_block {
numbers.entry(anchor.label.clone()).or_insert_with(|| {
let n = next;
next += 1;
n
});
}
}
Self {
numbers,
definitions,
definition_frames,
overrides: store.footnote_markers.read().clone(),
}
}
pub fn is_definition(&self, frame_id: EntityId) -> bool {
self.definition_frames.contains(&frame_id)
}
pub fn is_nested_reference(&self, label: &str) -> bool {
!self.numbers.contains_key(label) && self.definitions.contains_key(label)
}
pub fn marker(&self, label: &str) -> String {
if let Some(m) = self.overrides.get(label) {
return m.clone();
}
match self.numbers.get(label) {
Some(n) => n.to_string(),
None => label.to_string(),
}
}
pub fn is_empty(&self) -> bool {
self.numbers.is_empty() && self.definitions.is_empty()
}
pub fn in_print_order(&self) -> Vec<(usize, String, EntityId)> {
let mut out: Vec<(usize, String, EntityId)> = self
.definitions
.iter()
.filter_map(|(label, frame)| {
self.numbers.get(label).map(|n| (*n, label.clone(), *frame))
})
.collect();
out.sort_unstable();
out
}
}
pub fn safe_label_id(label: &str) -> String {
use std::hash::{Hash, Hasher};
let kept: String = label
.chars()
.filter(|c| c.is_ascii_alphanumeric())
.collect();
let mut hasher = std::collections::hash_map::DefaultHasher::new();
label.hash(&mut hasher);
format!("fn{kept}{:016x}", hasher.finish())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn safe_label_id_is_deterministic() {
assert_eq!(safe_label_id("n1"), safe_label_id("n1"));
}
#[test]
fn safe_label_id_differs_for_different_labels() {
assert_ne!(safe_label_id("n1"), safe_label_id("n2"));
}
#[test]
fn safe_label_id_does_not_collide_on_shared_alphanumerics() {
assert_ne!(safe_label_id("fn.1"), safe_label_id("fn-1"));
}
#[test]
fn safe_label_id_is_ascii_and_starts_with_a_letter() {
let id = safe_label_id("héllo Wörld! 42");
assert!(id.chars().next().unwrap().is_ascii_alphabetic());
assert!(id.chars().all(|c| c.is_ascii_alphanumeric()));
}
}