sync_readme/
render.rs

1use std::str::FromStr;
2
3use anyhow::Context;
4
5use crate::content::Content;
6
7static MARKER_REGEX: std::sync::LazyLock<regex::Regex> =
8    std::sync::LazyLock::new(|| regex::Regex::new(r#"<!-- sync-readme (\w+)?\s*(\[\[|\]\])? -->"#).expect("bad regex"));
9
10#[derive(Debug)]
11enum MarkerCategory {
12    Title,
13    Badge,
14    Rustdoc,
15}
16
17impl std::fmt::Display for MarkerCategory {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        match self {
20            Self::Title => f.write_str("title"),
21            Self::Badge => f.write_str("badge"),
22            Self::Rustdoc => f.write_str("rustdoc"),
23        }
24    }
25}
26
27impl FromStr for MarkerCategory {
28    type Err = anyhow::Error;
29
30    fn from_str(s: &str) -> Result<Self, Self::Err> {
31        match s {
32            "title" => Ok(Self::Title),
33            "badge" => Ok(Self::Badge),
34            "rustdoc" => Ok(Self::Rustdoc),
35            s => Err(anyhow::anyhow!("unknown marker {s}")),
36        }
37    }
38}
39
40#[derive(Debug)]
41struct Marker {
42    category: MarkerCategory,
43    start: usize,
44    end: usize,
45}
46
47pub fn render(readme: &str, content: &Content) -> anyhow::Result<String> {
48    let mut markers = Vec::new();
49
50    let mut iter = MARKER_REGEX.captures_iter(readme);
51
52    while let Some(capture) = iter.next() {
53        let marker = capture.get(0).expect("always a zero group");
54        let category: MarkerCategory = capture
55            .get(1)
56            .map(|s| s.as_str().parse())
57            .transpose()?
58            .context("missing category")?;
59        let open = capture.get(2).map(|c| c.as_str());
60
61        let start = marker.start();
62        let mut end = marker.end();
63        if open == Some("[[") {
64            let end_capture = iter.next().context("marker opens but never closes")?;
65            let marker = end_capture.get(0).expect("always a zero group");
66            anyhow::ensure!(
67                end_capture.get(1).is_none() && end_capture.get(2).is_some_and(|c| c.as_str() == "]]"),
68                "capture after an open must be a close: {}",
69                marker.as_str(),
70            );
71
72            end = marker.end();
73        }
74
75        markers.push(Marker { category, start, end });
76    }
77
78    let mut readme_builder = String::new();
79    let mut idx = 0;
80
81    for marker in markers {
82        readme_builder.push_str(&readme[idx..marker.start]);
83        idx = marker.end + 1;
84
85        use std::fmt::Write;
86
87        let content = match marker.category {
88            MarkerCategory::Badge => content.badge.as_str(),
89            MarkerCategory::Rustdoc => content.rustdoc.as_str(),
90            MarkerCategory::Title => content.title.as_str(),
91        }
92        .trim();
93
94        if content.is_empty() {
95            writeln!(&mut readme_builder, "<!-- sync-readme {} -->", marker.category).expect("write failed");
96        } else {
97            writeln!(
98                &mut readme_builder,
99                "<!-- sync-readme {} [[ -->\n{content}\n<!-- sync-readme ]] -->",
100                marker.category
101            )
102            .expect("write failed");
103        }
104    }
105
106    readme_builder.push_str(&readme[idx..]);
107
108    Ok(readme_builder)
109}