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 bold text to the buffer formatted as [Markdown::Bold].
93    pub fn write_bold(&mut self, text: &str) -> fmt::Result {
94        writeln!(self.buf, "{}", Markdown::Bold(text))
95    }
96
97    /// Writes link to the buffer formatted as [Markdown::Link].
98    pub fn write_link(&mut self, name: &str, path: &str) -> fmt::Result {
99        writeln!(self.buf, "{}", Markdown::Link(name, path))
100    }
101
102    /// Writes a list item to the buffer indented by specified depth.
103    pub fn write_list_item(&mut self, item: &str, depth: usize) -> fmt::Result {
104        let indent = " ".repeat(depth * 2);
105        writeln!(self.buf, "{indent}- {item}")
106    }
107
108    /// Writes a link to the buffer as a list item.
109    pub fn write_link_list_item(&mut self, name: &str, path: &str, depth: usize) -> fmt::Result {
110        let link = Markdown::Link(name, path);
111        self.write_list_item(&link.as_doc()?, depth)
112    }
113
114    /// Writes a solidity code block to the buffer.
115    pub fn write_code(&mut self, code: &str) -> fmt::Result {
116        writeln!(self.buf, "{}", Markdown::CodeBlock(SOLIDITY, code))
117    }
118
119    /// Write an item section to the buffer. First write comments, the item itself as code.
120    pub fn write_section(&mut self, comments: &Comments, code: &str) -> fmt::Result {
121        self.writeln_raw(comments.as_doc()?)?;
122        self.write_code(code)?;
123        self.writeln()
124    }
125
126    /// Tries to write the table to the buffer.
127    /// Doesn't write anything if either params or comments are empty.
128    fn try_write_table<T>(
129        &mut self,
130        tag: CommentTag,
131        params: &[T],
132        comments: &Comments,
133        heading: &str,
134    ) -> fmt::Result
135    where
136        T: ParamLike,
137    {
138        let comments = comments.include_tag(tag.clone());
139
140        // There is nothing to write.
141        if params.is_empty() || comments.is_empty() {
142            return Ok(());
143        }
144
145        self.write_bold(heading)?;
146        self.writeln()?;
147
148        self.write_piped(&PARAM_TABLE_HEADERS.join("|"))?;
149        self.write_piped(&PARAM_TABLE_SEPARATOR)?;
150
151        for (index, param) in params.iter().enumerate() {
152            let param_name = param.name();
153
154            let mut comment = param_name.as_ref().and_then(|name| {
155                comments.iter().find_map(|comment| comment.match_first_word(name))
156            });
157
158            // If it's a return tag and couldn't match by first word,
159            // lookup the doc by index.
160            if comment.is_none() && matches!(tag, CommentTag::Return) {
161                comment = comments.get(index).map(|c| &*c.value);
162            }
163
164            let row = [
165                Markdown::Code(param_name.unwrap_or("<none>")).as_doc()?,
166                Markdown::Code(&param.type_name()).as_doc()?,
167                comment.unwrap_or_default().replace('\n', " "),
168            ];
169            self.write_piped(&row.join("|"))?;
170        }
171
172        self.writeln()?;
173
174        Ok(())
175    }
176
177    /// Tries to write the properties table to the buffer.
178    /// Doesn't write anything if either params or comments are empty.
179    pub fn try_write_properties_table(
180        &mut self,
181        params: &[VariableDeclaration],
182        comments: &Comments,
183    ) -> fmt::Result {
184        self.try_write_table(CommentTag::Param, params, comments, "Properties")
185    }
186
187    /// Tries to write the variant table to the buffer.
188    /// Doesn't write anything if either params or comments are empty.
189    pub fn try_write_variant_table(
190        &mut self,
191        params: &EnumDefinition,
192        comments: &Comments,
193    ) -> fmt::Result {
194        let comments = comments.include_tags(&[CommentTag::Param]);
195
196        // There is nothing to write.
197        if comments.is_empty() {
198            return Ok(());
199        }
200
201        self.write_bold("Variants")?;
202        self.writeln()?;
203
204        self.write_piped(&VARIANTS_TABLE_HEADERS.join("|"))?;
205        self.write_piped(&VARIANTS_TABLE_SEPARATOR)?;
206
207        for value in &params.values {
208            let param_name = value.as_ref().map(|v| v.name.clone());
209
210            let comment = param_name.as_ref().and_then(|name| {
211                comments.iter().find_map(|comment| comment.match_first_word(name))
212            });
213
214            let row = [
215                Markdown::Code(&param_name.unwrap_or("<none>".to_string())).as_doc()?,
216                comment.unwrap_or_default().replace('\n', " "),
217            ];
218            self.write_piped(&row.join("|"))?;
219        }
220
221        self.writeln()?;
222
223        Ok(())
224    }
225
226    /// Tries to write the parameters table to the buffer.
227    /// Doesn't write anything if either params or comments are empty.
228    pub fn try_write_events_table(
229        &mut self,
230        params: &[EventParameter],
231        comments: &Comments,
232    ) -> fmt::Result {
233        self.try_write_table(CommentTag::Param, params, comments, "Parameters")
234    }
235
236    /// Tries to write the parameters table to the buffer.
237    /// Doesn't write anything if either params or comments are empty.
238    pub fn try_write_errors_table(
239        &mut self,
240        params: &[ErrorParameter],
241        comments: &Comments,
242    ) -> fmt::Result {
243        self.try_write_table(CommentTag::Param, params, comments, "Parameters")
244    }
245
246    /// Tries to write the parameters table to the buffer.
247    /// Doesn't write anything if either params or comments are empty.
248    pub fn try_write_param_table(
249        &mut self,
250        tag: CommentTag,
251        params: &[&Parameter],
252        comments: &Comments,
253    ) -> fmt::Result {
254        let heading = match &tag {
255            CommentTag::Param => "Parameters",
256            CommentTag::Return => "Returns",
257            _ => return Err(fmt::Error),
258        };
259
260        self.try_write_table(tag, params, comments, heading)
261    }
262
263    /// Writes the deployment table to the buffer.
264    pub fn write_deployments_table(&mut self, deployments: Vec<Deployment>) -> fmt::Result {
265        self.write_bold("Deployments")?;
266        self.writeln()?;
267
268        self.write_piped(&DEPLOYMENTS_TABLE_HEADERS.join("|"))?;
269        self.write_piped(&DEPLOYMENTS_TABLE_SEPARATOR)?;
270
271        for deployment in deployments {
272            let mut network = deployment.network.ok_or(fmt::Error)?;
273            network[0..1].make_ascii_uppercase();
274
275            let row = [
276                Markdown::Bold(&network).as_doc()?,
277                Markdown::Code(&format!("{:?}", deployment.address)).as_doc()?,
278            ];
279            self.write_piped(&row.join("|"))?;
280        }
281
282        self.writeln()?;
283
284        Ok(())
285    }
286
287    /// Write content to the buffer surrounded by pipes.
288    pub fn write_piped(&mut self, content: &str) -> fmt::Result {
289        self.write_raw("|")?;
290        self.write_raw(content)?;
291        self.writeln_raw("|")
292    }
293
294    /// Finish and return underlying buffer.
295    pub fn finish(self) -> String {
296        self.buf
297    }
298}