Skip to content

Commit e01d8b4

Browse files
committed
feat: remove formatting, only parse and lint for style attributes
1 parent c8d9ca3 commit e01d8b4

25 files changed

Lines changed: 218 additions & 496 deletions

.changeset/feat-html-style-attribute-css.md

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,4 @@
22
"@biomejs/biome": patch
33
---
44

5-
The HTML formatter now formats the value of a `style` attribute as CSS, the same way it already formatted a `<style>` element. The declarations stay on the tag's line while they fit there, and break onto their own lines once they do not:
6-
7-
```diff
8-
- <div style="color:#fFf; background:red"></div>
9-
+ <div style="color: #fff; background: red"></div>
10-
```
5+
HTML `style` attribute values are now parsed as CSS. All Biome CSS lint rules are applied to the `style` attributes.

crates/biome_cli/tests/cases/html_style_attribute.rs

Lines changed: 34 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,79 +5,86 @@ use biome_fs::MemoryFileSystem;
55
use bpaf::Args;
66
use camino::Utf8Path;
77

8-
const CONFIG: &str = r#"{ "html": { "formatter": { "enabled": true } } }"#;
8+
const LINTER_CONFIG: &str =
9+
r#"{ "html": { "linter": { "enabled": true }, "experimentalFullSupportEnabled": true } }"#;
910

