rustdoc/html/render/
sorted_template.rs

1use std::collections::BTreeSet;
2use std::fmt::{self, Write as _};
3use std::marker::PhantomData;
4use std::str::FromStr;
5
6use itertools::{Itertools as _, Position};
7use serde::{Deserialize, Serialize};
8
9/// Append-only templates for sorted, deduplicated lists of items.
10///
11/// Last line of the rendered output is a comment encoding the next insertion point.
12#[derive(Debug, Clone)]
13pub(crate) struct SortedTemplate<F> {
14    format: PhantomData<F>,
15    before: String,
16    after: String,
17    fragments: BTreeSet<String>,
18}
19
20/// Written to last line of file to specify the location of each fragment
21#[derive(Serialize, Deserialize, Debug, Clone)]
22struct Offset {
23    /// Index of the first byte in the template
24    start: usize,
25    /// The length of each fragment in the encoded template, including the separator
26    fragment_lengths: Vec<usize>,
27}
28
29impl<F> SortedTemplate<F> {
30    /// Generate this template from arbitrary text.
31    /// Will insert wherever the substring `delimiter` can be found.
32    /// Errors if it does not appear exactly once.
33    pub(crate) fn from_template(template: &str, delimiter: &str) -> Result<Self, Error> {
34        let mut split = template.split(delimiter);
35        let before = split.next().ok_or(Error("delimiter should appear at least once"))?;
36        let after = split.next().ok_or(Error("delimiter should appear at least once"))?;
37        // not `split_once` because we want to check for too many occurrences
38        if split.next().is_some() {
39            return Err(Error("delimiter should appear at most once"));
40        }
41        Ok(Self::from_before_after(before, after))
42    }
43
44    /// Template will insert fragments between `before` and `after`
45    pub(crate) fn from_before_after<S: ToString, T: ToString>(before: S, after: T) -> Self {
46        let before = before.to_string();
47        let after = after.to_string();
48        Self { format: PhantomData, before, after, fragments: Default::default() }
49    }
50}
51
52impl<F> SortedTemplate<F> {
53    /// Adds this text to the template
54    pub(crate) fn append(&mut self, insert: String) {
55        self.fragments.insert(insert);
56    }
57}
58
59impl<F: FileFormat> fmt::Display for SortedTemplate<F> {
60    fn fmt(&self, mut f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        let mut fragment_lengths = Vec::default();
62        write!(f, "{}", self.before)?;
63        for (p, fragment) in self.fragments.iter().with_position() {
64            let mut f = DeltaWriter { inner: &mut f, delta: 0 };
65            let sep = if matches!(p, Position::First | Position::Only) { "" } else { F::SEPARATOR };
66            write!(f, "{}{}", sep, fragment)?;
67            fragment_lengths.push(f.delta);
68        }
69        let offset = Offset { start: self.before.len(), fragment_lengths };
70        let offset = serde_json::to_string(&offset).unwrap();
71        write!(f, "{}\n{}{}{}", self.after, F::COMMENT_START, offset, F::COMMENT_END)
72    }
73}
74
75impl<F: FileFormat> FromStr for SortedTemplate<F> {
76    type Err = Error;
77    fn from_str(s: &str) -> Result<Self, Self::Err> {
78        let (s, offset) = s
79            .rsplit_once("\n")
80            .ok_or(Error("invalid format: should have a newline on the last line"))?;
81        let offset = offset
82            .strip_prefix(F::COMMENT_START)
83            .ok_or(Error("last line expected to start with a comment"))?;
84        let offset = offset
85            .strip_suffix(F::COMMENT_END)
86            .ok_or(Error("last line expected to end with a comment"))?;
87        let offset: Offset = serde_json::from_str(offset).map_err(|_| {
88            Error("could not find insertion location descriptor object on last line")
89        })?;
90        let (before, mut s) =
91            s.split_at_checked(offset.start).ok_or(Error("invalid start: out of bounds"))?;
92        let mut fragments = BTreeSet::default();
93        for (p, &index) in offset.fragment_lengths.iter().with_position() {
94            let (fragment, rest) =
95                s.split_at_checked(index).ok_or(Error("invalid fragment length: out of bounds"))?;
96            s = rest;
97            let sep = if matches!(p, Position::First | Position::Only) { "" } else { F::SEPARATOR };
98            let fragment = fragment
99                .strip_prefix(sep)
100                .ok_or(Error("invalid fragment length: expected to find separator here"))?;
101            fragments.insert(fragment.to_string());
102        }
103        Ok(Self {
104            format: PhantomData,
105            before: before.to_string(),
106            after: s.to_string(),
107            fragments,
108        })
109    }
110}
111
112pub(crate) trait FileFormat {
113    const COMMENT_START: &'static str;
114    const COMMENT_END: &'static str;
115    const SEPARATOR: &'static str;
116}
117
118#[derive(Debug, Clone)]
119pub(crate) struct Html;
120
121impl FileFormat for Html {
122    const COMMENT_START: &'static str = "<!--";
123    const COMMENT_END: &'static str = "-->";
124    const SEPARATOR: &'static str = "";
125}
126
127#[derive(Debug, Clone)]
128pub(crate) struct Js;
129
130impl FileFormat for Js {
131    const COMMENT_START: &'static str = "//";
132    const COMMENT_END: &'static str = "";
133    const SEPARATOR: &'static str = ",";
134}
135
136#[derive(Debug, Clone)]
137pub(crate) struct Error(&'static str);
138
139impl fmt::Display for Error {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        write!(f, "invalid template: {}", self.0)
142    }
143}
144
145struct DeltaWriter<W> {
146    inner: W,
147    delta: usize,
148}
149
150impl<W: fmt::Write> fmt::Write for DeltaWriter<W> {
151    fn write_str(&mut self, s: &str) -> fmt::Result {
152        self.inner.write_str(s)?;
153        self.delta += s.len();
154        Ok(())
155    }
156}
157
158#[cfg(test)]
159mod tests;