]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/html/length_limit.rs
Rollup merge of #107615 - notriddle:notriddle/nbsp, r=GuillaumeGomez
[rust.git] / src / librustdoc / html / length_limit.rs
1 //! See [`HtmlWithLimit`].
2
3 use std::fmt::Write;
4 use std::ops::ControlFlow;
5
6 use crate::html::escape::Escape;
7
8 /// A buffer that allows generating HTML with a length limit.
9 ///
10 /// This buffer ensures that:
11 ///
12 /// * all tags are closed,
13 /// * tags are closed in the reverse order of when they were opened (i.e., the correct HTML order),
14 /// * no tags are left empty (e.g., `<em></em>`) due to the length limit being reached,
15 /// * all text is escaped.
16 #[derive(Debug)]
17 pub(super) struct HtmlWithLimit {
18     buf: String,
19     len: usize,
20     limit: usize,
21     /// A list of tags that have been requested to be opened via [`Self::open_tag()`]
22     /// but have not actually been pushed to `buf` yet. This ensures that tags are not
23     /// left empty (e.g., `<em></em>`) due to the length limit being reached.
24     queued_tags: Vec<&'static str>,
25     /// A list of all tags that have been opened but not yet closed.
26     unclosed_tags: Vec<&'static str>,
27 }
28
29 impl HtmlWithLimit {
30     /// Create a new buffer, with a limit of `length_limit`.
31     pub(super) fn new(length_limit: usize) -> Self {
32         let buf = if length_limit > 1000 {
33             // If the length limit is really large, don't preallocate tons of memory.
34             String::new()
35         } else {
36             // The length limit is actually a good heuristic for initial allocation size.
37             // Measurements showed that using it as the initial capacity ended up using less memory
38             // than `String::new`.
39             // See https://github.com/rust-lang/rust/pull/88173#discussion_r692531631 for more.
40             String::with_capacity(length_limit)
41         };
42         Self {
43             buf,
44             len: 0,
45             limit: length_limit,
46             unclosed_tags: Vec::new(),
47             queued_tags: Vec::new(),
48         }
49     }
50
51     /// Finish using the buffer and get the written output.
52     /// This function will close all unclosed tags for you.
53     pub(super) fn finish(mut self) -> String {
54         self.close_all_tags();
55         self.buf
56     }
57
58     /// Write some plain text to the buffer, escaping as needed.
59     ///
60     /// This function skips writing the text if the length limit was reached
61     /// and returns [`ControlFlow::Break`].
62     pub(super) fn push(&mut self, text: &str) -> ControlFlow<(), ()> {
63         if self.len + text.len() > self.limit {
64             return ControlFlow::Break(());
65         }
66
67         self.flush_queue();
68         write!(self.buf, "{}", Escape(text)).unwrap();
69         self.len += text.len();
70
71         ControlFlow::Continue(())
72     }
73
74     /// Open an HTML tag.
75     ///
76     /// **Note:** HTML attributes have not yet been implemented.
77     /// This function will panic if called with a non-alphabetic `tag_name`.
78     pub(super) fn open_tag(&mut self, tag_name: &'static str) {
79         assert!(
80             tag_name.chars().all(|c| ('a'..='z').contains(&c)),
81             "tag_name contained non-alphabetic chars: {:?}",
82             tag_name
83         );
84         self.queued_tags.push(tag_name);
85     }
86
87     /// Close the most recently opened HTML tag.
88     pub(super) fn close_tag(&mut self) {
89         match self.unclosed_tags.pop() {
90             // Close the most recently opened tag.
91             Some(tag_name) => write!(self.buf, "</{}>", tag_name).unwrap(),
92             // There are valid cases where `close_tag()` is called without
93             // there being any tags to close. For example, this occurs when
94             // a tag is opened after the length limit is exceeded;
95             // `flush_queue()` will never be called, and thus, the tag will
96             // not end up being added to `unclosed_tags`.
97             None => {}
98         }
99     }
100
101     /// Write all queued tags and add them to the `unclosed_tags` list.
102     fn flush_queue(&mut self) {
103         for tag_name in self.queued_tags.drain(..) {
104             write!(self.buf, "<{}>", tag_name).unwrap();
105
106             self.unclosed_tags.push(tag_name);
107         }
108     }
109
110     /// Close all unclosed tags.
111     fn close_all_tags(&mut self) {
112         while !self.unclosed_tags.is_empty() {
113             self.close_tag();
114         }
115     }
116 }
117
118 #[cfg(test)]
119 mod tests;