krz/orgo

Lightning fast org-mode static site generator.

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

v0.20.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::{heading_anchor, output_path, plain_text};
 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
 24/// How a target is written in org source, so a broken-link warning names something the
 25/// author can search for.
 26impl std::fmt::Display for TargetId {
 27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 28        match self {
 29            TargetId::Id(s) => write!(f, "[[id:{s}]]"),
 30            TargetId::CustomId(s) => write!(f, "[[#{s}]]"),
 31            TargetId::Heading(s) => write!(f, "[[*{s}]]"),
 32            TargetId::File(p) => write!(f, "[[file:{p}]]"),
 33        }
 34    }
 35}
 36
 37impl TargetId {
 38    /// A stable string form used to order targets deterministically when hashing
 39    /// (so a page's `resolved_links_hash` does not depend on `HashSet` iteration order).
 40    pub fn sort_key(&self) -> String {
 41        match self {
 42            TargetId::Id(s) => format!("id:{s}"),
 43            TargetId::CustomId(s) => format!("custom:{s}"),
 44            TargetId::Heading(s) => format!("heading:{s}"),
 45            TargetId::File(p) => format!("file:{p}"),
 46        }
 47    }
 48}
 49
 50/// Where a resolved target lives, once INDEX has seen its defining file.
 51#[derive(Debug, Clone)]
 52pub struct TargetLocation {
 53    pub source_path: Utf8PathBuf,
 54    /// The page this target is emitted into. Recorded at INDEX time because it depends
 55    /// on the defining document's `#+SLUG:`, which only that document knows.
 56    pub output_path: Utf8PathBuf,
 57    /// Final URL fragment/anchor for the target, filled during resolution.
 58    pub anchor: Option<String>,
 59}
 60
 61/// Maps every collected target to its owning location (spec §4.3 "defines" edges).
 62#[derive(Debug, Default)]
 63pub struct SymbolTable {
 64    pub targets: HashMap<TargetId, TargetLocation>,
 65}
 66
 67impl SymbolTable {
 68    pub fn new() -> Self {
 69        Self::default()
 70    }
 71
 72    /// Walk one document, registering every `:ID:`/`:CUSTOM_ID:`/heading/file target.
 73    /// A target is owned by exactly one file (spec §4.3); the anchor is the fragment
 74    /// the renderer emits for that target's heading.
 75    pub fn index_document(&mut self, doc: &Document) {
 76        let path = &doc.source_path;
 77        let out = output_path(path, &doc.keywords);
 78        self.targets.insert(
 79            TargetId::File(path.clone()),
 80            TargetLocation {
 81                source_path: path.clone(),
 82                output_path: out.clone(),
 83                anchor: None,
 84            },
 85        );
 86        index_section(&doc.root, path, &out, &mut self.targets);
 87    }
 88}
 89
 90/// The set of link targets a single document *defines* (owns) — the `defines` edges of
 91/// the dependency graph (spec §4.3). Mirrors [`SymbolTable::index_document`] but returns
 92/// the targets for one file in isolation, which is what the incremental layer records
 93/// per-file in the cache manifest.
 94pub fn document_targets(doc: &Document) -> Vec<TargetId> {
 95    let mut out = vec![TargetId::File(doc.source_path.clone())];
 96    collect_targets(&doc.root, &mut out);
 97    out
 98}
 99
100fn collect_targets(section: &Section, out: &mut Vec<TargetId>) {
101    if let Some(h) = &section.heading {
102        if let Some(cid) = &h.custom_id {
103            out.push(TargetId::CustomId(cid.clone()));
104        }
105        if let Some(id) = &h.id {
106            out.push(TargetId::Id(id.clone()));
107        }
108        out.push(TargetId::Heading(plain_text(&h.title)));
109    }
110    for child in &section.children {
111        collect_targets(child, out);
112    }
113}
114
115fn index_section(
116    section: &Section,
117    path: &Utf8Path,
118    out: &Utf8Path,
119    targets: &mut HashMap<TargetId, TargetLocation>,
120) {
121    if let Some(h) = &section.heading {
122        let anchor = heading_anchor(h);
123        let mut record = |id: TargetId, anchor: Option<String>| {
124            targets.insert(
125                id,
126                TargetLocation {
127                    source_path: path.to_owned(),
128                    output_path: out.to_owned(),
129                    anchor,
130                },
131            );
132        };
133        if let Some(cid) = &h.custom_id {
134            record(TargetId::CustomId(cid.clone()), Some(cid.clone()));
135        }
136        if let Some(id) = &h.id {
137            record(TargetId::Id(id.clone()), Some(id.clone()));
138        }
139        record(TargetId::Heading(plain_text(&h.title)), Some(anchor));
140    }
141    for child in &section.children {
142        index_section(child, path, out, targets);
143    }
144}