Skip to main content

acorn/analyzer/
vale.rs

1//! # Vale interface
2//!
3//! Provides programmtic access to the [Vale prose analyzer](https://vale.sh/) - an open-source, command-line tool that brings your editorial style guide to life.
4//!
5use crate::analyzer::error::DocumentTarget;
6use crate::io::document::DocumentQuery;
7use crate::prelude::PathBuf;
8use crate::util::constants::app::{APPLICATION, ORGANIZATION};
9use crate::util::constants::vale::{DISABLED_VALE_RULES, ENABLED_VALE_PACKAGES};
10use crate::util::{Label, SemanticVersion};
11use ariadne::{Color, ReportKind};
12use bon::Builder;
13use color_eyre::owo_colors::OwoColorize;
14use derive_more::Display;
15use serde::{Deserialize, Serialize};
16use tracing::{error, trace};
17
18/// Vale output severity
19///
20/// See <https://vale.sh/docs/keys/minalertlevel> for more information
21#[derive(Clone, Debug, Default, Display, Serialize, Deserialize)]
22#[serde(rename_all = "lowercase")]
23pub enum ValeOutputItemSeverity {
24    /// Warning
25    ///
26    /// Should strongly consider fixing
27    #[display("warning")]
28    Warning,
29    /// Error
30    ///
31    /// Should fix
32    #[display("error")]
33    Error,
34    /// Suggestion
35    ///
36    /// Should consider fixing or changing
37    #[default]
38    #[display("suggestion")]
39    Suggestion,
40}
41/// Vale installation details
42#[derive(Builder, Clone, Debug, Default, Display)]
43#[display("{:?}", version)]
44#[builder(start_fn = init)]
45pub struct Vale {
46    /// Vale version
47    pub version: Option<SemanticVersion>,
48    /// Path to `vale` binary
49    pub binary: Option<PathBuf>,
50    /// Path to Vale configuration
51    pub config: Option<ValeConfig>,
52}
53/// Vale configuration
54///
55/// See <https://vale.sh/docs/vale-ini> for more information
56#[derive(Builder, Clone, Debug, Display)]
57#[display("{:?}", path)]
58#[builder(start_fn = init)]
59pub struct ValeConfig {
60    /// Path to Vale configuration
61    #[builder(default = PathBuf::from("./.vale/.vale.ini"))]
62    pub path: PathBuf,
63    /// List of Vale packages
64    ///
65    /// See <https://vale.sh/docs/keys/packages> for more information
66    #[builder(default = Vec::<String>::new())]
67    pub packages: Vec<String>,
68    /// List of Vale vocabularies
69    ///
70    /// See <https://vale.sh/docs/keys/vocab> for more information
71    #[builder(default = Vec::<String>::new())]
72    pub vocabularies: Vec<String>,
73    /// List of Vale rules to disable
74    ///
75    /// See <https://vale.sh/docs/styles#rules> for more information
76    #[builder(default = Vec::<String>::new())]
77    pub disabled: Vec<String>,
78}
79/// Vale output
80#[derive(Clone, Debug, Serialize, Deserialize)]
81pub struct ValeOutput {
82    /// List of output items
83    pub items: Vec<ValeOutputItem>,
84}
85/// Vale output item
86///
87/// The primary purpose of this struct is to enable presenting a custom view of Vale output
88#[derive(Clone, Debug, Serialize, Deserialize)]
89#[serde(rename_all = "PascalCase")]
90pub struct ValeOutputItem {
91    pub(crate) action: ValeOutputItemAction,
92    /// Name of Vale check
93    /// ### Example
94    /// `Vale.Spelling`
95    pub check: String,
96    pub(crate) description: String,
97    /// Line number
98    pub line: u32,
99    pub(crate) link: String,
100    /// Text describing the issue and/or providing suggestions
101    pub message: String,
102    /// Severity of the issue (e.g., warning, error, suggestion)
103    pub severity: ValeOutputItemSeverity,
104    /// Span of the issue (e.g., starting and ending characters of issue location in context)
105    pub span: Vec<u32>,
106    #[serde(rename = "Match")]
107    pub(crate) word_match: String,
108}
109/// Vale output item action
110#[derive(Clone, Debug, Serialize, Deserialize)]
111#[serde(rename_all = "PascalCase")]
112pub struct ValeOutputItemAction {
113    /// Action name
114    pub(crate) name: String,
115    /// Action parameters
116    pub(crate) params: Option<Vec<String>>,
117}
118impl ValeOutput {
119    /// Parse Vale output
120    pub fn parse(output: &str, path: PathBuf) -> Vec<ValeOutputItem> {
121        let processed = preprocess_vale_output(path, output);
122        if processed != "{}" {
123            let parsed: serde_json::Result<ValeOutput> = serde_json::from_str(&processed);
124            match parsed {
125                | Ok(ValeOutput { items }) => items,
126                | Err(why) => {
127                    error!("=> {} Parse Vale output - {why}", Label::fail());
128                    vec![]
129                }
130            }
131        } else {
132            vec![]
133        }
134    }
135}
136impl Default for ValeConfig {
137    fn default() -> Self {
138        fn to_string(values: Vec<&str>) -> Vec<String> {
139            values.iter().map(|s| s.to_string()).collect()
140        }
141        let config = ValeConfig::init()
142            .packages(to_string(ENABLED_VALE_PACKAGES.to_vec()))
143            .vocabularies(to_string(vec![&ORGANIZATION.to_uppercase(), APPLICATION]))
144            .disabled(to_string(DISABLED_VALE_RULES.to_vec()))
145            .build();
146        trace!("=> {} Default - {:#?}", Label::using(), config.dimmed().cyan());
147        config
148    }
149}
150impl ValeOutputItemSeverity {
151    /// Returns colored output based on severity
152    pub fn colored(&self) -> String {
153        match self {
154            | ValeOutputItemSeverity::Warning => self.to_string().yellow().to_string(),
155            | ValeOutputItemSeverity::Error => self.to_string().red().to_string(),
156            | ValeOutputItemSeverity::Suggestion => self.to_string().blue().to_string(),
157        }
158    }
159}
160impl From<&ValeOutputItemSeverity> for Color {
161    fn from(value: &ValeOutputItemSeverity) -> Self {
162        match value {
163            | ValeOutputItemSeverity::Warning => Color::Yellow,
164            | ValeOutputItemSeverity::Error => Color::Red,
165            | ValeOutputItemSeverity::Suggestion => Color::Blue,
166        }
167    }
168}
169impl From<&ValeOutputItemSeverity> for ReportKind<'_> {
170    fn from(value: &ValeOutputItemSeverity) -> Self {
171        match value {
172            | ValeOutputItemSeverity::Warning => ReportKind::Warning,
173            | ValeOutputItemSeverity::Error => ReportKind::Error,
174            | ValeOutputItemSeverity::Suggestion => ReportKind::Custom("Suggestion", Color::Blue),
175        }
176    }
177}
178impl DocumentTarget for ValeOutputItem {
179    fn locator(&self) -> String {
180        let character = self.span.first().copied().unwrap_or_default();
181        format!("Line {}, Character {}", self.line, character)
182    }
183    fn source_text(&self) -> String {
184        match self.word_match.is_empty() {
185            | true => " ".to_string(),
186            | false => self.word_match.clone(),
187        }
188    }
189    fn query(&self) -> DocumentQuery {
190        match self.word_match.is_empty() {
191            | true => DocumentQuery::new(),
192            | false => DocumentQuery::new().with_needle(self.word_match.clone()),
193        }
194    }
195}
196/// Preprocess Vale output
197#[cfg(any(unix, target_os = "wasi", target_os = "redox"))]
198pub(crate) fn preprocess_vale_output(path: PathBuf, output: &str) -> String {
199    let input = path.display().to_string();
200    output.replace(&input, "items")
201}
202/// Preprocess Vale output
203#[cfg(windows)]
204pub(crate) fn preprocess_vale_output(path: PathBuf, output: &str) -> String {
205    let input = path.as_path().display().to_string().replace("\\", "/");
206    // First replace double backslashes with forward slashes
207    let normalized = output.replace("\\\\", "/");
208    // Then handle the extended-length path prefix and replace the full path
209    normalized.replace(&format!("//?/{}", input), "items").replace(&input, "items")
210}