rustdoc/html/
length_limit.rs

1//! See [`HtmlWithLimit`].
2
3use std::fmt::Write;
4use std::ops::ControlFlow;
5
6use 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)]
17pub(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
29impl 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: char| c.is_ascii_lowercase()),
81            "tag_name contained non-alphabetic chars: {tag_name:?}",
82        );
83        self.queued_tags.push(tag_name);
84    }
85
86    /// Close the most recently opened HTML tag.
87    pub(super) fn close_tag(&mut self) {
88        if let Some(tag_name) = self.unclosed_tags.pop() {
89            // Close the most recently opened tag.
90            write!(self.buf, "</{tag_name}>").unwrap()
91        }
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    }
98
99    /// Write all queued tags and add them to the `unclosed_tags` list.
100    fn flush_queue(&mut self) {
101        for tag_name in self.queued_tags.drain(..) {
102            write!(self.buf, "<{tag_name}>").unwrap();
103
104            self.unclosed_tags.push(tag_name);
105        }
106    }
107
108    /// Close all unclosed tags.
109    fn close_all_tags(&mut self) {
110        while !self.unclosed_tags.is_empty() {
111            self.close_tag();
112        }
113    }
114}
115
116#[cfg(test)]
117mod tests;