Skip to main content

specfold_core/export/
payload.rs

1//! The payload an exporter plugin asked for with `wants`: bodies, resolved links and a hash on
2//! top of the compact view every plugin gets. Plugins that ask for nothing get the compact view.
3
4use super::links;
5use crate::corpus::Corpus;
6use crate::graph::Graph;
7use crate::markdown::frontmatter;
8use crate::plugins::payload;
9use crate::translation;
10use serde_json::{Value, json};
11
12/// The extras a plugin can ask for in `describe`'s `wants`.
13#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
14pub struct Wants {
15    pub body: bool,
16    pub links: bool,
17}
18
19impl Wants {
20    pub fn from(list: &[String]) -> Self {
21        Wants { body: list.iter().any(|w| w == "body"), links: list.iter().any(|w| w == "links") }
22    }
23
24    pub fn any(&self) -> bool {
25        self.body || self.links
26    }
27}
28
29/// The documents in `only` with what `w` asks for; link spans are byte ranges in the body. Past
30/// `budget` bytes, bodies go as `body_path` for the plugin to read. A reference to a document left
31/// out is sent unresolved (`reason: "not exported"`) so nothing about it reaches the plugin.
32pub fn docs(c: &Corpus, g: &Graph, w: Wants, budget: Option<usize>, only: &[usize]) -> Value {
33    let mut used = 0usize;
34    let root = c.cfg.root.clone();
35    let read = |p: &str| crate::corpus::overlay::read(&root.join(p)).ok();
36    let mut out = Vec::new();
37    for &i in only {
38        let mut v = payload::doc(c, i);
39        if !w.any() {
40            out.push(v);
41            continue;
42        }
43        let d = &c.docs[i];
44        let mut parts = vec![(d.path.clone(), read(&d.path).unwrap_or_default())];
45        let mut chapters = d.chapters.clone();
46        chapters.sort();
47        parts.extend(chapters.into_iter().map(|ch| {
48            let t = read(&ch).unwrap_or_default();
49            (ch, t)
50        }));
51        let bodies: Vec<(String, String)> =
52            parts.iter().map(|(p, t)| (p.clone(), frontmatter::split(t).body.to_string())).collect();
53        let parts_for_rev: Vec<translation::Part> =
54            bodies.iter().map(|(p, b)| translation::Part { label: p.clone(), body: b.clone() }).collect();
55        let m = v.as_object_mut().expect("payload is built as an object");
56        m.insert("hash".into(), json!(translation::rev_of_parts(&parts_for_rev)));
57        let size: usize = bodies.iter().map(|(_, b)| b.len()).sum();
58        let inline = w.body && budget.is_none_or(|b| used + size <= b);
59        if inline {
60            used += size;
61        }
62        let part = |(p, b): &(String, String)| {
63            let mut o = serde_json::Map::new();
64            o.insert("path".into(), json!(p));
65            if w.body {
66                if inline {
67                    o.insert("body".into(), json!(b));
68                } else {
69                    o.insert("body_path".into(), json!(p));
70                }
71            }
72            if w.links {
73                let ls: Vec<links::Link> = links::of_body(c, g, b).into_iter().map(|l| hide(l, only)).collect();
74                o.insert("links".into(), json!(ls));
75            }
76            Value::Object(o)
77        };
78        let main = part(&bodies[0]);
79        for (k, val) in main.as_object().expect("part() returns an object") {
80            if k != "path" {
81                m.insert(k.clone(), val.clone());
82            }
83        }
84        if bodies.len() > 1 {
85            m.insert("chapter_bodies".into(), Value::Array(bodies[1..].iter().map(part).collect()));
86        }
87        out.push(v);
88    }
89    Value::Array(out)
90}
91
92/// Which documents an export may show: committed ones, plus local ones with `local`.
93pub fn visible(c: &Corpus, local: bool) -> Vec<usize> {
94    (0..c.docs.len()).filter(|&i| local || c.docs[i].tier == crate::model::Tier::Committed).collect()
95}
96
97/// Edges between documents in `only`.
98pub fn edges(c: &Corpus, g: &Graph, only: &[usize]) -> Value {
99    Value::Array(
100        g.edges
101            .iter()
102            .filter(|e| only.contains(&e.from) && only.contains(&e.to))
103            .map(|e| json!({"from": c.docs[e.from].id.to_string(), "to": c.docs[e.to].id.to_string(), "kind": e.kind, "anchor": e.anchor}))
104            .collect(),
105    )
106}
107
108fn hide(mut l: links::Link, only: &[usize]) -> links::Link {
109    if let Some(i) = l.idx
110        && !only.contains(&i)
111    {
112        l.target_id = None;
113        l.anchor = None;
114        l.resolved_path = None;
115        l.title = None;
116        l.status = None;
117        l.idx = None;
118        l.reason = Some("not exported".into());
119    }
120    l
121}