diff --git a/Cargo.lock b/Cargo.lock index aed11d0845e8b4..6a758ce09b2e00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -456,7 +456,7 @@ dependencies = [ "bitflags 2.5.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.10.5", "lazy_static", "lazycell", "log", @@ -1186,6 +1186,7 @@ dependencies = [ "libz-sys", "log", "lsp-types", + "malva", "memmem", "monch", "napi_sym", @@ -1612,7 +1613,7 @@ dependencies = [ "hyper 0.14.28", "hyper 1.4.1", "hyper-util", - "itertools", + "itertools 0.10.5", "memmem", "mime", "once_cell", @@ -3852,6 +3853,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -4235,6 +4245,19 @@ dependencies = [ "libc", ] +[[package]] +name = "malva" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf982faadf37679302c3fcd8d843d29bf4bb4d323323cffb2f5217ad39578470" +dependencies = [ + "aho-corasick", + "itertools 0.13.0", + "memchr", + "raffia", + "tiny_pretty", +] + [[package]] name = "maplit" version = "1.0.2" @@ -5290,7 +5313,7 @@ checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270" dependencies = [ "bytes", "heck 0.4.1", - "itertools", + "itertools 0.10.5", "lazy_static", "log", "multimap", @@ -5311,7 +5334,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 1.0.109", @@ -5473,6 +5496,27 @@ dependencies = [ "nibble_vec", ] +[[package]] +name = "raffia" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a26ea01d105c934a90ddd1fca6a343bec14e1a24b72126ee6f364f5a5dd5ec2" +dependencies = [ + "raffia_macro", + "smallvec", +] + +[[package]] +name = "raffia_macro" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fdb50eb5bf734fa5a770680a61876a6ec77b99c1e0e52d1f18ad6ebfa85759f" +dependencies = [ + "heck 0.5.0", + "quote", + "syn 2.0.72", +] + [[package]] name = "rand" version = "0.8.5" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 0ea7d95ee41bf9..df7323e71e58b7 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -123,6 +123,7 @@ libc.workspace = true libz-sys.workspace = true log = { workspace = true, features = ["serde"] } lsp-types.workspace = true +malva = "=0.8.0" memmem.workspace = true monch.workspace = true notify.workspace = true diff --git a/cli/args/flags.rs b/cli/args/flags.rs index e28ce549b45d1a..acdcaf8a711d04 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -200,6 +200,7 @@ pub struct FmtFlags { pub prose_wrap: Option, pub no_semicolons: Option, pub watch: Option, + pub unstable_css: bool, pub unstable_yaml: bool, } @@ -2018,8 +2019,8 @@ Ignore formatting a file by adding an ignore comment at the top of the file: // prefer using ts for formatting instead of js because ts works in more scenarios .default_value("ts") .value_parser([ - "ts", "tsx", "js", "jsx", "md", "json", "jsonc", "yml", "yaml", - "ipynb", + "ts", "tsx", "js", "jsx", "md", "json", "jsonc", "css", "scss", + "sass", "less", "yml", "yaml", "ipynb", ]), ) .arg( @@ -2096,6 +2097,13 @@ Ignore formatting a file by adding an ignore comment at the top of the file: "Don't use semicolons except where necessary. Defaults to false.", ), ) + .arg( + Arg::new("unstable-css") + .long("unstable-css") + .help("Enable formatting CSS, SCSS, Sass and Less files.") + .value_parser(FalseyValueParser::new()) + .action(ArgAction::SetTrue), + ) .arg( Arg::new("unstable-yaml") .long("unstable-yaml") @@ -4163,6 +4171,7 @@ fn fmt_parse(flags: &mut Flags, matches: &mut ArgMatches) { let single_quote = matches.remove_one::("single-quote"); let prose_wrap = matches.remove_one::("prose-wrap"); let no_semicolons = matches.remove_one::("no-semicolons"); + let unstable_css = matches.get_flag("unstable-css"); let unstable_yaml = matches.get_flag("unstable-yaml"); flags.subcommand = DenoSubcommand::Fmt(FmtFlags { @@ -4175,6 +4184,7 @@ fn fmt_parse(flags: &mut Flags, matches: &mut ArgMatches) { prose_wrap, no_semicolons, watch: watch_arg_parse(matches), + unstable_css, unstable_yaml, }); } @@ -5881,6 +5891,7 @@ mod tests { single_quote: None, prose_wrap: None, no_semicolons: None, + unstable_css: false, unstable_yaml: false, watch: Default::default(), }), @@ -5905,6 +5916,7 @@ mod tests { single_quote: None, prose_wrap: None, no_semicolons: None, + unstable_css: false, unstable_yaml: false, watch: Default::default(), }), @@ -5929,6 +5941,7 @@ mod tests { single_quote: None, prose_wrap: None, no_semicolons: None, + unstable_css: false, unstable_yaml: false, watch: Default::default(), }), @@ -5953,6 +5966,7 @@ mod tests { single_quote: None, prose_wrap: None, no_semicolons: None, + unstable_css: false, unstable_yaml: false, watch: Some(Default::default()), }), @@ -5966,6 +5980,7 @@ mod tests { "fmt", "--watch", "--no-clear-screen", + "--unstable-css", "--unstable-yaml" ]); assert_eq!( @@ -5983,6 +5998,7 @@ mod tests { single_quote: None, prose_wrap: None, no_semicolons: None, + unstable_css: true, unstable_yaml: true, watch: Some(WatchFlags { hmr: false, @@ -6018,6 +6034,7 @@ mod tests { single_quote: None, prose_wrap: None, no_semicolons: None, + unstable_css: false, unstable_yaml: false, watch: Some(Default::default()), }), @@ -6042,6 +6059,7 @@ mod tests { single_quote: None, prose_wrap: None, no_semicolons: None, + unstable_css: false, unstable_yaml: false, watch: Default::default(), }), @@ -6074,6 +6092,7 @@ mod tests { single_quote: None, prose_wrap: None, no_semicolons: None, + unstable_css: false, unstable_yaml: false, watch: Some(Default::default()), }), @@ -6111,6 +6130,7 @@ mod tests { single_quote: Some(true), prose_wrap: Some("never".to_string()), no_semicolons: Some(true), + unstable_css: false, unstable_yaml: false, watch: Default::default(), }), @@ -6142,6 +6162,7 @@ mod tests { single_quote: Some(false), prose_wrap: None, no_semicolons: Some(false), + unstable_css: false, unstable_yaml: false, watch: Default::default(), }), diff --git a/cli/args/mod.rs b/cli/args/mod.rs index bd49c0c1519da5..afad0528c3e2f5 100644 --- a/cli/args/mod.rs +++ b/cli/args/mod.rs @@ -281,6 +281,7 @@ impl BenchOptions { #[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] pub struct UnstableFmtOptions { + pub css: bool, pub yaml: bool, } @@ -314,6 +315,7 @@ impl FmtOptions { Self { options: resolve_fmt_options(fmt_flags, fmt_config.options), unstable: UnstableFmtOptions { + css: unstable.css || fmt_flags.unstable_css, yaml: unstable.yaml || fmt_flags.unstable_yaml, }, files: fmt_config.files, @@ -1330,8 +1332,10 @@ impl CliOptions { } pub fn resolve_config_unstable_fmt_options(&self) -> UnstableFmtOptions { + let workspace = self.workspace(); UnstableFmtOptions { - yaml: self.workspace().has_unstable("fmt-yaml"), + css: workspace.has_unstable("fmt-css"), + yaml: workspace.has_unstable("fmt-yaml"), } } @@ -1664,6 +1668,7 @@ impl CliOptions { "sloppy-imports", "byonm", "bare-node-builtins", + "fmt-css", "fmt-yaml", ]); // add more unstable flags to the same vector holding granular flags diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 314c9ec14773a5..186c941985e45f 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -1368,6 +1368,9 @@ impl Inner { .data_for_specifier(&specifier) .map(|d| &d.member_dir.workspace); let unstable_options = UnstableFmtOptions { + css: maybe_workspace + .map(|w| w.has_unstable("fmt-css")) + .unwrap_or(false), yaml: maybe_workspace .map(|w| w.has_unstable("fmt-yaml")) .unwrap_or(false), diff --git a/cli/tools/fmt.rs b/cli/tools/fmt.rs index c4eebddf7a43c7..348e2d862a10bf 100644 --- a/cli/tools/fmt.rs +++ b/cli/tools/fmt.rs @@ -244,6 +244,10 @@ fn format_markdown( | "typescript" | "json" | "jsonc" + | "css" + | "scss" + | "sass" + | "less" | "yml" | "yaml" ) { @@ -263,6 +267,13 @@ fn format_markdown( json_config.line_width = line_width; dprint_plugin_json::format_text(&fake_filename, text, &json_config) } + "css" | "scss" | "sass" | "less" => { + if unstable_options.css { + format_css(&fake_filename, text, fmt_options) + } else { + Ok(None) + } + } "yml" | "yaml" => { if unstable_options.yaml { pretty_yaml::format_text( @@ -305,6 +316,20 @@ pub fn format_json( dprint_plugin_json::format_text(file_path, file_text, &config) } +pub fn format_css( + file_path: &Path, + file_text: &str, + fmt_options: &FmtOptionsConfig, +) -> Result, AnyError> { + malva::format_text( + file_text, + malva::detect_syntax(file_path).unwrap_or(malva::Syntax::Css), + &get_resolved_malva_config(fmt_options), + ) + .map(Some) + .map_err(AnyError::from) +} + /// Formats a single TS, TSX, JS, JSX, JSONC, JSON, MD, or IPYNB file. pub fn format_file( file_path: &Path, @@ -319,6 +344,13 @@ pub fn format_file( format_markdown(file_text, fmt_options, unstable_options) } "json" | "jsonc" => format_json(file_path, file_text, fmt_options), + "css" | "scss" | "sass" | "less" => { + if unstable_options.css { + format_css(file_path, file_text, fmt_options) + } else { + Ok(None) + } + } "yml" | "yaml" => { if unstable_options.yaml { pretty_yaml::format_text( @@ -737,6 +769,58 @@ fn get_resolved_json_config( builder.build() } +fn get_resolved_malva_config( + options: &FmtOptionsConfig, +) -> malva::config::FormatOptions { + use malva::config::*; + + let layout_options = LayoutOptions { + print_width: options.line_width.unwrap_or(80) as usize, + use_tabs: options.use_tabs.unwrap_or_default(), + indent_width: options.indent_width.unwrap_or(2) as usize, + line_break: LineBreak::Lf, + }; + + let language_options = LanguageOptions { + hex_case: HexCase::Lower, + hex_color_length: None, + quotes: if let Some(true) = options.single_quote { + Quotes::PreferSingle + } else { + Quotes::PreferDouble + }, + operator_linebreak: OperatorLineBreak::Before, + block_selector_linebreak: BlockSelectorLineBreak::Consistent, + omit_number_leading_zero: false, + trailing_comma: true, + format_comments: false, + linebreak_in_pseudo_parens: true, + declaration_order: None, + single_line_block_threshold: None, + keyframe_selector_notation: None, + attr_value_quotes: AttrValueQuotes::Always, + prefer_single_line: false, + selectors_prefer_single_line: None, + function_args_prefer_single_line: None, + sass_content_at_rule_prefer_single_line: None, + sass_include_at_rule_prefer_single_line: None, + sass_map_prefer_single_line: None, + sass_module_config_prefer_single_line: None, + sass_params_prefer_single_line: None, + less_import_options_prefer_single_line: None, + less_mixin_args_prefer_single_line: None, + less_mixin_params_prefer_single_line: None, + top_level_declarations_prefer_single_line: None, + selector_override_comment_directive: "deno-fmt-selector-override".into(), + ignore_comment_directive: "deno-fmt-ignore".into(), + }; + + FormatOptions { + layout: layout_options, + language: language_options, + } +} + fn get_resolved_yaml_config( options: &FmtOptionsConfig, ) -> pretty_yaml::config::FormatOptions { @@ -864,6 +948,10 @@ fn is_supported_ext_fmt(path: &Path) -> bool { | "mts" | "json" | "jsonc" + | "css" + | "scss" + | "sass" + | "less" | "md" | "mkd" | "mkdn" @@ -906,6 +994,14 @@ mod test { assert!(is_supported_ext_fmt(Path::new("foo.JSONC"))); assert!(is_supported_ext_fmt(Path::new("foo.json"))); assert!(is_supported_ext_fmt(Path::new("foo.JsON"))); + assert!(is_supported_ext_fmt(Path::new("foo.css"))); + assert!(is_supported_ext_fmt(Path::new("foo.Css"))); + assert!(is_supported_ext_fmt(Path::new("foo.scss"))); + assert!(is_supported_ext_fmt(Path::new("foo.SCSS"))); + assert!(is_supported_ext_fmt(Path::new("foo.sass"))); + assert!(is_supported_ext_fmt(Path::new("foo.Sass"))); + assert!(is_supported_ext_fmt(Path::new("foo.less"))); + assert!(is_supported_ext_fmt(Path::new("foo.LeSS"))); assert!(is_supported_ext_fmt(Path::new("foo.yml"))); assert!(is_supported_ext_fmt(Path::new("foo.Yml"))); assert!(is_supported_ext_fmt(Path::new("foo.yaml"))); diff --git a/tests/integration/fmt_tests.rs b/tests/integration/fmt_tests.rs index dab2b2ce45c8b5..7a37ec2fce792d 100644 --- a/tests/integration/fmt_tests.rs +++ b/tests/integration/fmt_tests.rs @@ -31,6 +31,12 @@ fn fmt_test() { let badly_formatted_json = t.path().join("badly_formatted.json"); badly_formatted_original_json.copy(&badly_formatted_json); + let fixed_css = testdata_fmt_dir.join("badly_formatted_fixed.css"); + let badly_formatted_original_css = + testdata_fmt_dir.join("badly_formatted.css"); + let badly_formatted_css = t.path().join("badly_formatted.css"); + badly_formatted_original_css.copy(&badly_formatted_css); + let fixed_ipynb = testdata_fmt_dir.join("badly_formatted_fixed.ipynb"); let badly_formatted_original_ipynb = testdata_fmt_dir.join("badly_formatted.ipynb"); @@ -49,12 +55,13 @@ fn fmt_test() { .current_dir(&testdata_fmt_dir) .args_vec(vec![ "fmt".to_string(), + "--unstable-css".to_string(), "--unstable-yaml".to_string(), format!( - "--ignore={badly_formatted_js},{badly_formatted_md},{badly_formatted_json},{badly_formatted_yaml},{badly_formatted_ipynb}", + "--ignore={badly_formatted_js},{badly_formatted_md},{badly_formatted_json},{badly_formatted_css},{badly_formatted_yaml},{badly_formatted_ipynb}", ), format!( - "--check {badly_formatted_js} {badly_formatted_md} {badly_formatted_json} {badly_formatted_yaml} {badly_formatted_ipynb}", + "--check {badly_formatted_js} {badly_formatted_md} {badly_formatted_json} {badly_formatted_css} {badly_formatted_yaml} {badly_formatted_ipynb}", ), ]) .run(); @@ -70,10 +77,12 @@ fn fmt_test() { .args_vec(vec![ "fmt".to_string(), "--check".to_string(), + "--unstable-css".to_string(), "--unstable-yaml".to_string(), badly_formatted_js.to_string(), badly_formatted_md.to_string(), badly_formatted_json.to_string(), + badly_formatted_css.to_string(), badly_formatted_yaml.to_string(), badly_formatted_ipynb.to_string(), ]) @@ -88,10 +97,12 @@ fn fmt_test() { .current_dir(&testdata_fmt_dir) .args_vec(vec![ "fmt".to_string(), + "--unstable-css".to_string(), "--unstable-yaml".to_string(), badly_formatted_js.to_string(), badly_formatted_md.to_string(), badly_formatted_json.to_string(), + badly_formatted_css.to_string(), badly_formatted_yaml.to_string(), badly_formatted_ipynb.to_string(), ]) @@ -103,16 +114,19 @@ fn fmt_test() { let expected_js = fixed_js.read_to_string(); let expected_md = fixed_md.read_to_string(); let expected_json = fixed_json.read_to_string(); + let expected_css = fixed_css.read_to_string(); let expected_yaml = fixed_yaml.read_to_string(); let expected_ipynb = fixed_ipynb.read_to_string(); let actual_js = badly_formatted_js.read_to_string(); let actual_md = badly_formatted_md.read_to_string(); let actual_json = badly_formatted_json.read_to_string(); + let actual_css = badly_formatted_css.read_to_string(); let actual_yaml = badly_formatted_yaml.read_to_string(); let actual_ipynb = badly_formatted_ipynb.read_to_string(); assert_eq!(expected_js, actual_js); assert_eq!(expected_md, actual_md); assert_eq!(expected_json, actual_json); + assert_eq!(expected_css, actual_css); assert_eq!(expected_yaml, actual_yaml); assert_eq!(expected_ipynb, actual_ipynb); } diff --git a/tests/specs/fmt/unstable_css/__test__.jsonc b/tests/specs/fmt/unstable_css/__test__.jsonc new file mode 100644 index 00000000000000..32259f3ae4e8c6 --- /dev/null +++ b/tests/specs/fmt/unstable_css/__test__.jsonc @@ -0,0 +1,25 @@ +{ + "tempDir": true, + "tests": { + "nothing": { + "args": "fmt", + "output": "Checked 1 file\n" + }, + "flag": { + "args": "fmt --unstable-css", + "output": "[WILDLINE]badly_formatted.css\nChecked 1 file\n" + }, + "config_file": { + "steps": [{ + "args": [ + "eval", + "Deno.writeTextFile('deno.json', '{\\n \"unstable\": [\"fmt-css\"]\\n}\\n')" + ], + "output": "[WILDCARD]" + }, { + "args": "fmt", + "output": "[WILDLINE]badly_formatted.css\nChecked 2 files\n" + }] + } + } +} diff --git a/tests/specs/fmt/unstable_css/badly_formatted.css b/tests/specs/fmt/unstable_css/badly_formatted.css new file mode 100644 index 00000000000000..e57adb7969ae61 --- /dev/null +++ b/tests/specs/fmt/unstable_css/badly_formatted.css @@ -0,0 +1 @@ +#app>.btn{ color : #000 } diff --git a/tests/testdata/fmt/badly_formatted.css b/tests/testdata/fmt/badly_formatted.css new file mode 100644 index 00000000000000..bfe18b82183d09 --- /dev/null +++ b/tests/testdata/fmt/badly_formatted.css @@ -0,0 +1,2 @@ +#app>.btn{ color : #000 } + diff --git a/tests/testdata/fmt/badly_formatted.md b/tests/testdata/fmt/badly_formatted.md index 05a4b2f97cf78f..29d73b365da35f 100644 --- a/tests/testdata/fmt/badly_formatted.md +++ b/tests/testdata/fmt/badly_formatted.md @@ -32,7 +32,7 @@ function foo(): number { { // Comment in JSON "key": "value", - "key2": + "key2": "value2", } @@ -49,3 +49,7 @@ function foo(): number { - item1 - item2 ``` + +```css +#app>.btn{ color : #000 } +``` diff --git a/tests/testdata/fmt/badly_formatted_fixed.css b/tests/testdata/fmt/badly_formatted_fixed.css new file mode 100644 index 00000000000000..1653551f472f19 --- /dev/null +++ b/tests/testdata/fmt/badly_formatted_fixed.css @@ -0,0 +1,3 @@ +#app > .btn { + color: #000; +} diff --git a/tests/testdata/fmt/badly_formatted_fixed.md b/tests/testdata/fmt/badly_formatted_fixed.md index 7a7d1913b7ef9f..db2afc809458c9 100644 --- a/tests/testdata/fmt/badly_formatted_fixed.md +++ b/tests/testdata/fmt/badly_formatted_fixed.md @@ -40,3 +40,9 @@ function foo(): number { - item1 - item2 ``` + +```css +#app > .btn { + color: #000; +} +```