Skip to content

Commit 22d6847

Browse files
committed
feat(format/html): handle iframe allow special formatting
1 parent 893e36c commit 22d6847

File tree

8 files changed

+559
-15
lines changed

8 files changed

+559
-15
lines changed

.changeset/busy-zebras-begin.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
The HTML formatter has been updated to match Prettier 3.7's behavior for handling `<iframe>`'s `allow` attribute.
6+
7+
```diff
8+
- <iframe allow="layout-animations 'none'; unoptimized-images 'none'; oversized-images 'none'; sync-script 'none'; sync-xhr 'none'; unsized-media 'none';"></iframe>
9+
+ <iframe
10+
+ allow="
11+
+ layout-animations 'none';
12+
+ unoptimized-images 'none';
13+
+ oversized-images 'none';
14+
+ sync-script 'none';
15+
+ sync-xhr 'none';
16+
+ unsized-media 'none';
17+
+ "
18+
+ ></iframe>
19+
```

crates/biome_html_formatter/src/html/auxiliary/attribute.rs

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
use crate::{html::auxiliary::attribute_name::FormatHtmlAttributeNameOptions, prelude::*};
1+
use crate::{
2+
html::auxiliary::{
3+
attribute_initializer_clause::FormatHtmlAttributeInitializerClauseOptions,
4+
attribute_name::FormatHtmlAttributeNameOptions,
5+
},
6+
prelude::*,
7+
};
28
use biome_formatter::{FormatRuleWithOptions, write};
39
use biome_html_syntax::{HtmlAttribute, HtmlAttributeFields, HtmlTagName};
410

