krz/orgo

Lightning fast org-mode static site generator.

clone: git clone https://gitbay.org/krz/orgo.git

v0.3.0: src/index.rs · raw

  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) = &section.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 &section.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) = &section.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 &section.children {
131        index_section(child, path, targets);
132    }
133}