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