Skip to main content

specfold_core/assets/
asset.rs

1//! One asset file as read: its kind, location, frontmatter and text.
2
3use super::join;
4use crate::config::types::AssetKind;
5use crate::graph::{Graph, Resolution};
6use crate::markdown::frontmatter;
7use serde_json::{Map, Value};
8use std::collections::BTreeSet;
9
10#[derive(Debug, Clone)]
11pub struct Asset {
12    pub kind: AssetKind,
13    /// Repository-relative path of the file read (SKILL.md for a skill).
14    pub path: String,
15    /// The name the location gives: a skill's directory, otherwise the file stem.
16    pub stem: String,
17    /// Frontmatter; None when the file has none.
18    pub fields: Option<Map<String, Value>>,
19    /// Why the frontmatter could not be read.
20    pub invalid: Option<String>,
21    /// 1-based line where the body starts.
22    pub body_line: usize,
23    pub text: String,
24}
25
26impl Asset {
27    /// A frontmatter string field.
28    pub fn field(&self, key: &str) -> Option<&str> {
29        self.fields.as_ref()?.get(key)?.as_str()
30    }
31
32    /// The declared name, or the one the location gives.
33    pub fn name(&self) -> &str {
34        self.field("name").unwrap_or(&self.stem)
35    }
36
37    pub fn body(&self) -> &str {
38        frontmatter::split(&self.text).body
39    }
40
41    /// The documents this asset names, by index: a token that is a document id or path, written
42    /// from the repository root or relative to the asset. Aliases are left out so a common word
43    /// is never a link. Only read: nothing is added to the graph.
44    pub fn docs(&self, g: &Graph) -> Vec<usize> {
45        let dir = self.path.rsplit_once('/').map(|(d, _)| d).unwrap_or("");
46        let mut out = BTreeSet::new();
47        let ok = |c: char| c.is_ascii_alphanumeric() || ":/._#-".contains(c);
48        for raw in self.text.split(|c: char| !ok(c)) {
49            let t = raw.trim_end_matches(['.', ':', '#']);
50            if !(t.contains(':') || t.contains('/') || t.ends_with(".md")) || t.contains("://") {
51                continue;
52            }
53            for cand in [t.to_string(), join(dir, t)] {
54                if let Resolution::Doc { idx, via_alias: false, .. } = g.resolve(&cand) {
55                    out.insert(idx);
56                    break;
57                }
58            }
59        }
60        out.into_iter().collect()
61    }
62}