forge_doc/writer/
buf_writer.rs

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