Skip to main content

specfold_core/authoring/
mv.rs

1//! Moving or renaming a document, and rewriting every reference to it.
2
3use super::plan::Plan;
4use super::refs;
5use crate::corpus::Corpus;
6use crate::markdown::frontmatter::edit;
7use crate::model::doctype::KeyKind;
8use crate::model::{DocId, Tier, stamp};
9use crate::placement::{self, paths};
10use anyhow::{Result, anyhow, bail};
11use serde::Serialize;
12use serde_json::Value;
13
14#[derive(Debug, Clone, Default)]
15pub struct Move {
16    /// Target owner; computed from governs when absent.
17    pub owner: Option<String>,
18    /// New slug.
19    pub slug: Option<String>,
20}
21
22#[derive(Debug, Clone, Serialize)]
23pub struct Moved {
24    pub from: String,
25    pub to: String,
26    pub path: String,
27    pub references: usize,
28}
29
30pub fn plan(c: &Corpus, idx: usize, req: &Move) -> Result<(Moved, Plan)> {
31    let d = &c.docs[idx];
32    let ty = c.cfg.registry.get(&d.id.ty).ok_or_else(|| anyhow!("unknown type {}", d.id.ty))?;
33    let current = c.owners.by_id(&d.id.owner).ok_or_else(|| anyhow!("owner {} no longer exists", d.id.owner))?;
34    let owner = match &req.owner {
35        Some(o) => c.owners.by_id(o).or_else(|| c.owners.by_path(o)).ok_or_else(|| anyhow!("no owner '{o}'"))?,
36        None => placement::decide(&c.owners, &d.rel().remove("governs").unwrap_or_default(), current).owner,
37    };
38    let slug = req.slug.clone().unwrap_or_else(|| d.slug.clone());
39    if !stamp::is_slug(&slug) {
40        bail!("'{slug}' is not a kebab-case slug");
41    }
42    let key = if ty.key == KeyKind::Slug { slug.clone() } else { d.id.key.clone() };
43    let new_id = DocId::new(&ty.name, &owner.id, &key);
44    let dir = paths::type_dir(&c.cfg, &c.owners, owner, d.tier, ty);
45    let stem = paths::stem(ty, &key, &slug);
46    let new_main = paths::main_file(&dir, ty, &stem, d.dir.is_some());
47    if new_main == d.path {
48        return Ok((
49            Moved { from: d.id.to_string(), to: new_id.to_string(), path: new_main, references: 0 },
50            Plan::default(),
51        ));
52    }
53    if c.docs.iter().any(|x| x.id == new_id) {
54        bail!("{new_id} already exists");
55    }
56    let mut plan = Plan::default();
57    match &d.dir {
58        Some(bundle) => plan.mv(bundle.clone(), new_main.rsplit_once('/').map(|x| x.0.to_string()).unwrap_or_default()),
59        None => plan.mv(d.path.clone(), new_main.clone()),
60    }
61    let (old_s, new_s) = (d.id.to_string(), new_id.to_string());
62    let mut references = 0;
63    for (j, other) in c.docs.iter().enumerate() {
64        if other.tier == Tier::Committed && d.tier == Tier::Local && j != idx {
65            continue;
66        }
67        for file in std::iter::once(&other.path).chain(&other.chapters) {
68            let text = std::fs::read_to_string(c.cfg.root.join(file))?;
69            let mut body = refs::rewrite_body(&text, &old_s, &new_s);
70            let is_main = file == &other.path;
71            if is_main {
72                let mut rel_changed = false;
73                body = edit::rewrite(&body, |m| {
74                    rel_changed = refs::rewrite_rel(m, &old_s, &new_s);
75                    if j == idx {
76                        m.insert("id".into(), Value::String(new_s.clone()));
77                        m.insert("updated".into(), Value::String(stamp::today()));
78                    }
79                })?;
80                references += usize::from(rel_changed);
81            }
82            if body != text {
83                references += usize::from(j != idx);
84                let target = if j != idx {
85                    file.clone()
86                } else if is_main {
87                    new_main.clone()
88                } else {
89                    let base = d.dir.as_deref().unwrap_or("");
90                    let new_base = new_main.rsplit_once('/').map(|x| x.0).unwrap_or("");
91                    file.replacen(base, new_base, 1)
92                };
93                plan.write(target, body, false);
94            }
95        }
96    }
97    if let Some(file) = &c.aliases.file
98        && let Ok(text) = std::fs::read_to_string(c.cfg.root.join(file))
99    {
100        let new_text: String = text
101            .lines()
102            .map(|l| match l.split_once('\t') {
103                Some((o, n)) if n.trim() == old_s => format!("{o}\t{new_s}\n"),
104                _ => format!("{l}\n"),
105            })
106            .collect();
107        if new_text != text {
108            plan.write(file.clone(), new_text, false);
109        }
110    }
111    Ok((Moved { from: old_s, to: new_s, path: new_main, references }, plan))
112}