use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use super::DocumentProvenance;
#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct BlockProvenance {
#[serde(default, flatten)]
pub provenance: DocumentProvenance,
#[serde(default)]
pub anchor: Option<String>,
}
impl BlockProvenance {
pub fn from_document(provenance: DocumentProvenance) -> Self {
Self {
provenance,
anchor: None,
}
}
pub fn with_anchor(mut self, anchor: impl Into<String>) -> Self {
self.anchor = Some(anchor.into());
self
}
}
#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(transparent)]
pub struct BlockProvenanceMap {
by_block: HashMap<usize, BlockProvenance>,
}
impl BlockProvenanceMap {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, block_index: usize, provenance: BlockProvenance) {
self.by_block.insert(block_index, provenance);
}
pub fn get(&self, block_index: usize) -> Option<&BlockProvenance> {
self.by_block.get(&block_index)
}
pub fn resolve<'a>(
&'a self,
block_index: usize,
document_fallback: &'a DocumentProvenance,
) -> ResolvedProvenance<'a> {
if let Some(block) = self.by_block.get(&block_index) {
ResolvedProvenance {
provenance: &block.provenance,
anchor: block.anchor.as_deref(),
from_block_override: true,
}
} else {
ResolvedProvenance {
provenance: document_fallback,
anchor: None,
from_block_override: false,
}
}
}
pub fn is_empty(&self) -> bool {
self.by_block.is_empty()
}
pub fn len(&self) -> usize {
self.by_block.len()
}
pub fn iter(&self) -> impl Iterator<Item = (usize, &BlockProvenance)> {
self.by_block.iter().map(|(idx, prov)| (*idx, prov))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ResolvedProvenance<'a> {
pub provenance: &'a DocumentProvenance,
pub anchor: Option<&'a str>,
pub from_block_override: bool,
}
#[cfg(test)]
mod tests {
use super::*;
fn doc_provenance() -> DocumentProvenance {
DocumentProvenance::for_engine("nematic.markdown", "file:///doc.md")
}
fn other_provenance() -> DocumentProvenance {
DocumentProvenance::for_engine("nematic.gemtext", "gemini://other/")
}
#[test]
fn empty_map_resolves_to_document_fallback() {
let map = BlockProvenanceMap::new();
let doc = doc_provenance();
let resolved = map.resolve(0, &doc);
assert_eq!(resolved.provenance, &doc);
assert!(resolved.anchor.is_none());
assert!(!resolved.from_block_override);
}
#[test]
fn override_wins_for_recorded_block_only() {
let mut map = BlockProvenanceMap::new();
let other = other_provenance();
map.insert(
2,
BlockProvenance::from_document(other.clone()).with_anchor("L42-L58"),
);
let doc = doc_provenance();
let block_2 = map.resolve(2, &doc);
assert_eq!(block_2.provenance, &other);
assert_eq!(block_2.anchor, Some("L42-L58"));
assert!(block_2.from_block_override);
let block_0 = map.resolve(0, &doc);
assert_eq!(block_0.provenance, &doc);
assert!(!block_0.from_block_override);
}
#[test]
fn map_is_sparse_and_reports_size() {
let mut map = BlockProvenanceMap::new();
assert!(map.is_empty());
assert_eq!(map.len(), 0);
map.insert(5, BlockProvenance::from_document(other_provenance()));
assert!(!map.is_empty());
assert_eq!(map.len(), 1);
map.insert(5, BlockProvenance::from_document(doc_provenance()));
assert_eq!(map.len(), 1);
}
#[test]
fn with_anchor_attaches_anchor() {
let bp = BlockProvenance::from_document(other_provenance()).with_anchor("guid:abc");
assert_eq!(bp.anchor.as_deref(), Some("guid:abc"));
assert_eq!(
bp.provenance.canonical_uri.as_deref(),
Some("gemini://other/")
);
}
#[test]
fn get_returns_recorded_block_only() {
let mut map = BlockProvenanceMap::new();
map.insert(7, BlockProvenance::from_document(other_provenance()));
assert!(map.get(7).is_some());
assert!(map.get(0).is_none());
}
}