10-
/// The value of a `style` attribute is CSS, so it is formatted as a list of
11-
/// declarations. It stays on the tag's line while it fits, and breaks onto its
12-
/// own lines once it does not.
1311
#[test]
14-
fn formats_style_attribute_as_css() {
12+
fn lints_dom_style_attributes_as_css() {
1513
let mut console = BufferConsole::default();
1614
let fs = MemoryFileSystem::default();
1715

1816
let config = Utf8Path::new("biome.json");
19-
fs.insert(config.into(), CONFIG.as_bytes());
17+
fs.insert(config.into(), LINTER_CONFIG.as_bytes());
2018

21-
let file = Utf8Path::new("index.html");
19+
let html_file = Utf8Path::new("index.html");
2220
fs.insert(
23-
file.into(),
24-
r#"<div style="color:#fFf; background:red"></div>
25-
<div style='color:red'></div>
26-
<div style="all: initial;display:block;contain:content;text-align:center;max-width:500px;margin:0 auto"></div>
21+
html_file.into(),
22+
r#"<div style="colr: blue"></div>
23+
<my-element style="colr: blue"></my-element>
24+
"#
25+
.as_bytes(),
26+
);
27+
28+
let vue_file = Utf8Path::new("component.vue");
29+
fs.insert(
30+
vue_file.into(),
31+
r#"<template><Button style="colr: blue" /></template>
2732
"#
2833
.as_bytes(),
2934
);
3035

3136
let (fs, result) = run_cli(
3237
fs,
3338
&mut console,
34-
Args::from(["format", "--write", file.as_str()].as_slice()),
39+
Args::from(["lint", html_file.as_str(), vue_file.as_str()].as_slice()),
3540
);
3641

37-
assert!(result.is_ok(), "run_cli returned {result:?}");
42+
assert!(result.is_err(), "run_cli returned {result:?}");
3843

3944
assert_cli_snapshot(SnapshotPayload::new(
4045
module_path!(),
41-
"formats_style_attribute_as_css",
46+
"lints_dom_style_attributes_as_css",
4247
fs,
4348
console,
4449
result,
4550
));
4651
}
4752

48-
/// A value that is not a list of declarations is left exactly as written: a
49-
/// prop that happens to be called `style`, an interpolation, or nothing at all.
5053
#[test]
51-
fn leaves_non_css_style_attributes_alone() {
54+
fn does_not_lint_component_style_props_as_css() {
5255
let mut console = BufferConsole::default();
5356
let fs = MemoryFileSystem::default();
5457

5558
let config = Utf8Path::new("biome.json");
56-
fs.insert(config.into(), CONFIG.as_bytes());
59+
fs.insert(config.into(), LINTER_CONFIG.as_bytes());
60+
61+
let svelte_file = Utf8Path::new("component.svelte");
62+
fs.insert(
63+
svelte_file.into(),
64+
r#"<Button style="colr: blue" />
65+
"#
66+
.as_bytes(),
67+
);
5768

58-
let file = Utf8Path::new("index.html");
69+
let astro_file = Utf8Path::new("component.astro");
5970
fs.insert(
60-
file.into(),
61-
r#"<div style="{{ dynamic }}"></div>
62-
<div style="primary"></div>
63-
<div style=""></div>
64-
<div style=" "></div>
65-
<div style></div>
71+
astro_file.into(),
72+
r#"<Button style="colr: blue" />
6673
"#
6774
.as_bytes(),
6875
);
6976

7077
let (fs, result) = run_cli(
7178
fs,
7279
&mut console,
73-
Args::from(["format", "--write", file.as_str()].as_slice()),
80+
Args::from(["lint", svelte_file.as_str(), astro_file.as_str()].as_slice()),
7481
);
7582

7683
assert!(result.is_ok(), "run_cli returned {result:?}");
7784

7885
assert_cli_snapshot(SnapshotPayload::new(
7986
module_path!(),
80-
"leaves_non_css_style_attributes_alone",
87+
"does_not_lint_component_style_props_as_css",
8188
fs,
8289
console,
8390
result,
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
source: crates/biome_cli/tests/snap_test.rs
3+
expression: redactor(content)
4+
---
5+
## `biome.json`
6+
7+
```json
8+
{
9+
"html": {
10+
"linter": { "enabled": true },
11+
"experimentalFullSupportEnabled": true
12+
}
13+
}
14+
```
15+
16+
## `component.astro`
17+
18+
```astro
19+
<Button style="colr: blue" />
20+
21+
```
22+
23+
## `component.svelte`
24+
25+
```svelte
26+
<Button style="colr: blue" />
27+
28+
```
29+
30+
# Emitted Messages
31+
32+
```block
33+
Checked 2 files in <TIME>. No fixes applied.
34+
```

crates/biome_cli/tests/snapshots/main_cases_html_style_attribute/formats_style_attribute_as_css.snap

Lines changed: 0 additions & 33 deletions
This file was deleted.

crates/biome_cli/tests/snapshots/main_cases_html_style_attribute/leaves_non_css_style_attributes_alone.snap

Lines changed: 0 additions & 26 deletions
This file was deleted.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
---
2+
source: crates/biome_cli/tests/snap_test.rs
3+
expression: redactor(content)
4+
---
5+
## `biome.json`
6+
7+
```json
8+
{
9+
"html": {
10+
"linter": { "enabled": true },
11+
"experimentalFullSupportEnabled": true
12+
}
13+
}
14+
```
15+
16+
## `component.vue`
17+
18+
```vue
19+
<template><Button style="colr: blue" /></template>
20+
21+
```
22+
23+
## `index.html`
24+
25+
```html
26+
<div style="colr: blue"></div>
27+
<my-element style="colr: blue"></my-element>
28+
29+
```
30+
31+
# Termination Message
32+
33+
```block
34+
lint ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
35+
36+
× Some errors were emitted while running checks.
37+
38+
39+
40+
```
41+
42+
# Emitted Messages
43+
44+
```block
45+
component.vue:1:26 lint/correctness/noUnknownProperty ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
46+
47+
× Unknown property is not allowed.
48+
49+
> 1 │ <template><Button style="colr: blue" /></template>
50+
│ ^^^^
51+
2 │
52+
53+
i See CSS Specifications and browser specific properties for more details.
54+
55+
i To resolve this issue, replace the unknown property with a valid CSS property.
56+
57+
58+
```
59+
60+
```block
61+
index.html:1:13 lint/correctness/noUnknownProperty ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
62+
63+
× Unknown property is not allowed.
64+
65+
> 1 │ <div style="colr: blue"></div>
66+
│ ^^^^
67+
2 │ <my-element style="colr: blue"></my-element>
68+
3 │
69+
70+
i See CSS Specifications and browser specific properties for more details.
71+
72+
i To resolve this issue, replace the unknown property with a valid CSS property.
73+
74+
75+
```
76+
77+
```block
78+
index.html:2:20 lint/correctness/noUnknownProperty ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
79+
80+
× Unknown property is not allowed.
81+
82+
1 │ <div style="colr: blue"></div>
83+
> 2 │ <my-element style="colr: blue"></my-element>
84+
│ ^^^^
85+
3 │
86+
87+
i See CSS Specifications and browser specific properties for more details.
88+
89+
i To resolve this issue, replace the unknown property with a valid CSS property.
90+
91+
92+
```
93+
94+
```block
95+
Checked 2 files in <TIME>. No fixes applied.
96+
Found 3 errors.
97+
```

crates/biome_css_formatter/src/context.rs

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,13 @@ pub struct CssFormatOptions {
7777
delimiter_spacing: DelimiterSpacing,
7878
/// Whether to add a trailing newline at the end of the file. Defaults to true.
7979
trailing_newline: TrailingNewline,
80-
file_source: CssFileSource,
80+
_file_source: CssFileSource,
8181
}
8282

8383
impl CssFormatOptions {
8484
pub fn new(file_source: CssFileSource) -> Self {
8585
Self {
86-
file_source,
86+
_file_source: file_source,
8787
indent_style: IndentStyle::default(),
8888
indent_width: IndentWidth::default(),
8989
line_ending: LineEnding::default(),
@@ -94,20 +94,6 @@ impl CssFormatOptions {
9494
}
9595
}
9696

97-
/// The source the CSS came from, which says whether it is a whole
98-
/// stylesheet or a snippet embedded in another language.
99-
pub fn file_source(&self) -> &CssFileSource {
100-
&self.file_source
101-
}
102-
103-
/// Whether this CSS is the value of an HTML `style` attribute, which is
104-
/// printed on the attribute's line rather than as a block of its own.
105-
pub fn is_html_style_attribute(&self) -> bool {
106-
self.file_source
107-
.as_embedding_kind()
108-
.is_html_style_attribute()
109-
}
110-
11197
pub fn with_indent_style(mut self, indent_style: IndentStyle) -> Self {
11298
self.indent_style = indent_style;
11399
self

crates/biome_css_formatter/src/css/auxiliary/declaration_with_semicolon.rs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,6 @@ impl FormatNodeRule<CssDeclarationWithSemicolon> for FormatCssDeclarationWithSem
2424

2525
write!(f, [declaration.format()])?;
2626

27-
// The last declaration of a `style` attribute only needs its semicolon
28-
// once the attribute breaks across lines. Kept on one line, a trailing
29-
// `;` is noise the author did not write.
30-
if f.options().is_html_style_attribute() && node.syntax().next_sibling().is_none() {
31-
if let Some(semicolon) = semicolon_token.as_ref() {
32-
write!(f, [format_removed(semicolon)])?;
33-
}
34-
return write!(f, [if_group_breaks(&token(";"))]);
35-
}
36-
3727
match semicolon_token.as_ref() {
3828
Some(semicolon) => {
3929
if preserve_source_gap_before_semicolon

crates/biome_css_formatter/src/css/lists/declaration_list.rs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,6 @@ pub(crate) struct FormatCssDeclarationList;
66
impl FormatRule<CssDeclarationList> for FormatCssDeclarationList {
77
type Context = CssFormatContext;
88
fn fmt(&self, node: &CssDeclarationList, f: &mut CssFormatter) -> FormatResult<()> {
9-
// A `style` attribute stays on the tag's line while its declarations fit.
10-
if f.options().is_html_style_attribute() {
11-
let mut join = f.join_nodes_with_soft_line();
12-
13-
for declaration in node {
14-
join.entry(
15-
declaration.syntax(),
16-
&format_or_verbatim(declaration.format()),
17-
);
18-
}
19-
20-
return join.finish();
21-
}
22-
239
// This is one of the few cases where we _do_ want to respect empty
2410
// lines from the input, so we can use `join_nodes_with_hardline`.
2511
let mut join = f.join_nodes_with_hardline();

0 commit comments

Comments
 (0)