diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index f8087bd..260f5ef 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -62,6 +62,10 @@ jobs: otp-version: "26" gleam-version: "1.0.0" elixir-version: "1.16.1" + # Used go shfmt + - uses: actions/setup-go@v5 + with: + go-version: "stable" - run: rustup toolchain install stable --profile minimal - run: rustup component add rustfmt clippy @@ -99,4 +103,10 @@ jobs: - name: Install stylua run: cargo install stylua + - name: Install shfmt + run: go install mvdan.cc/sh/v3/cmd/shfmt@latest + + - name: Validate shfmt + run: shfmt --version + - run: cargo test diff --git a/schemas/v0.0.0/mdsf.schema.json b/schemas/v0.0.0/mdsf.schema.json index 5957806..19b61d9 100644 --- a/schemas/v0.0.0/mdsf.schema.json +++ b/schemas/v0.0.0/mdsf.schema.json @@ -3,6 +3,17 @@ "title": "MdsfConfig", "type": "object", "properties": { + "bash": { + "default": { + "enabled": true, + "formatter": "shfmt" + }, + "allOf": [ + { + "$ref": "#/definitions/Bash" + } + ] + }, "css": { "default": { "enabled": true, @@ -170,6 +181,27 @@ } }, "definitions": { + "Bash": { + "type": "object", + "properties": { + "enabled": { + "default": true, + "type": "boolean" + }, + "formatter": { + "default": "shfmt", + "allOf": [ + { + "$ref": "#/definitions/BashFormatter" + } + ] + } + } + }, + "BashFormatter": { + "type": "string", + "enum": ["shfmt"] + }, "Css": { "type": "object", "properties": { diff --git a/src/config.rs b/src/config.rs index 2025637..b62beab 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,8 @@ use schemars::JsonSchema; use crate::languages::{ - css::Css, elixir::Elixir, gleam::Gleam, html::Html, javascript::JavaScript, json::Json, - lua::Lua, markdown::Markdown, nim::Nim, python::Python, rust::Rust, toml::Toml, + bash::Bash, css::Css, elixir::Elixir, gleam::Gleam, html::Html, javascript::JavaScript, + json::Json, lua::Lua, markdown::Markdown, nim::Nim, python::Python, rust::Rust, toml::Toml, typescript::TypeScript, yaml::Yaml, zig::Zig, }; @@ -12,6 +12,9 @@ pub struct MdsfConfig { #[serde(rename = "$schema", default = "default_schema_location")] pub schema: String, + #[serde(default)] + pub bash: Bash, + #[serde(default)] pub css: Css, @@ -63,6 +66,7 @@ impl Default for MdsfConfig { fn default() -> Self { Self { schema: default_schema_location(), + bash: Bash::default(), css: Css::default(), elixir: Elixir::default(), gleam: Gleam::default(), diff --git a/src/formatters/mod.rs b/src/formatters/mod.rs index 4190ef1..fd510ac 100644 --- a/src/formatters/mod.rs +++ b/src/formatters/mod.rs @@ -17,6 +17,7 @@ pub mod nimpretty; pub mod prettier; pub mod ruff; pub mod rustfmt; +pub mod shfmt; pub mod stylua; pub mod taplo; pub mod zigfmt; @@ -55,6 +56,7 @@ pub fn format_snippet(config: &MdsfConfig, language: &Language, code: &str) -> S let snippet_path = snippet.path(); if let Ok(Some(formatted_code)) = match language { + Language::Bash => config.bash.format(snippet_path), Language::Css => config.css.format(snippet_path), Language::Elixir => config.elixir.format(snippet_path), Language::Gleam => config.gleam.format(snippet_path), diff --git a/src/formatters/shfmt.rs b/src/formatters/shfmt.rs new file mode 100644 index 0000000..c2b6635 --- /dev/null +++ b/src/formatters/shfmt.rs @@ -0,0 +1,93 @@ +use super::{execute_command, read_snippet}; + +#[inline] +pub fn format_using_shfmt(file_path: &std::path::Path) -> std::io::Result> { + let mut cmd = std::process::Command::new("shfmt"); + + // Incase the use hasn't installed biome + cmd.arg("--write").arg(file_path); + + if execute_command(&mut cmd)? { + return read_snippet(file_path).map(Some); + } + + Ok(None) +} + +#[cfg(test)] +mod test_shfmt { + use crate::{ + formatters::{setup_snippet, shfmt::format_using_shfmt}, + languages::Language, + }; + + #[test] + fn it_should_format_sh() { + let input = " + +#!/bin/sh + + add () { + echo \"$1\" + \"$2\" + } + + + + + + + + +"; + let expected_output = "#!/bin/sh + +add() { +\techo \"$1\" + \"$2\" +} +"; + + let snippet = setup_snippet(input, Language::Bash.to_file_ext()) + .expect("it to create a snippet file"); + + let output = format_using_shfmt(snippet.path()) + .expect("it to be succesful") + .expect("it to be some"); + + assert_eq!(expected_output, output); + } + + #[test] + fn it_should_format_bash() { + let input = " + +#!/bin/bash + + add () { + echo \"$1\" + \"$2\" + } + + + + + + + + +"; + let expected_output = "#!/bin/bash + +add() { +\techo \"$1\" + \"$2\" +} +"; + + let snippet = setup_snippet(input, Language::Bash.to_file_ext()) + .expect("it to create a snippet file"); + + let output = format_using_shfmt(snippet.path()) + .expect("it to be succesful") + .expect("it to be some"); + + assert_eq!(expected_output, output); + } +} diff --git a/src/languages/bash.rs b/src/languages/bash.rs new file mode 100644 index 0000000..6958dc4 --- /dev/null +++ b/src/languages/bash.rs @@ -0,0 +1,43 @@ +use schemars::JsonSchema; + +use crate::{config::default_enabled, formatters::shfmt::format_using_shfmt}; + +use super::LanguageFormatter; + +#[derive(Debug, Default, serde::Serialize, serde::Deserialize, JsonSchema)] +pub enum BashFormatter { + #[default] + #[serde(rename = "shfmt")] + Shfmt, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize, JsonSchema)] +pub struct Bash { + #[serde(default = "default_enabled")] + pub enabled: bool, + #[serde(default)] + pub formatter: BashFormatter, +} + +impl Default for Bash { + #[inline] + fn default() -> Self { + Self { + enabled: true, + formatter: BashFormatter::default(), + } + } +} + +impl LanguageFormatter for Bash { + #[inline] + fn format(&self, snippet_path: &std::path::Path) -> std::io::Result> { + if !self.enabled { + return Ok(None); + } + + match self.formatter { + BashFormatter::Shfmt => format_using_shfmt(snippet_path), + } + } +} diff --git a/src/languages/mod.rs b/src/languages/mod.rs index 9f4936a..03a8b33 100644 --- a/src/languages/mod.rs +++ b/src/languages/mod.rs @@ -1,4 +1,5 @@ pub enum Language { + Bash, Css, Elixir, Gleam, @@ -16,6 +17,7 @@ pub enum Language { Zig, } +pub mod bash; pub mod css; pub mod elixir; pub mod gleam; @@ -40,6 +42,7 @@ impl Language { #[inline] pub fn maybe_from_str(input: &str) -> Option { match input { + "sh" | "shell" | "bash" => Some(Self::Bash), "css" | "scss" => Some(Self::Css), "elixir" => Some(Self::Elixir), "gleam" => Some(Self::Gleam), @@ -62,6 +65,7 @@ impl Language { #[inline] pub const fn to_file_ext(&self) -> &'static str { match self { + Self::Bash => ".sh", // NOTE: since scss is a superset of css we might as well support both at the same time Self::Css => ".scss", Self::Elixir => ".ex", diff --git a/src/lib.rs b/src/lib.rs index bf96e14..eaa25c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -104,9 +104,13 @@ pub fn format_file(config: &MdsfConfig, path: &std::path::Path) -> Result<(), Md s.finalize(&mut output).map_err(MdsfError::from)?; } - if config.markdown.enabled && !output.is_empty() { - output = format_snippet(config, &Language::Markdown, &output); - modified = true; + if config.markdown.enabled { + if !output.is_empty() { + output = format_snippet(config, &Language::Markdown, &output); + modified = true; + } + } else { + output.push('\n'); } let duration = time.elapsed(); diff --git a/tests/bash.md b/tests/bash.md new file mode 100644 index 0000000..2ab2861 --- /dev/null +++ b/tests/bash.md @@ -0,0 +1,18 @@ +```sh + +#!/bin/bash + + add () { + echo "$1" + "$2" + } + + + + + + + + + + +``` diff --git a/tests/sh.md b/tests/sh.md new file mode 100644 index 0000000..b0e68b4 --- /dev/null +++ b/tests/sh.md @@ -0,0 +1,18 @@ +```sh + +#!/bin/sh + + add () { + echo "$1" + "$2" + } + + + + + + + + + + +```