Skip to main content

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}