Skip to main content

pine_format/
lib.rs

1//! An opinionated formatter for Pine Script.
2//!
3//! [`format`] lexes and parses the source, then renders the AST back through a
4//! Prettier-style document layout. It preserves `//` comments (carried as lexer
5//! trivia) and normalizes spacing, indentation, and line wrapping. Formatting
6//! requires the source to parse; a lex or parse error is returned unchanged.
7//!
8//! # Example
9//!
10//! ```
11//! let src = "x=close+1\n";
12//! assert_eq!(pine_format::format(src).unwrap(), "x = close + 1\n");
13//! ```
14
15mod comments;
16mod doc;
17mod rules;
18
19#[cfg(test)]
20mod tests;
21
22use std::fmt;
23
24use pine_ast::Program;
25use pine_core::{PineVersion, VersionError};
26use pine_lexer::{Lexer, LexerError};
27use pine_parser::{Parser, ParserError};
28
29/// The target line width before a bracketed construct wraps.
30const MAX_WIDTH: usize = 100;
31
32/// Why formatting could not run: the source did not lex or parse.
33#[derive(Debug)]
34pub enum FormatError {
35    Version(VersionError),
36    Lex(LexerError),
37    Parse(ParserError),
38}
39
40impl fmt::Display for FormatError {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        match self {
43            FormatError::Version(e) => write!(f, "{e}"),
44            FormatError::Lex(e) => write!(f, "{e}"),
45            FormatError::Parse(e) => write!(f, "{e}"),
46        }
47    }
48}
49
50impl std::error::Error for FormatError {}
51
52/// Format `source`, returning canonical Pine text. Returns [`FormatError`] when
53/// the source does not lex or parse.
54pub fn format(source: &str) -> Result<String, FormatError> {
55    let version = PineVersion::detect(source)
56        .map_err(FormatError::Version)?
57        .unwrap_or(PineVersion::LATEST);
58
59    let tokens = Lexer::with_version(source, version)
60        .tokenize()
61        .map_err(FormatError::Lex)?;
62
63    let statements = Parser::new(tokens.clone())
64        .parse()
65        .map_err(FormatError::Parse)?;
66    let program = Program::new(statements);
67
68    let comments = comments::Comments::extract(&tokens);
69    let document = rules::Rules::new(comments).program(&program);
70
71    let laid_out = doc::layout(&document, MAX_WIDTH);
72    if laid_out.is_empty() {
73        return Ok(laid_out);
74    }
75
76    // Blank lines land with indentation; trim every line's trailing whitespace.
77    let mut out: String = laid_out
78        .lines()
79        .map(str::trim_end)
80        .collect::<Vec<_>>()
81        .join("\n");
82    out.push('\n');
83    Ok(out)
84}