diff --git a/changelog_unreleased/html/17879.md b/changelog_unreleased/html/17879.md
new file mode 100644
index 000000000..4374739ac
--- /dev/null
+++ b/changelog_unreleased/html/17879.md
@@ -0,0 +1,22 @@
+#### Support format `allow` attribute of iframe element (#17879 by @kovsu)
+
+
+```html
+
+
+
+
+
+
+
+
+```
diff --git a/src/language-html/clean.js b/src/language-html/clean.js
index 84866990b..07942672a 100644
--- a/src/language-html/clean.js
+++ b/src/language-html/clean.js
@@ -23,7 +23,7 @@ const embeddedAngularControlFlowBlocks = new Set([
"case",
]);
-function clean(original, cloned) {
+function clean(original, cloned, parent) {
if (original.type === "text" || original.type === "comment") {
return null;
}
@@ -42,7 +42,9 @@ function clean(original, cloned) {
// HTML attributes
name === "style" ||
name === "class" ||
- name === "srcset" ||
+ (name === "srcset" &&
+ (parent.fullName === "img" || parent.fullName === "source")) ||
+ (name === "allow" && parent.fullName === "iframe") ||
name.startsWith("on") ||
// Vue attributes
name.startsWith("@") ||
@@ -50,9 +52,9 @@ function clean(original, cloned) {
name.startsWith(".") ||
name.startsWith("#") ||
name.startsWith("v-") ||
- name === "vars" ||
- name === "setup" ||
- name === "generic" ||
+ (name === "vars" && parent.fullName === "style") ||
+ ((name === "setup" || name === "generic") &&
+ parent.fullName === "script") ||
name === "slot-scope" ||
// Angular attributes
name.startsWith("(") ||
diff --git a/src/language-html/embed/attribute.js b/src/language-html/embed/attribute.js
index 7e3d201cb..9aaa8c555 100644
--- a/src/language-html/embed/attribute.js
+++ b/src/language-html/embed/attribute.js
@@ -3,6 +3,10 @@ import { mapDoc } from "../../document/utils.js";
import angularAttributePrinters from "./angular-attributes.js";
import { isClassNames, printClassNames } from "./class-names.js";
import { isEventHandler, printEventHandler } from "./event-handler.js";
+import {
+ isPermissionsPolicy,
+ printPermissionsPolicy,
+} from "./permissions-policy.js";
import { isSrcset, printSrcset } from "./srcset.js";
import { isStyle, printStyle } from "./style.js";
import vueAttributePrinters from "./vue-attributes.js";
@@ -24,6 +28,7 @@ const printers = [
{ test: isStyle, print: printStyle },
{ test: isEventHandler, print: printEventHandler },
{ test: isClassNames, print: printClassNames },
+ { test: isPermissionsPolicy, print: printPermissionsPolicy },
...vueAttributePrinters,
...angularAttributePrinters,
].map(({ test, print }) => ({
diff --git a/src/language-html/embed/parse-permissions-policy.js b/src/language-html/embed/parse-permissions-policy.js
new file mode 100644
index 000000000..9e38a9573
--- /dev/null
+++ b/src/language-html/embed/parse-permissions-policy.js
@@ -0,0 +1,64 @@
+/*
+Based on https://github.com/helmetjs/content-security-policy-parser/blob/main/mod.ts with modifications:
+
+1. Emit policy list instead of a `Map`, so we won't remove duplicated directives.
+1. Skip ASCII check, so we won't remove invalid directives.
+1. Skip directive name normalization, so the printer can know what's the original name.
+*/
+import htmlWhitespaceUtils from "../../utils/html-whitespace-utils.js";
+
+/**
+@typedef {{
+ name: string,
+ value: string[],
+}} Directive
+*/
+
+/**
+Parse a serialized Content Security Policy.
+https://w3c.github.io/webappsec-csp/#parse-serialized-policy
+
+@param {string} policy
+@returns {Directive[]}
+*/
+function parsePermissionsPolicy(policy) {
+ const directives = [];
+
+ // "For each token returned by strictly splitting serialized on the
+ // U+003B SEMICOLON character (;):"
+ for (let token of policy.split(";")) {
+ // "1. Strip leading and trailing ASCII whitespace from token."
+ token = htmlWhitespaceUtils.trim(token);
+
+ // "2. If token is an empty string, or if token is not an ASCII string,
+ // continue."
+ // Prettier: Skipped ASCII check.
+ if (!token) {
+ continue;
+ }
+
+ // We do these at the same time:
+ // "3. Let directive name be the result of collecting a sequence of
+ // code points from token which are not ASCII whitespace."
+ // "6. Let directive value be the result of splitting token on
+ // ASCII whitespace."
+ const [name, ...value] = htmlWhitespaceUtils.split(token);
+
+ // "4. Set directive name to be the result of running ASCII lowercase on
+ // directive name."
+ // Prettier: Skipped directive name normalization.
+
+ // "5. If policy's directive set contains a directive whose name is
+ // directive name, continue."
+ // Prettier: We preserve duplicated directives.
+
+ // "7. Let directive be a new directive whose name is directive name, and
+ // value is directive value."
+ // "8. Append directive to policy's directive set."
+ directives.push({ name, value });
+ }
+
+ return directives;
+}
+
+export default parsePermissionsPolicy;
diff --git a/src/language-html/embed/permissions-policy.js b/src/language-html/embed/permissions-policy.js
new file mode 100644
index 000000000..3d780e31c
--- /dev/null
+++ b/src/language-html/embed/permissions-policy.js
@@ -0,0 +1,35 @@
+import { ifBreak, line } from "../../document/builders.js";
+import { getUnescapedAttributeValue } from "../utils/index.js";
+import parsePermissionsPolicy from "./parse-permissions-policy.js";
+import { printExpand } from "./utils.js";
+
+/**
+@import {AttributeValuePredicate, AttributeValuePrint} from "./attribute.js"
+*/
+
+/** @type {AttributeValuePredicate} */
+const isPermissionsPolicy = ({ node }, options) =>
+ node.parent.name === "iframe" &&
+ node.fullName === "allow" &&
+ !options.parentParser &&
+ !node.value.includes("{{");
+
+/** @type {AttributeValuePrint} */
+function printPermissionsPolicy(textToDoc, print, path /* , options*/) {
+ const { node } = path;
+ const directives = parsePermissionsPolicy(getUnescapedAttributeValue(node));
+
+ if (directives.length === 0) {
+ // Return a truthy value to bypass the check in `printAttributeWithValuePrinter`
+ return [""];
+ }
+
+ return printExpand(
+ directives.map(({ name, value }, index) => [
+ [name, ...value].join(" "),
+ index === directives.length - 1 ? ifBreak(";") : [";", line],
+ ]),
+ );
+}
+
+export { isPermissionsPolicy, printPermissionsPolicy };
diff --git a/tests/format/html/attributes/iframe-allow-attribute/__snapshots__/format.test.js.snap b/tests/format/html/attributes/iframe-allow-attribute/__snapshots__/format.test.js.snap
new file mode 100644
index 000000000..91fc9bd1e
--- /dev/null
+++ b/tests/format/html/attributes/iframe-allow-attribute/__snapshots__/format.test.js.snap
@@ -0,0 +1,95 @@
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
+
+exports[`allow-attribute.html format 1`] = `
+====================================options=====================================
+parsers: ["html"]
+printWidth: 80
+ | printWidth
+=====================================input======================================
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+=====================================output=====================================
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+================================================================================
+`;
diff --git a/tests/format/html/attributes/iframe-allow-attribute/allow-attribute.html b/tests/format/html/attributes/iframe-allow-attribute/allow-attribute.html
new file mode 100644
index 000000000..6b5dafb27
--- /dev/null
+++ b/tests/format/html/attributes/iframe-allow-attribute/allow-attribute.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/format/html/attributes/iframe-allow-attribute/format.test.js b/tests/format/html/attributes/iframe-allow-attribute/format.test.js
new file mode 100644
index 000000000..88a427864
--- /dev/null
+++ b/tests/format/html/attributes/iframe-allow-attribute/format.test.js
@@ -0,0 +1 @@
+runFormatTest(import.meta, ["html"]);
diff --git a/tests/format/html/attributes/iframe-allow-attribute/small-print-width/__snapshots__/format.test.js.snap b/tests/format/html/attributes/iframe-allow-attribute/small-print-width/__snapshots__/format.test.js.snap
new file mode 100644
index 000000000..ba3c35e28
--- /dev/null
+++ b/tests/format/html/attributes/iframe-allow-attribute/small-print-width/__snapshots__/format.test.js.snap
@@ -0,0 +1,35 @@
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
+
+exports[`allow-attribute.html - {"printWidth":1} format 1`] = `
+====================================options=====================================
+parsers: ["html"]
+printWidth: 1
+ | printWidth
+=====================================input======================================
+
+
+
+
+
+=====================================output=====================================
+
+
+
+
+
+================================================================================
+`;
diff --git a/tests/format/html/attributes/iframe-allow-attribute/small-print-width/allow-attribute.html b/tests/format/html/attributes/iframe-allow-attribute/small-print-width/allow-attribute.html
new file mode 100644
index 000000000..1fd2d0f08
--- /dev/null
+++ b/tests/format/html/attributes/iframe-allow-attribute/small-print-width/allow-attribute.html
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/tests/format/html/attributes/iframe-allow-attribute/small-print-width/format.test.js b/tests/format/html/attributes/iframe-allow-attribute/small-print-width/format.test.js
new file mode 100644
index 000000000..e4bcf4d24
--- /dev/null
+++ b/tests/format/html/attributes/iframe-allow-attribute/small-print-width/format.test.js
@@ -0,0 +1 @@
+runFormatTest(import.meta, ["html"], { printWidth: 1 });