krz/orgo

Lightning fast org-mode static site generator.

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

v0.3.0: src/util.rs · raw

  1//! Small shared helpers used across INDEX, RESOLVE, RENDER and the site build:
  2//! flattening inline objects to plain text, slugifying heading text into anchors,
  3//! and computing relative output URLs between pages.
  4
  5use camino::{Utf8Path, Utf8PathBuf};
  6
  7use crate::model::Object;
  8
  9/// Flatten inline objects to their plain-text content (markup stripped). Used to
 10/// derive heading anchors and `[[*Heading]]` link identities (spec §4.3).
 11pub fn plain_text(objs: &[Object]) -> String {
 12    let mut out = String::new();
 13    plain_text_into(objs, &mut out);
 14    out
 15}
 16
 17fn plain_text_into(objs: &[Object], out: &mut String) {
 18    for obj in objs {
 19        match obj {
 20            Object::Text(t) => out.push_str(t),
 21            Object::Bold(i)
 22            | Object::Italic(i)
 23            | Object::Underline(i)
 24            | Object::StrikeThrough(i) => plain_text_into(i, out),
 25            Object::Verbatim(s) | Object::Code(s) | Object::Entity(s) => out.push_str(s),
 26            Object::Link(l) => {
 27                if let Some(desc) = &l.description {
 28                    plain_text_into(desc, out);
 29                }
 30            }
 31            Object::FootnoteRef { .. } | Object::Timestamp(_) | Object::LineBreak => {}
 32        }
 33    }
 34}
 35
 36/// Turn heading text into a URL-safe anchor slug.
 37pub fn slugify(text: &str) -> String {
 38    let mut out = String::new();
 39    let mut prev_dash = false;
 40    for c in text.chars() {
 41        if c.is_alphanumeric() {
 42            out.extend(c.to_lowercase());
 43            prev_dash = false;
 44        } else if !prev_dash {
 45            out.push('-');
 46            prev_dash = true;
 47        }
 48    }
 49    out.trim_matches('-').to_string()
 50}
 51
 52/// The output URL to reach `to_rel` (a source `.org` path relative to the site root)
 53/// from the page at `from_rel`, honoring an optional `anchor`. Same-file links reduce
 54/// to a bare `#anchor` fragment; cross-file links become a relative `.html` path.
 55pub fn output_url(from_rel: &Utf8Path, to_rel: &Utf8Path, anchor: Option<&str>) -> String {
 56    let path = if from_rel == to_rel {
 57        String::new()
 58    } else {
 59        let to_html = to_rel.with_extension("html");
 60        let from_dir = from_rel.parent().unwrap_or_else(|| Utf8Path::new(""));
 61        relative_path(from_dir, &to_html)
 62    };
 63    match anchor {
 64        Some(a) if !a.is_empty() => {
 65            if path.is_empty() {
 66                format!("#{a}")
 67            } else {
 68                format!("{path}#{a}")
 69            }
 70        }
 71        _ => {
 72            if path.is_empty() {
 73                "#".to_string()
 74            } else {
 75                path
 76            }
 77        }
 78    }
 79}
 80
 81/// Relative path from `from_dir` to `to`, using `../` where needed. `/`-joined for URLs.
 82fn relative_path(from_dir: &Utf8Path, to: &Utf8Path) -> String {
 83    let from_c: Vec<&str> = from_dir.components().map(|c| c.as_str()).collect();
 84    let to_c: Vec<&str> = to.components().map(|c| c.as_str()).collect();
 85    let mut i = 0;
 86    while i < from_c.len() && i < to_c.len() && from_c[i] == to_c[i] {
 87        i += 1;
 88    }
 89    let mut parts: Vec<&str> = vec![".."; from_c.len() - i];
 90    parts.extend(&to_c[i..]);
 91    parts.join("/")
 92}
 93
 94/// Resolve a `file:` link path (as written) against the linking page's directory,
 95/// normalizing `.`/`..` so it can be matched against indexed file targets.
 96pub fn normalize_link_path(from_rel: &Utf8Path, path: &Utf8Path) -> Utf8PathBuf {
 97    let base = from_rel.parent().unwrap_or_else(|| Utf8Path::new(""));
 98    let joined = base.join(path);
 99    let mut stack: Vec<&str> = Vec::new();
100    for comp in joined.components() {
101        match comp.as_str() {
102            "." => {}
103            ".." => {
104                stack.pop();
105            }
106            other => stack.push(other),
107        }
108    }
109    Utf8PathBuf::from(stack.join("/"))
110}