@@ -35,13 +41,27 @@ impl FormatNodeRule<HtmlAttribute> for FormatHtmlAttribute {
3541

3642
write!(
3743
f,
38-
[
39-
name.format()?.with_options(FormatHtmlAttributeNameOptions {
40-
is_canonical_html_element: self.is_canonical_html_element,
41-
tag_name: self.tag_name.clone(),
42-
}),
43-
initializer.format()
44-
]
45-
)
44+
[name.format()?.with_options(FormatHtmlAttributeNameOptions {
45+
is_canonical_html_element: self.is_canonical_html_element,
46+
tag_name: self.tag_name.clone(),
47+
}),]
48+
)?;
49+
50+
if let Some(initializer) = initializer.as_ref() {
51+
write!(
52+
f,
53+
[initializer
54+
.format()
55+
.with_options(FormatHtmlAttributeInitializerClauseOptions {
56+
tag_name: self
57+
.tag_name
58+
.as_ref()
59+
.and_then(|name| name.token_text_trimmed()),
60+
attribute_name: name.ok().and_then(|name| name.token_text_trimmed()),
61+
})]
62+
)?;
63+
}
64+
65+
Ok(())
4666
}
4767
}
Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,39 @@
1+
use std::fmt::Debug;
2+
13
use crate::prelude::*;
2-
use biome_formatter::write;
4+
use biome_formatter::{CstFormatContext, FormatRuleWithOptions, write};
35
use biome_html_syntax::{HtmlAttributeInitializerClause, HtmlAttributeInitializerClauseFields};
6+
use biome_rowan::TokenText;
7+
48
#[derive(Debug, Clone, Default)]
5-
pub(crate) struct FormatHtmlAttributeInitializerClause;
9+
pub(crate) struct FormatHtmlAttributeInitializerClause {
10+
/// The name of the tag this attribute belongs to.
11+
pub tag_name: Option<TokenText>,
12+
13+
/// The name of the attribute this initializer clause belongs to.
14+
pub attribute_name: Option<TokenText>,
15+
}
16+
17+
pub(crate) struct FormatHtmlAttributeInitializerClauseOptions {
18+
/// The name of the tag this attribute belongs to.
19+
pub tag_name: Option<TokenText>,
20+
21+
/// The name of the attribute this initializer clause belongs to.
22+
pub attribute_name: Option<TokenText>,
23+
}
24+
25+
impl FormatRuleWithOptions<HtmlAttributeInitializerClause>
26+
for FormatHtmlAttributeInitializerClause
27+
{
28+
type Options = FormatHtmlAttributeInitializerClauseOptions;
29+
30+
fn with_options(mut self, options: Self::Options) -> Self {
31+
self.tag_name = options.tag_name;
32+
self.attribute_name = options.attribute_name;
33+
self
34+
}
35+
}
36+
637
impl FormatNodeRule<HtmlAttributeInitializerClause> for FormatHtmlAttributeInitializerClause {
738
fn fmt_fields(
839
&self,
@@ -11,6 +42,68 @@ impl FormatNodeRule<HtmlAttributeInitializerClause> for FormatHtmlAttributeIniti
1142
) -> FormatResult<()> {
1243
let HtmlAttributeInitializerClauseFields { eq_token, value } = node.as_fields();
1344

14-
write![f, [eq_token.format(), value.format()]]
45+
// We currently only have special formatting for when the value is a string.
46+
if let Some(html_string) = value.as_ref()?.as_html_string()
47+
&& !f.context().comments().is_suppressed(html_string.syntax())
48+
{
49+
match (self.tag_name.as_deref(), self.attribute_name.as_deref()) {
50+
// Prettier 3.7 handles allow attribute on iframes specially by splitting the
51+
// value on semicolons and formatting it like a list, breaking if its too long, or leaving it on one line if it fits in the line width.
52+
// It also trims whitespace around each item, and removes empty items.
53+
//
54+
// Before:
55+
// ```html
56+
// <iframe allow=" camera; ; ; accelerometer;"></iframe>
57+
// ```
58+
//
59+
// After:
60+
// ```html
61+
// <iframe allow="camera; accelerometer"></iframe>
62+
// ```
63+
(Some("iframe"), Some("allow")) => {
64+
let content = html_string.inner_string_text()?;
65+
let value_token = html_string.value_token()?;
66+
67+
struct JoinWithSemicolon;
68+
impl Format<HtmlFormatContext> for JoinWithSemicolon {
69+
fn fmt(&self, f: &mut HtmlFormatter) -> FormatResult<()> {
70+
write!(f, [token(";"), soft_line_break_or_space()])
71+
}
72+
}
73+
74+
write!(
75+
f,
76+
[
77+
eq_token.format(),
78+
format_removed(&value_token),
79+
token("\""),
80+
group(&soft_block_indent(&format_with(|f| {
81+
let items = content
82+
.split(';')
83+
.map(TokenText::trim_token)
84+
.filter(|s| !s.is_empty())
85+
.collect::<Vec<_>>();
86+
f.join_with(JoinWithSemicolon)
87+
.entries(items.into_iter().map(|item| {
88+
located_token_text(
89+
&value_token,
90+
item.source_range(value_token.text_range()),
91+
)
92+
}))
93+
.finish()?;
94+
write!(f, [if_group_breaks(&token(";"))])?;
95+
Ok(())
96+
}))),
97+
token("\"")
98+
]
99+
)
100+
}
101+
_ => {
102+
write!(f, [eq_token.format(), value.format()])
103+
}
104+
}
105+
} else {
106+
write!(f, [eq_token.format(), value.format()])
107+
}
15108
}
16109
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<iframe allow></iframe>
2+
<iframe allow=""></iframe>
3+
<iframe allow=" "></iframe>
4+
<iframe allow=" ; "></iframe>
5+
<iframe allow=" ; ; ; "></iframe>
6+
<iframe allow=" camera"></iframe>
7+
<iframe allow="payment; usb; serial"></iframe>
8+
<iframe allow=" camera;; microphone;;;"></iframe>
9+
<iframe allow=" camera; ; ; accelerometer;"></iframe>
10+
<iframe allow="camera https://very-long-subdomain.very-long-domain-name.example.com https://another-very-long-subdomain.another-very-long-domain.example.org; microphone https://extremely-long-subdomain-name.extremely-long-domain-name.example.net"></iframe>
11+
<iframe allow="camera https://sub-domain.example.com:8080/path?query=value#fragment; microphone 'self'"></iframe>
12+
<iframe allow="camera https://subdomain1.example.com https://subdomain2.example.com https://subdomain3.example.com https://subdomain4.example.com https://subdomain5.example.com"></iframe>
13+
<iframe allow="layout-animations 'none'; unoptimized-images 'none'; oversized-images 'none'; sync-script 'none'; sync-xhr 'none'; unsized-media 'none';"></iframe>
14+
15+
<!-- Not iframe-->
16+
<a allow="
17+
camera 'self';
18+
ambient-light-sensor 'self'
19+
"></a>
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
---
2+
source: crates/biome_formatter_test/src/snapshot_builder.rs
3+
info: elements/iframe-allow.html
4+
---
5+
# Input
6+
7+
```html
8+
<iframe allow></iframe>
9+
<iframe allow=""></iframe>
10+
<iframe allow=" "></iframe>
11+
<iframe allow=" ; "></iframe>
12+
<iframe allow=" ; ; ; "></iframe>
13+
<iframe allow=" camera"></iframe>
14+
<iframe allow="payment; usb; serial"></iframe>
15+
<iframe allow=" camera;; microphone;;;"></iframe>
16+
<iframe allow=" camera; ; ; accelerometer;"></iframe>
17+
<iframe allow="camera https://very-long-subdomain.very-long-domain-name.example.com https://another-very-long-subdomain.another-very-long-domain.example.org; microphone https://extremely-long-subdomain-name.extremely-long-domain-name.example.net"></iframe>
18+
<iframe allow="camera https://sub-domain.example.com:8080/path?query=value#fragment; microphone 'self'"></iframe>
19+
<iframe allow="camera https://subdomain1.example.com https://subdomain2.example.com https://subdomain3.example.com https://subdomain4.example.com https://subdomain5.example.com"></iframe>
20+
<iframe allow="layout-animations 'none'; unoptimized-images 'none'; oversized-images 'none'; sync-script 'none'; sync-xhr 'none'; unsized-media 'none';"></iframe>
21+
22+
<!-- Not iframe-->
23+
<a allow="
24+
camera 'self';
25+
ambient-light-sensor 'self'
26+
"></a>
27+
28+
```
29+
30+
31+
=============================
32+
33+
# Outputs
34+
35+
## Output 1
36+
37+
-----
38+
Indent style: Tab
39+
Indent width: 2
40+
Line ending: LF
41+
Line width: 80
42+
Attribute Position: Auto
43+
Bracket same line: false
44+
Whitespace sensitivity: css
45+
Indent script and style: false
46+
Self close void elements: never
47+
-----
48+
49+
```html
50+
<iframe allow></iframe>
51+
<iframe allow=""></iframe>
52+
<iframe allow=""></iframe>
53+
<iframe allow=""></iframe>
54+
<iframe allow=""></iframe>
55+
<iframe allow="camera"></iframe>
56+
<iframe allow="payment; usb; serial"></iframe>
57+
<iframe allow="camera; microphone"></iframe>
58+
<iframe allow="camera; accelerometer"></iframe>
59+
<iframe
60+
allow="
61+
camera https://very-long-subdomain.very-long-domain-name.example.com https://another-very-long-subdomain.another-very-long-domain.example.org;
62+
microphone https://extremely-long-subdomain-name.extremely-long-domain-name.example.net;
63+
"
64+
></iframe>
65+
<iframe
66+
allow="
67+
camera https://sub-domain.example.com:8080/path?query=value#fragment;
68+
microphone 'self';
69+
"
70+
></iframe>
71+
<iframe
72+
allow="
73+
camera https://subdomain1.example.com https://subdomain2.example.com https://subdomain3.example.com https://subdomain4.example.com https://subdomain5.example.com;
74+
"
75+
></iframe>
76+
<iframe
77+
allow="
78+
layout-animations 'none';
79+
unoptimized-images 'none';
80+
oversized-images 'none';
81+
sync-script 'none';
82+
sync-xhr 'none';
83+
unsized-media 'none';
84+
"
85+
></iframe>
86+
87+
<!-- Not iframe-->
88+
<a
89+
allow="
90+
camera 'self';
91+
ambient-light-sensor 'self'
92+
"
93+
></a>
94+
```
95+
96+
# Lines exceeding max width of 80 characters
97+
```
98+
12: camera https://very-long-subdomain.very-long-domain-name.example.com https://another-very-long-subdomain.another-very-long-domain.example.org;
99+
13: microphone https://extremely-long-subdomain-name.extremely-long-domain-name.example.net;
100+
24: camera https://subdomain1.example.com https://subdomain2.example.com https://subdomain3.example.com https://subdomain4.example.com https://subdomain5.example.com;
101+
```

crates/biome_html_syntax/src/attr_ext.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use crate::{
22
AnyHtmlAttribute, AnyHtmlAttributeInitializer, HtmlAttribute, HtmlAttributeList,
3-
inner_string_text,
3+
HtmlAttributeName, inner_string_text,
44
};
5-
use biome_rowan::{AstNodeList, Text};
5+
use biome_rowan::{AstNodeList, Text, TokenText};
66

77
impl AnyHtmlAttributeInitializer {
88
/// Returns the string value of the attribute, if available, without quotes.
@@ -32,3 +32,17 @@ impl HtmlAttributeList {
3232
})
3333
}
3434
}
35+
36+
impl HtmlAttributeName {
37+
/// Returns the token text of the attribute name.
38+
pub fn token_text(&self) -> Option<TokenText> {
39+
self.value_token().ok().map(|token| token.token_text())
40+
}
41+
42+
/// Returns the trimmed token text of the attribute name.
43+
pub fn token_text_trimmed(&self) -> Option<TokenText> {
44+
self.value_token()
45+
.ok()
46+
.map(|token| token.token_text_trimmed())
47+
}
48+
}

crates/biome_html_syntax/src/element_ext.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{
22
AnyHtmlElement, AstroEmbeddedContent, HtmlAttribute, HtmlElement, HtmlEmbeddedContent,
3-
HtmlSelfClosingElement, HtmlSyntaxToken, ScriptType, inner_string_text,
3+
HtmlSelfClosingElement, HtmlSyntaxToken, HtmlTagName, ScriptType, inner_string_text,
44
};
55
use biome_rowan::{AstNodeList, SyntaxResult, TokenText, declare_node_union};
66

@@ -206,6 +206,20 @@ impl HtmlElement {
206206
}
207207
}
208208

209+
impl HtmlTagName {
210+
/// Returns the token text of the attribute name.
211+
pub fn token_text(&self) -> Option<TokenText> {
212+
self.value_token().ok().map(|token| token.token_text())
213+
}
214+
215+
/// Returns the trimmed token text of the attribute name.
216+
pub fn token_text_trimmed(&self) -> Option<TokenText> {
217+
self.value_token()
218+
.ok()
219+
.map(|token| token.token_text_trimmed())
220+
}
221+
}
222+
209223
#[cfg(test)]
210224
mod tests {
211225
use biome_html_factory::syntax::HtmlElement;

0 commit comments

Comments
 (0)