Skip to content

Commit 31b7eca

Browse files
dyc3ematipico
authored andcommitted
feat(format/html): handle iframe allow special formatting (#8290)
<!-- IMPORTANT!! If you generated this PR with the help of any AI assistance, please disclose it in the PR. https://github.com/biomejs/biome/blob/main/CONTRIBUTING.md#ai-assistance-notice --> <!-- Thanks for submitting a Pull Request! We appreciate you spending the time to work on these changes. Please provide enough information so that others can review your PR. Once created, your PR will be automatically labeled according to changed files. Learn more about contributing: https://github.com/biomejs/biome/blob/main/CONTRIBUTING.md --> ## Summary <!-- Explain the **motivation** for making this change. What existing problem does the pull request solve?--> Prettier 3.7 changed how iframe allow attributes are formatted. Prettier also changes it's behavior for some other attributes, and this PR lays out some infra to help with that in future PRs. See: https://prettier.io/blog/2025/11/27/3.7.0#change-17879 <!-- Link any relevant issues if necessary or include a transcript of any Discord discussion. --> <!-- If you create a user-facing change, please write a changeset: https://github.com/biomejs/biome/blob/main/CONTRIBUTING.md#writing-a-changeset (your changeset is often a good starting point for this summary as well) --> ## Test Plan <!-- What demonstrates that your implementation is correct? --> added snapshot test. ## Docs <!-- If you're submitting a new rule or action (or an option for them), the documentation is part of the code. Make sure rules and actions have example usages, and that all options are documented. --> <!-- For other features, please submit a documentation PR to the `next` branch of our website: https://github.com/biomejs/website/. Link the PR here once it's ready. -->
1 parent afb79bb commit 31b7eca

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

@@ -211,6 +211,20 @@ impl HtmlElement {
211211
}
212212
}
213213

214+
impl HtmlTagName {
215+
/// Returns the token text of the attribute name.
216+
pub fn token_text(&self) -> Option<TokenText> {
217+
self.value_token().ok().map(|token| token.token_text())
218+
}
219+
220+
/// Returns the trimmed token text of the attribute name.
221+
pub fn token_text_trimmed(&self) -> Option<TokenText> {
222+
self.value_token()
223+
.ok()
224+
.map(|token| token.token_text_trimmed())
225+
}
226+
}
227+
214228
#[cfg(test)]
215229
mod tests {
216230
use biome_html_factory::syntax::HtmlElement;

0 commit comments

Comments
 (0)