krz/orgo
Lightning fast org-mode static site generator.
clone: git clone https://gitbay.org/krz/orgo.git
1//! INDEX stage (spec §2.1, §4.3): collect link targets across all documents into a
2//! global symbol table. This is the only inherently global stage before RESOLVE.
3
4use std::collections::HashMap;
5
6use camino::{Utf8Path, Utf8PathBuf};
7use serde::{Deserialize, Serialize};
8
9use crate::model::{Document, Section};
10use crate::util::{plain_text, slugify};
11
12/// Identity of a link target. A target is owned by exactly one file (spec §4.3).
13///
14/// `Serialize`/`Deserialize` so the dependency graph (defines/uses edges) round-trips
15/// through the on-disk cache manifest (spec §4.5).
16#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub enum TargetId {
18 Id(String),
19 CustomId(String),
20 Heading(String),
21 File(Utf8PathBuf),
22}
23
24impl TargetId {
25 /// A stable string form used to order targets deterministically when hashing
26 /// (so a page's `resolved_links_hash` does not depend on `HashSet` iteration order).
27 pub fn sort_key(&self) -> String {
28 match self {
29 TargetId::Id(s) => format!("id:{s}"),
30 TargetId::CustomId(s) => format!("custom:{s}"),
31 TargetId::Heading(s) => format!("heading:{s}"),
32 TargetId::File(p) => format!("file:{p}"),
33 }
34 }
35}
36
37/// Where a resolved target lives, once INDEX has seen its defining file.
38#[derive(Debug, Clone)]
39pub struct TargetLocation {
40 pub source_path: Utf8PathBuf,
41 /// Final URL fragment/anchor for the target, filled during resolution.
42 pub anchor: Option<String>,
43}
44
45/// Maps every collected target to its owning location (spec §4.3 "defines" edges).
46#[derive(Debug, Default)]
47pub struct SymbolTable {
48 pub targets: HashMap<TargetId, TargetLocation>,
49}
50
51impl SymbolTable {
52 pub fn new() -> Self {
53 Self::default()
54 }
55
56 /// Walk one document, registering every `:ID:`/`:CUSTOM_ID:`/heading/file target.
57 /// A target is owned by exactly one file (spec §4.3); the anchor is the fragment
58 /// the renderer emits for that target's heading.
59 pub fn index_document(&mut self, doc: &Document) {
60 let path = &doc.source_path;
61 self.targets.insert(
62 TargetId::File(path.clone()),
63 TargetLocation {
64 source_path: path.clone(),
65 anchor: None,
66 },
67 );
68 index_section(&doc.root, path, &mut self.targets);
69 }
70}
71
72/// The set of link targets a single document *defines* (owns) — the `defines` edges of
73/// the dependency graph (spec §4.3). Mirrors [`SymbolTable::index_document`] but returns
74/// the targets for one file in isolation, which is what the incremental layer records
75/// per-file in the cache manifest.
76pub fn document_targets(doc: &Document) -> Vec<TargetId> {
77 let mut out = vec![TargetId::File(doc.source_path.clone())];
78 collect_targets(&doc.root, &mut out);
79 out
80}
81
82fn collect_targets(section: &Section, out: &mut Vec<TargetId>) {
83 if let Some(h) = §ion.heading {
84 if let Some(cid) = &h.custom_id {
85 out.push(TargetId::CustomId(cid.clone()));
86 }
87 if let Some(id) = &h.id {
88 out.push(TargetId::Id(id.clone()));
89 }
90 out.push(TargetId::Heading(plain_text(&h.title)));
91 }
92 for child in §ion.children {
93 collect_targets(child, out);
94 }
95}
96
97fn index_section(section: &Section, path: &Utf8Path, targets: &mut HashMap<TargetId, TargetLocation>) {
98 if let Some(h) = §ion.heading {
99 let anchor = h
100 .custom_id
101 .clone()
102 .or_else(|| h.id.clone())
103 .unwrap_or_else(|| slugify(&plain_text(&h.title)));
104 if let Some(cid) = &h.custom_id {
105 targets.insert(
106 TargetId::CustomId(cid.clone()),
107 TargetLocation {
108 source_path: path.to_owned(),
109 anchor: Some(cid.clone()),
110 },
111 );
112 }
113 if let Some(id) = &h.id {
114 targets.insert(
115 TargetId::Id(id.clone()),
116 TargetLocation {
117 source_path: path.to_owned(),
118 anchor: Some(id.clone()),
119 },
120 );
121 }
122 targets.insert(
123 TargetId::Heading(plain_text(&h.title)),
124 TargetLocation {
125 source_path: path.to_owned(),
126 anchor: Some(anchor),
127 },
128 );
129 }
130 for child in §ion.children {
131 index_section(child, path, targets);
132 }
133}