rustdoc/html/render/
sorted_template.rs1use 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#[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#[derive(Serialize, Deserialize, Debug, Clone)]
22struct Offset {
23 start: usize,
25 fragment_lengths: Vec<usize>,
27}
28
29impl<F> SortedTemplate<F> {
30 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 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 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 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 f.write_str(sep)?;
67 f.write_str(fragment)?;
68 fragment_lengths.push(f.delta);
69 }
70 let offset = Offset { start: self.before.len(), fragment_lengths };
71 let offset = serde_json::to_string(&offset).unwrap();
72 write!(f, "{}\n{}{}{}", self.after, F::COMMENT_START, offset, F::COMMENT_END)
73 }
74}
75
76impl<F: FileFormat> FromStr for SortedTemplate<F> {
77 type Err = Error;
78 fn from_str(s: &str) -> Result<Self, Self::Err> {
79 let (s, offset) = s
80 .rsplit_once("\n")
81 .ok_or(Error("invalid format: should have a newline on the last line"))?;
82 let offset = offset
83 .strip_prefix(F::COMMENT_START)
84 .ok_or(Error("last line expected to start with a comment"))?;
85 let offset = offset
86 .strip_suffix(F::COMMENT_END)
87 .ok_or(Error("last line expected to end with a comment"))?;
88 let offset: Offset = serde_json::from_str(offset).map_err(|_| {
89 Error("could not find insertion location descriptor object on last line")
90 })?;
91 let (before, mut s) =
92 s.split_at_checked(offset.start).ok_or(Error("invalid start: out of bounds"))?;
93 let mut fragments = BTreeSet::default();
94 for (p, &index) in offset.fragment_lengths.iter().with_position() {
95 let (fragment, rest) =
96 s.split_at_checked(index).ok_or(Error("invalid fragment length: out of bounds"))?;
97 s = rest;
98 let sep = if matches!(p, Position::First | Position::Only) { "" } else { F::SEPARATOR };
99 let fragment = fragment
100 .strip_prefix(sep)
101 .ok_or(Error("invalid fragment length: expected to find separator here"))?;
102 fragments.insert(fragment.to_string());
103 }
104 Ok(Self {
105 format: PhantomData,
106 before: before.to_string(),
107 after: s.to_string(),
108 fragments,
109 })
110 }
111}
112
113pub(crate) trait FileFormat {
114 const COMMENT_START: &'static str;
115 const COMMENT_END: &'static str;
116 const SEPARATOR: &'static str;
117}
118
119#[derive(Debug, Clone)]
120pub(crate) struct Html;
121
122impl FileFormat for Html {
123 const COMMENT_START: &'static str = "<!--";
124 const COMMENT_END: &'static str = "-->";
125 const SEPARATOR: &'static str = "";
126}
127
128#[derive(Debug, Clone)]
129pub(crate) struct Js;
130
131impl FileFormat for Js {
132 const COMMENT_START: &'static str = "//";
133 const COMMENT_END: &'static str = "";
134 const SEPARATOR: &'static str = ",";
135}
136
137#[derive(Debug, Clone)]
138pub(crate) struct Error(&'static str);
139
140impl fmt::Display for Error {
141 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142 write!(f, "invalid template: {}", self.0)
143 }
144}
145
146struct DeltaWriter<W> {
147 inner: W,
148 delta: usize,
149}
150
151impl<W: fmt::Write> fmt::Write for DeltaWriter<W> {
152 fn write_str(&mut self, s: &str) -> fmt::Result {
153 self.inner.write_str(s)?;
154 self.delta += s.len();
155 Ok(())
156 }
157}
158
159#[cfg(test)]
160mod tests;