Skip to main content

mant_ir/references/
events.rs

1//! Optional navigation facts emitted by the same authoritative content walk.
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6use super::{LinkOccurrenceRef, ReferenceOwnerRef};
7use crate::{
8    ContentLocation, ContentLocationRef, DocumentReference, EntryOwnerLocationRef, FragmentAlias,
9    LinkTarget, NodeId, SourceSpan,
10};
11
12/// Kind of the original typed link, before any destination lookup.
13#[derive(
14    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
15)]
16#[serde(rename_all = "kebab-case")]
17pub enum ReferenceTargetType {
18    /// Registered Markdown document reference.
19    Document,
20    /// Native manual reference, with or without a section.
21    Manual,
22    /// Same-document content identity or authored fragment.
23    Local,
24    /// External URI; no existence probing is implied.
25    External,
26    /// Email address; no existence probing is implied.
27    Email,
28}
29
30impl ReferenceTargetType {
31    /// Classify without cloning or inspecting a target's strings.
32    #[must_use]
33    pub const fn of(target: &LinkTarget) -> Self {
34        match target {
35            LinkTarget::Document { .. } => Self::Document,
36            LinkTarget::Manual { .. } => Self::Manual,
37            LinkTarget::Section { .. } => Self::Local,
38            LinkTarget::External { .. } => Self::External,
39            LinkTarget::Email { .. } => Self::Email,
40        }
41    }
42}
43
44/// Allocation-free link selection, applied before inspecting target strings.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct ReferenceLinkFilter(u8);
47
48impl ReferenceLinkFilter {
49    /// No visible-link callbacks or target-string inspection.
50    pub const NONE: Self = Self(0);
51    /// Every original typed link.
52    pub const ALL: Self = Self(31);
53    /// Markdown and manual targets, the default outline discovery policy.
54    pub const DOCUMENTS: Self = Self(3);
55
56    /// Select these types; duplicates have no additional effect.
57    #[must_use]
58    pub fn from_types(types: &[ReferenceTargetType]) -> Self {
59        Self(types.iter().fold(0, |bits, kind| bits | (1 << *kind as u8)))
60    }
61
62    /// Whether the original target kind is selected.
63    #[must_use]
64    pub const fn contains(self, target: &LinkTarget) -> bool {
65        self.0 & (1 << ReferenceTargetType::of(target) as u8) != 0
66    }
67}
68
69/// Requested event families. Unrequested alias/domain metadata is not inspected.
70#[derive(Debug, Clone, Copy)]
71pub struct NavigationScanOptions {
72    /// Visible target types to visit.
73    pub links: ReferenceLinkFilter,
74    /// Emit canonical destinations and authored fragment aliases.
75    pub targets: bool,
76    /// Emit independent semantic entry-set relations.
77    pub entry_sets: bool,
78}
79
80impl Default for NavigationScanOptions {
81    fn default() -> Self {
82        Self {
83            links: ReferenceLinkFilter::ALL,
84            targets: false,
85            entry_sets: false,
86        }
87    }
88}
89
90/// Exact destination in an already loaded document, not a readable selector.
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
92#[serde(
93    tag = "kind",
94    rename_all = "kebab-case",
95    rename_all_fields = "camelCase",
96    deny_unknown_fields
97)]
98pub enum ContentReveal {
99    /// Document origin, including heading-only or alias-only documents.
100    Document {},
101    /// Section start, independent of semantic entries.
102    Section {
103        /// Zero-based section path.
104        sections: Vec<u32>,
105    },
106    /// Original list/definition item; not its remote document target.
107    Owner {
108        /// Zero-based section path.
109        sections: Vec<u32>,
110        /// Exact containing list/definition-list path.
111        blocks: Vec<crate::ContentBlockStep>,
112        /// Zero-based item index.
113        item_index: u32,
114    },
115    /// Exact inline destination, including zero-width anchors.
116    Inline {
117        /// Snapshot-local inline node position.
118        location: ContentLocation,
119    },
120}
121
122/// Borrowed counterpart, valid only during a navigation callback.
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum ContentRevealRef<'a> {
125    /// Document origin.
126    Document,
127    /// Section start.
128    Section(&'a [u32]),
129    /// Original item.
130    Owner(EntryOwnerLocationRef<'a>),
131    /// Exact inline node.
132    Inline(ContentLocationRef<'a>),
133}
134
135impl ContentReveal {
136    /// Borrow an exact destination without allocating another coordinate path.
137    #[must_use]
138    pub fn as_ref(&self) -> ContentRevealRef<'_> {
139        match self {
140            Self::Document {} => ContentRevealRef::Document,
141            Self::Section { sections } => ContentRevealRef::Section(sections),
142            Self::Owner {
143                sections,
144                blocks,
145                item_index,
146            } => ContentRevealRef::Owner(EntryOwnerLocationRef {
147                sections,
148                blocks,
149                item_index: *item_index,
150            }),
151            Self::Inline { location } => ContentRevealRef::Inline(location.as_ref()),
152        }
153    }
154}
155
156impl ContentRevealRef<'_> {
157    /// Number of coordinates inspected or retained by this destination.
158    #[must_use]
159    pub fn depth(self) -> usize {
160        match self {
161            Self::Document => 0,
162            Self::Section(sections) => sections.len(),
163            Self::Owner(owner) => owner
164                .sections
165                .len()
166                .saturating_add(owner.blocks.len())
167                .saturating_add(1),
168            Self::Inline(location) => location.depth(),
169        }
170    }
171
172    /// Exact compact encoded size, checked before allocation.
173    #[must_use]
174    pub fn encoded_size_bound(self) -> usize {
175        match self {
176            Self::Document => r#"{"kind":"document"}"#.len(),
177            Self::Section(sections) => {
178                ContentLocationRef::SectionHeading {
179                    sections,
180                    path: &[],
181                }
182                .encoded_len()
183                    - "-heading".len()
184                    - ",\"path\":[]".len()
185            }
186            Self::Owner(owner) => {
187                let raw = ContentLocationRef::Content {
188                    sections: owner.sections,
189                    blocks: owner.blocks,
190                    root: crate::ContentInlineRoot::Inlines,
191                    path: &[],
192                }
193                .encoded_len();
194                raw - "content".len() + "owner".len()
195                    - ",\"root\":{\"kind\":\"inlines\"},\"path\":[]".len()
196                    + ",\"itemIndex\":".len()
197                    + owner
198                        .item_index
199                        .checked_ilog10()
200                        .map_or(1, |n| n as usize + 1)
201            }
202            Self::Inline(location) => location
203                .encoded_len()
204                .saturating_add(r#"{"kind":"inline","location":}"#.len()),
205        }
206    }
207
208    /// Retain a bounded destination. Callers charge copy work/materialization first.
209    #[must_use]
210    pub fn to_owned(self) -> Option<ContentReveal> {
211        if self.depth() > crate::MAX_CONTENT_DEPTH
212            || self.encoded_size_bound() > crate::MAX_CONTENT_LOCATION_BYTES
213        {
214            return None;
215        }
216        Some(match self {
217            Self::Document => ContentReveal::Document {},
218            Self::Section(sections) => ContentReveal::Section {
219                sections: sections.to_vec(),
220            },
221            Self::Owner(owner) => ContentReveal::Owner {
222                sections: owner.sections.to_vec(),
223                blocks: owner.blocks.to_vec(),
224                item_index: owner.item_index,
225            },
226            Self::Inline(location) => ContentReveal::Inline {
227                location: location.to_owned()?,
228            },
229        })
230    }
231}
232
233/// One logical destination and all explicitly authored aliases at that location.
234#[derive(Debug, Clone, Copy)]
235pub struct NavigationTargetRef<'ir, 'path> {
236    /// Exact canonical identity; never normalized again during navigation.
237    pub id: &'ir NodeId,
238    /// Authored spellings, inspected only when target events are requested.
239    pub aliases: &'ir [FragmentAlias],
240    /// Exact destination in this loaded IR snapshot.
241    pub reveal: ContentRevealRef<'path>,
242}
243
244/// Independent entry-set relation, never counted as a visible occurrence.
245#[derive(Debug, Clone, Copy)]
246pub struct EntrySetReferenceRef<'ir, 'path> {
247    /// Original restricted document/manual reference.
248    pub reference: &'ir DocumentReference,
249    /// Actual declaring item, not a form copy.
250    pub owner: ReferenceOwnerRef<'ir, 'path>,
251    /// Declaration provenance for source-order merging.
252    pub source: Option<SourceSpan>,
253}
254
255/// Optional facts from one bounded DFS over authoritative content.
256#[derive(Debug, Clone, Copy)]
257pub enum NavigationEvent<'ir, 'path> {
258    /// One selected real `Inline::Link`.
259    Link(LinkOccurrenceRef<'ir, 'path>),
260    /// A local reveal destination; no catalog lookup is performed.
261    Target(NavigationTargetRef<'ir, 'path>),
262    /// An independent semantic relation.
263    EntrySet(EntrySetReferenceRef<'ir, 'path>),
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    #[test]
270    fn reveal_sizes_match_closed_wire_and_do_not_reject_deep_sections() {
271        let sections = vec![u32::MAX; 200];
272        let blocks = [crate::ContentBlockStep::Block { index: 123 }];
273        for reveal in [
274            ContentRevealRef::Document,
275            ContentRevealRef::Section(&sections),
276            ContentRevealRef::Owner(EntryOwnerLocationRef {
277                sections: &[2],
278                blocks: &blocks,
279                item_index: 12,
280            }),
281            ContentRevealRef::Inline(ContentLocationRef::DocumentHeading { path: &[1] }),
282        ] {
283            let owned = reveal.to_owned().unwrap();
284            assert_eq!(
285                reveal.encoded_size_bound(),
286                serde_json::to_vec(&owned).unwrap().len()
287            );
288        }
289    }
290}