rustdoc/html/render/
sorted_template.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
use std::collections::BTreeSet;
use std::fmt::{self, Write as _};
use std::marker::PhantomData;
use std::str::FromStr;

use itertools::{Itertools as _, Position};
use serde::{Deserialize, Serialize};

/// Append-only templates for sorted, deduplicated lists of items.
///
/// Last line of the rendered output is a comment encoding the next insertion point.
#[derive(Debug, Clone)]
pub(crate) struct SortedTemplate<F> {
    format: PhantomData<F>,
    before: String,
    after: String,
    fragments: BTreeSet<String>,
}

/// Written to last line of file to specify the location of each fragment
#[derive(Serialize, Deserialize, Debug, Clone)]
struct Offset {
    /// Index of the first byte in the template
    start: usize,
    /// The length of each fragment in the encoded template, including the separator
    fragment_lengths: Vec<usize>,
}

impl<F> SortedTemplate<F> {
    /// Generate this template from arbitary text.
    /// Will insert wherever the substring `delimiter` can be found.
    /// Errors if it does not appear exactly once.
    pub(crate) fn from_template(template: &str, delimiter: &str) -> Result<Self, Error> {
        let mut split = template.split(delimiter);
        let before = split.next().ok_or(Error("delimiter should appear at least once"))?;
        let after = split.next().ok_or(Error("delimiter should appear at least once"))?;
        // not `split_once` because we want to check for too many occurrences
        if split.next().is_some() {
            return Err(Error("delimiter should appear at most once"));
        }
        Ok(Self::from_before_after(before, after))
    }

    /// Template will insert fragments between `before` and `after`
    pub(crate) fn from_before_after<S: ToString, T: ToString>(before: S, after: T) -> Self {
        let before = before.to_string();
        let after = after.to_string();
        Self { format: PhantomData, before, after, fragments: Default::default() }
    }
}

impl<F> SortedTemplate<F> {
    /// Adds this text to the template
    pub(crate) fn append(&mut self, insert: String) {
        self.fragments.insert(insert);
    }
}

impl<F: FileFormat> fmt::Display for SortedTemplate<F> {
    fn fmt(&self, mut f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut fragment_lengths = Vec::default();
        write!(f, "{}", self.before)?;
        for (p, fragment) in self.fragments.iter().with_position() {
            let mut f = DeltaWriter { inner: &mut f, delta: 0 };
            let sep = if matches!(p, Position::First | Position::Only) { "" } else { F::SEPARATOR };
            write!(f, "{}{}", sep, fragment)?;
            fragment_lengths.push(f.delta);
        }
        let offset = Offset { start: self.before.len(), fragment_lengths };
        let offset = serde_json::to_string(&offset).unwrap();
        write!(f, "{}\n{}{}{}", self.after, F::COMMENT_START, offset, F::COMMENT_END)
    }
}

impl<F: FileFormat> FromStr for SortedTemplate<F> {
    type Err = Error;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let (s, offset) = s
            .rsplit_once("\n")
            .ok_or(Error("invalid format: should have a newline on the last line"))?;
        let offset = offset
            .strip_prefix(F::COMMENT_START)
            .ok_or(Error("last line expected to start with a comment"))?;
        let offset = offset
            .strip_suffix(F::COMMENT_END)
            .ok_or(Error("last line expected to end with a comment"))?;
        let offset: Offset = serde_json::from_str(offset).map_err(|_| {
            Error("could not find insertion location descriptor object on last line")
        })?;
        let (before, mut s) =
            s.split_at_checked(offset.start).ok_or(Error("invalid start: out of bounds"))?;
        let mut fragments = BTreeSet::default();
        for (p, &index) in offset.fragment_lengths.iter().with_position() {
            let (fragment, rest) =
                s.split_at_checked(index).ok_or(Error("invalid fragment length: out of bounds"))?;
            s = rest;
            let sep = if matches!(p, Position::First | Position::Only) { "" } else { F::SEPARATOR };
            let fragment = fragment
                .strip_prefix(sep)
                .ok_or(Error("invalid fragment length: expected to find separator here"))?;
            fragments.insert(fragment.to_string());
        }
        Ok(Self {
            format: PhantomData,
            before: before.to_string(),
            after: s.to_string(),
            fragments,
        })
    }
}

pub(crate) trait FileFormat {
    const COMMENT_START: &'static str;
    const COMMENT_END: &'static str;
    const SEPARATOR: &'static str;
}

#[derive(Debug, Clone)]
pub(crate) struct Html;

impl FileFormat for Html {
    const COMMENT_START: &'static str = "<!--";
    const COMMENT_END: &'static str = "-->";
    const SEPARATOR: &'static str = "";
}

#[derive(Debug, Clone)]
pub(crate) struct Js;

impl FileFormat for Js {
    const COMMENT_START: &'static str = "//";
    const COMMENT_END: &'static str = "";
    const SEPARATOR: &'static str = ",";
}

#[derive(Debug, Clone)]
pub(crate) struct Error(&'static str);

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "invalid template: {}", self.0)
    }
}

struct DeltaWriter<W> {
    inner: W,
    delta: usize,
}

impl<W: fmt::Write> fmt::Write for DeltaWriter<W> {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        self.inner.write_str(s)?;
        self.delta += s.len();
        Ok(())
    }
}

#[cfg(test)]
mod tests;