forge_doc/writer/
buf_writer.rs

1use crate::{AsDoc, CommentTag, Comments, Deployment, Markdown, writer::traits::ParamLike};
2use itertools::Itertools;
3use solang_parser::pt::{
4    EnumDefinition, ErrorParameter, EventParameter, Parameter, VariableDeclaration,
5};
6use std::{
7    fmt::{self, Display, Write},
8    sync::LazyLock,
9};
10
11/// Solidity language name.
12const SOLIDITY: &str = "solidity";
13
14/// Headers and separator for rendering parameter table.
15const PARAM_TABLE_HEADERS: &[&str] = &["Name", "Type", "Description"];
16static PARAM_TABLE_SEPARATOR: LazyLock<String> =
17    LazyLock::new(|| PARAM_TABLE_HEADERS.iter().map(|h| "-".repeat(h.len())).join("|"));
18
19/// Headers and separator for rendering the deployments table.
20const DEPLOYMENTS_TABLE_HEADERS: &[&str] = &["Network", "Address"];
21static DEPLOYMENTS_TABLE_SEPARATOR: LazyLock<String> =
22    LazyLock::new(|| DEPLOYMENTS_TABLE_HEADERS.iter().map(|h| "-".repeat(h.len())).join("|"));
23
24/// Headers and separator for rendering the variants table.
25const VARIANTS_TABLE_HEADERS: &[&str] = &["Name", "Description"];
26static VARIANTS_TABLE_SEPARATOR: LazyLock<String> =
27    LazyLock::new(|| VARIANTS_TABLE_HEADERS.iter().map(|h| "-".repeat(h.len())).join("|"));
28
29/// The buffered writer.
30/// Writes various display items into the internal buffer.
31#[derive(Debug, Default)]
32pub struct BufWriter {
33    buf: String,
34}
35
36impl BufWriter {
37    /// Create new instance of [BufWriter] from [ToString].
38    pub fn new(content: impl ToString) -> Self {
39        Self { buf: content.to_string() }
40    }
41
42    /// Returns true if the buffer is empty.
43    pub fn is_empty(&self) -> bool {
44        self.buf.is_empty()
45    }
46
47    /// Write [AsDoc] implementation to the buffer.
48    pub fn write_doc<T: AsDoc>(&mut self, doc: &T) -> fmt::Result {
49        write!(self.buf, "{}", doc.as_doc()?)
50    }
51
52    /// Write [AsDoc] implementation to the buffer with newline.
53    pub fn writeln_doc<T: AsDoc>(&mut self, doc: &T) -> fmt::Result {
54        writeln!(self.buf, "{}", doc.as_doc()?)
55    }
56
57    /// Writes raw content to the buffer.
58    pub fn write_raw<T: Display>(&mut self, content: T) -> fmt::Result {
59        write!(self.buf, "{content}")
60    }
61
62    /// Writes raw content to the buffer with newline.
63    pub fn writeln_raw<T: Display>(&mut self, content: T) -> fmt::Result {
64        writeln!(self.buf, "{content}")
65    }
66
67    /// Writes newline to the buffer.
68    pub fn writeln(&mut self) -> fmt::Result {
69        writeln!(self.buf)
70    }
71
72    /// Writes a title to the buffer formatted as [Markdown::H1].
73    pub fn write_title(&mut self, title: &str) -> fmt::Result {
74        writeln!(self.buf, "{}", Markdown::H1(title))
75    }
76
77    /// Writes a subtitle to the bugger formatted as [Markdown::H2].
78    pub fn write_subtitle(&mut self, subtitle: &str) -> fmt::Result {
79        writeln!(self.buf, "{}", Markdown::H2(subtitle))
80    }
81
82    /// Writes heading to the buffer formatted as [Markdown::H3].
83    pub fn write_heading(&mut self, heading: &str) -> fmt::Result {
84        writeln!(self.buf, "{}", Markdown::H3(heading))
85    }
86
87    /// Writes text in italics to the buffer formatted as [Markdown::Italic].
88    pub fn write_italic(&mut self, text: &str) -> fmt::Result {
89        writeln!(self.buf, "{}", Markdown::Italic(text))
90    }
91
92    /// Writes dev content to the buffer, handling markdown lists properly.
93    /// If the content contains markdown lists, it formats them correctly.
94    /// Otherwise, it writes the content in italics.
95    pub fn write_dev_content(&mut self, text: &str) -> fmt::Result {
96        for line in text.lines() {
97            writeln!(self.buf, "{line}")?;
98        }
99
100        Ok(())
101    }
102
103    /// Writes bold text to the buffer formatted as [Markdown::Bold].
104    pub fn write_bold(&mut self, text: &str) -> fmt::Result {
105        writeln!(self.buf, "{}", Markdown::Bold(text))
106    }
107
108    /// Writes link to the buffer formatted as [Markdown::Link].
109    pub fn write_link(&mut self, name: &str, path: &str) -> fmt::Result {
110        writeln!(self.buf, "{}", Markdown::Link(name, path))
111    }
112
113    /// Writes a list item to the buffer indented by specified depth.
114    pub fn write_list_item(&mut self, item: &str, depth: usize) -> fmt::Result {
115        let indent = " ".repeat(depth * 2);
116        writeln!(self.buf, "{indent}- {item}")
117    }
118
119    /// Writes a link to the buffer as a list item.
120    pub fn write_link_list_item(&mut self, name: &str, path: &str, depth: usize) -> fmt::Result {
121        let link = Markdown::Link(name, path);
122        self.write_list_item(&link.as_doc()?, depth)
123    }
124
125    /// Writes a solidity code block to the buffer.
126    pub fn write_code(&mut self, code: &str) -> fmt::Result {
127        writeln!(self.buf, "{}", Markdown::CodeBlock(SOLIDITY, code))
128    }
129
130    /// Write an item section to the buffer. First write comments, the item itself as code.
131    pub fn write_section(&mut self, comments: &Comments, code: &str) -> fmt::Result {
132        self.writeln_raw(comments.as_doc()?)?;
133        self.write_code(code)?;
134        self.writeln()
135    }
136
137    /// Tries to write the table to the buffer.
138    /// Doesn't write anything if either params or comments are empty.
139    fn try_write_table<T>(
140        &mut self,
141        tag: CommentTag,
142        params: &[T],
143        comments: &Comments,
144        heading: &str,
145    ) -> fmt::Result
146    where
147        T: ParamLike,
148    {
149        let comments = comments.include_tag(tag.clone());
150
151        // There is nothing to write.
152        if params.is_empty() || comments.is_empty() {
153            return Ok(());
154        }
155
156        self.write_bold(heading)?;
157        self.writeln()?;
158
159        self.write_piped(&PARAM_TABLE_HEADERS.join("|"))?;
160        self.write_piped(&PARAM_TABLE_SEPARATOR)?;
161
162        for (index, param) in params.iter().enumerate() {
163            let param_name = param.name();
164
165            let mut comment = param_name.as_ref().and_then(|name| {
166                comments.iter().find_map(|comment| comment.match_first_word(name))
167            });
168
169            // If it's a return tag and couldn't match by first word,
170            // lookup the doc by index.
171            if comment.is_none() && matches!(tag, CommentTag::Return) {
172                comment = comments.get(index).map(|c| &*c.value);
173            }
174
175            let row = [
176                Markdown::Code(param_name.unwrap_or("<none>")).as_doc()?,
177                Markdown::Code(&param.type_name()).as_doc()?,
178                comment.unwrap_or_default().replace('\n', " "),
179            ];
180            self.write_piped(&row.join("|"))?;
181        }
182
183        self.writeln()?;
184
185        Ok(())
186    }
187
188    /// Tries to write the properties table to the buffer.
189    /// Doesn't write anything if either params or comments are empty.
190    pub fn try_write_properties_table(
191        &mut self,
192        params: &[VariableDeclaration],
193        comments: &Comments,
194    ) -> fmt::Result {
195        self.try_write_table(CommentTag::Param, params, comments, "Properties")
196    }
197
198    /// Tries to write the variant table to the buffer.
199    /// Doesn't write anything if either params or comments are empty.
200    pub fn try_write_variant_table(
201        &mut self,
202        params: &EnumDefinition,
203        comments: &Comments,
204    ) -> fmt::Result {
205        let comments = comments.include_tags(&[CommentTag::Param]);
206
207        // There is nothing to write.
208        if comments.is_empty() {
209            return Ok(());
210        }
211
212        self.write_bold("Variants")?;
213        self.writeln()?;
214
215        self.write_piped(&VARIANTS_TABLE_HEADERS.join("|"))?;
216        self.write_piped(&VARIANTS_TABLE_SEPARATOR)?;
217
218        for value in &params.values {
219            let param_name = value.as_ref().map(|v| v.name.clone());
220
221            let comment = param_name.as_ref().and_then(|name| {
222                comments.iter().find_map(|comment| comment.match_first_word(name))
223            });
224
225            let row = [
226                Markdown::Code(&param_name.unwrap_or("<none>".to_string())).as_doc()?,
227                comment.unwrap_or_default().replace('\n', " "),
228            ];
229            self.write_piped(&row.join("|"))?;
230        }
231
232        self.writeln()?;
233
234        Ok(())
235    }
236
237    /// Tries to write the parameters table to the buffer.
238    /// Doesn't write anything if either params or comments are empty.
239    pub fn try_write_events_table(
240        &mut self,
241        params: &[EventParameter],
242        comments: &Comments,
243    ) -> fmt::Result {
244        self.try_write_table(CommentTag::Param, params, comments, "Parameters")
245    }
246
247    /// Tries to write the parameters table to the buffer.
248    /// Doesn't write anything if either params or comments are empty.
249    pub fn try_write_errors_table(
250        &mut self,
251        params: &[ErrorParameter],
252        comments: &Comments,
253    ) -> fmt::Result {
254        self.try_write_table(CommentTag::Param, params, comments, "Parameters")
255    }
256
257    /// Tries to write the parameters table to the buffer.
258    /// Doesn't write anything if either params or comments are empty.
259    pub fn try_write_param_table(
260        &mut self,
261        tag: CommentTag,
262        params: &[&Parameter],
263        comments: &Comments,
264    ) -> fmt::Result {
265        let heading = match &tag {
266            CommentTag::Param => "Parameters",
267            CommentTag::Return => "Returns",
268            _ => return Err(fmt::Error),
269        };
270
271        self.try_write_table(tag, params, comments, heading)
272    }
273
274    /// Writes the deployment table to the buffer.
275    pub fn write_deployments_table(&mut self, deployments: Vec<Deployment>) -> fmt::Result {
276        self.write_bold("Deployments")?;
277        self.writeln()?;
278
279        self.write_piped(&DEPLOYMENTS_TABLE_HEADERS.join("|"))?;
280        self.write_piped(&DEPLOYMENTS_TABLE_SEPARATOR)?;
281
282        for deployment in deployments {
283            let mut network = deployment.network.ok_or(fmt::Error)?;
284            network[0..1].make_ascii_uppercase();
285
286            let row = [
287                Markdown::Bold(&network).as_doc()?,
288                Markdown::Code(&format!("{:?}", deployment.address)).as_doc()?,
289            ];
290            self.write_piped(&row.join("|"))?;
291        }
292
293        self.writeln()?;
294
295        Ok(())
296    }
297
298    /// Write content to the buffer surrounded by pipes.
299    pub fn write_piped(&mut self, content: &str) -> fmt::Result {
300        self.write_raw("|")?;
301        self.write_raw(content)?;
302        self.writeln_raw("|")
303    }
304
305    /// Finish and return underlying buffer.
306    pub fn finish(self) -> String {
307        self.buf
308    }
309}