ardent/lib.rs
1//! Opinionated formatter for [NSIS](https://nsis.sourceforge.io/) scripts.
2//!
3//! Ardent parses NSIS scripts into a concrete syntax tree, then pretty-prints them with
4//! canonical casing, consistent indentation, and normalized parameters.
5//!
6//! # Examples
7//!
8//! ```
9//! use ardent::{Formatter, FormatterOptions, EndOfLine};
10//!
11//! let formatter = Formatter::new(FormatterOptions {
12//! end_of_line: Some(EndOfLine::Lf),
13//! ..Default::default()
14//! }).unwrap();
15//!
16//! let input = "section \"Hello World\"\ndetailprint \"hi\"\nsectionend\n";
17//! let output = formatter.format(input).unwrap();
18//! assert_eq!(output, "Section \"Hello World\"\n\tDetailPrint \"hi\"\nSectionEnd\n");
19//! ```
20//!
21//! For CLI usage, see the [repository](https://github.com/idleberg/ardent).
22
23#![warn(missing_docs)]
24
25/// Canonical casing lookup table for NSIS instructions.
26pub mod canonical_casing;
27/// Canonical casing lookup table for NSIS bundled include library macros.
28pub mod canonical_includes;
29/// Canonical parameter lookup tables for NSIS instructions.
30pub mod canonical_parameters;
31/// PEG-based parser that produces a concrete syntax tree from NSIS source.
32pub mod parser;
33/// Pretty-printer that renders a CST back to formatted NSIS source.
34pub mod printer;
35/// Block-structure rules defining which keywords open, close, or continue blocks.
36pub mod rules;
37
38use parser::parse;
39use printer::print;
40
41const DEFAULT_INDENT_SIZE: usize = 2;
42const DEFAULT_PRINT_WIDTH: usize = 120;
43
44/// Line ending style for formatted output.
45#[derive(Debug, Clone)]
46pub enum EndOfLine {
47 /// Windows-style line endings (`\r\n`).
48 Crlf,
49 /// Unix-style line endings (`\n`).
50 Lf,
51}
52
53/// Configuration for the formatter.
54///
55/// # Examples
56///
57/// ```
58/// use ardent::FormatterOptions;
59///
60/// // Use defaults (tabs, indent size 2, trim empty lines)
61/// let opts = FormatterOptions::default();
62///
63/// // Use 4-space indentation
64/// let opts = FormatterOptions {
65/// use_tabs: false,
66/// indent_size: 4,
67/// ..Default::default()
68/// };
69/// ```
70#[derive(Debug, Clone)]
71pub struct FormatterOptions {
72 /// Line ending style. When `None`, the formatter auto-detects from the input.
73 pub end_of_line: Option<EndOfLine>,
74 /// Number of spaces per indent level (ignored when `use_tabs` is `true`).
75 pub indent_size: usize,
76 /// Whether to collapse consecutive blank lines and strip leading/trailing blanks.
77 pub trim_empty_lines: bool,
78 /// Whether to indent with tabs (`true`) or spaces (`false`).
79 pub use_tabs: bool,
80 /// Maximum line width before breaking with `\` continuations. `0` disables wrapping.
81 pub print_width: usize,
82 /// Whether to prefer single quotes (`true`) or double quotes (`false`).
83 pub single_quote: bool,
84}
85
86impl Default for FormatterOptions {
87 fn default() -> Self {
88 Self {
89 end_of_line: None,
90 indent_size: DEFAULT_INDENT_SIZE,
91 trim_empty_lines: true,
92 use_tabs: true,
93 print_width: DEFAULT_PRINT_WIDTH,
94 single_quote: false,
95 }
96 }
97}
98
99/// An NSIS script formatter.
100///
101/// Parses the input into a concrete syntax tree, applies canonical casing and
102/// indentation rules, then prints the result.
103///
104/// # Examples
105///
106/// ```
107/// use ardent::{Formatter, FormatterOptions, EndOfLine};
108///
109/// let formatter = Formatter::new(FormatterOptions {
110/// end_of_line: Some(EndOfLine::Lf),
111/// ..Default::default()
112/// }).unwrap();
113///
114/// // Format a script
115/// let formatted = formatter.format("detailprint \"hi\"\n").unwrap();
116/// assert_eq!(formatted, "DetailPrint \"hi\"\n");
117///
118/// // Check whether a script is already formatted
119/// let result = formatter.check("DetailPrint \"hi\"\n").unwrap();
120/// assert!(result.is_none()); // None means already formatted
121/// ```
122pub struct Formatter {
123 options: FormatterOptions,
124}
125
126impl Formatter {
127 /// Creates a new formatter with the given options.
128 ///
129 /// Returns an error if `use_tabs` is `false` and `indent_size` is zero.
130 pub fn new(options: FormatterOptions) -> Result<Self, String> {
131 if !options.use_tabs && options.indent_size == 0 {
132 return Err("The indent_size option expects a positive integer".to_string());
133 }
134 Ok(Self { options })
135 }
136
137 /// Formats an NSIS script, returning the formatted source.
138 ///
139 /// Returns an error if the input cannot be parsed.
140 pub fn format(&self, input: &str) -> Result<String, String> {
141 let nodes = parse(input)?;
142 let eol = self.detect_eol(input);
143
144 Ok(print(&nodes, &self.options, &eol))
145 }
146
147 /// Checks whether an NSIS script is already formatted.
148 ///
149 /// Returns `Ok(None)` if the input matches the formatted output, or
150 /// `Ok(Some(formatted))` with the formatted version if it differs.
151 pub fn check(&self, input: &str) -> Result<Option<String>, String> {
152 let formatted = self.format(input)?;
153 if formatted == input {
154 Ok(None)
155 } else {
156 Ok(Some(formatted))
157 }
158 }
159
160 fn detect_eol(&self, input: &str) -> String {
161 if let Some(ref eol) = self.options.end_of_line {
162 return match eol {
163 EndOfLine::Crlf => "\r\n".to_string(),
164 EndOfLine::Lf => "\n".to_string(),
165 };
166 }
167
168 if input.contains('\n') && !input.contains("\r\n") {
169 "\n".to_string()
170 } else {
171 "\r\n".to_string()
172 }
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn format_basic() {
182 let formatter = Formatter::new(FormatterOptions {
183 end_of_line: Some(EndOfLine::Lf),
184 ..Default::default()
185 })
186 .unwrap();
187
188 let result = formatter
189 .format("section \"Test\"\nDetailPrint \"hello\"\nsectionend\n")
190 .unwrap();
191 assert_eq!(
192 result,
193 "Section \"Test\"\n\tDetailPrint \"hello\"\nSectionEnd\n"
194 );
195 }
196
197 #[test]
198 fn check_returns_none_when_formatted() {
199 let formatter = Formatter::new(FormatterOptions {
200 end_of_line: Some(EndOfLine::Lf),
201 ..Default::default()
202 })
203 .unwrap();
204
205 let input = "Section \"Test\"\n\tDetailPrint \"hello\"\nSectionEnd\n";
206 assert!(formatter.check(input).unwrap().is_none());
207 }
208
209 #[test]
210 fn check_returns_some_when_unformatted() {
211 let formatter = Formatter::new(FormatterOptions {
212 end_of_line: Some(EndOfLine::Lf),
213 ..Default::default()
214 })
215 .unwrap();
216
217 let input = "section \"Test\"\nDetailPrint \"hello\"\nsectionend\n";
218 assert!(formatter.check(input).unwrap().is_some());
219 }
220
221 #[test]
222 fn spaces_indent() {
223 let formatter = Formatter::new(FormatterOptions {
224 end_of_line: Some(EndOfLine::Lf),
225 use_tabs: false,
226 indent_size: 4,
227 ..Default::default()
228 })
229 .unwrap();
230
231 let result = formatter
232 .format("section \"Test\"\nDetailPrint \"hello\"\nsectionend\n")
233 .unwrap();
234 assert_eq!(
235 result,
236 "Section \"Test\"\n DetailPrint \"hello\"\nSectionEnd\n"
237 );
238 }
239}