Allow applying @media, @keyframes and @import in custom CSS (#7395)

* allow @media and @keyframes in custom CSS

* add changeset

* demo tweak

* formatting

* fix

* tweak

* add .dark test

* formatting

* add font-face test

* support @import statements

* add changeset

* tweak

* fix test

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
This commit is contained in:
Hannah 2024-02-13 00:39:30 +01:00 committed by GitHub
parent fa8225d24d
commit 46b45683e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 194 additions and 3 deletions

View File

@ -0,0 +1,6 @@
---
"@gradio/app": patch
"gradio": patch
---
fix:Allow applying `@media`, `@keyframes` and `@import` in custom CSS

View File

@ -0,0 +1 @@
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: custom_css"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "css = \"\"\"\n", "/* CSSKeyframesRule for animation */\n", "@keyframes animation {\n", " from {background-color: red;}\n", " to {background-color: blue;}\n", "}\n", "\n", ".cool-col {\n", " animation-name: animation;\n", " animation-duration: 4s;\n", " animation-iteration-count: infinite;\n", " border-radius: 10px;\n", " padding: 20px;\n", "}\n", "\n", "/* CSSStyleRule */\n", ".markdown {\n", " background-color: lightblue;\n", " padding: 20px;\n", "}\n", "\n", ".markdown p {\n", " color: royalblue;\n", "}\n", "\n", "/* CSSMediaRule */\n", "@media screen and (max-width: 600px) {\n", " .markdown {\n", " background: blue;\n", " }\n", " .markdown p {\n", " color: lightblue;\n", " }\n", "}\n", "\n", ".dark .markdown {\n", " background: pink;\n", "}\n", "\n", ".darktest h3 {\n", " color: black;\n", "}\n", "\n", ".dark .darktest h3 {\n", " color: yellow;\n", "}\n", "\n", "/* CSSFontFaceRule */\n", "@font-face {\n", " font-family: \"test-font\";\n", " src: url(\"https://mdn.github.io/css-examples/web-fonts/VeraSeBd.ttf\") format(\"truetype\");\n", "}\n", "\n", ".cool-col {\n", " font-family: \"test-font\";\n", "}\n", "\n", "/* CSSImportRule */\n", "@import url(\"https://fonts.googleapis.com/css2?family=Protest+Riot&display=swap\");\n", "\n", ".markdown {\n", " font-family: \"Protest Riot\", sans-serif;\n", "}\n", "\"\"\"\n", "\n", "with gr.Blocks(css=css) as demo:\n", " with gr.Column(elem_classes=\"cool-col\"):\n", " gr.Markdown(\"### Gradio Demo with Custom CSS\", elem_classes=\"darktest\")\n", " gr.Markdown(elem_classes=\"markdown\", value=\"Resize the browser window to see the CSS media query in action.\")\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}

74
demo/custom_css/run.py Normal file
View File

@ -0,0 +1,74 @@
import gradio as gr
css = """
/* CSSKeyframesRule for animation */
@keyframes animation {
from {background-color: red;}
to {background-color: blue;}
}
.cool-col {
animation-name: animation;
animation-duration: 4s;
animation-iteration-count: infinite;
border-radius: 10px;
padding: 20px;
}
/* CSSStyleRule */
.markdown {
background-color: lightblue;
padding: 20px;
}
.markdown p {
color: royalblue;
}
/* CSSMediaRule */
@media screen and (max-width: 600px) {
.markdown {
background: blue;
}
.markdown p {
color: lightblue;
}
}
.dark .markdown {
background: pink;
}
.darktest h3 {
color: black;
}
.dark .darktest h3 {
color: yellow;
}
/* CSSFontFaceRule */
@font-face {
font-family: "test-font";
src: url("https://mdn.github.io/css-examples/web-fonts/VeraSeBd.ttf") format("truetype");
}
.cool-col {
font-family: "test-font";
}
/* CSSImportRule */
@import url("https://fonts.googleapis.com/css2?family=Protest+Riot&display=swap");
.markdown {
font-family: "Protest Riot", sans-serif;
}
"""
with gr.Blocks(css=css) as demo:
with gr.Column(elem_classes="cool-col"):
gr.Markdown("### Gradio Demo with Custom CSS", elem_classes="darktest")
gr.Markdown(elem_classes="markdown", value="Resize the browser window to see the CSS media query in action.")
if __name__ == "__main__":
demo.launch()

View File

@ -29,9 +29,16 @@ export function prefix_css(
const stylesheet = new CSSStyleSheet();
stylesheet.replaceSync(string);
let importString = "";
string = string.replace(/@import\s+url\((.*?)\);\s*/g, (match, url) => {
importString += `@import url(${url});\n`;
return ""; // remove and store any @import statements from the CSS
});
const rules = stylesheet.cssRules;
let css_string = "";
let gradio_css_infix = `gradio-app .gradio-container.gradio-container-${version} .contain `;
for (let i = 0; i < rules.length; i++) {
const rule = rules[i];
@ -45,17 +52,49 @@ export function prefix_css(
.split(",")
.map(
(s) =>
`${
is_dark_rule ? ".dark" : ""
} gradio-app .gradio-container.gradio-container-${version} .contain ${s.trim()} `
`${is_dark_rule ? ".dark" : ""} ${gradio_css_infix} ${s.trim()} `
)
.join(",");
css_string += rule.cssText;
css_string += rule.cssText.replace(selector, new_selector);
}
} else if (rule instanceof CSSMediaRule) {
let mediaCssString = `@media ${rule.media.mediaText} {`;
for (let j = 0; j < rule.cssRules.length; j++) {
const innerRule = rule.cssRules[j];
if (innerRule instanceof CSSStyleRule) {
let is_dark_rule = innerRule.cssText.includes(".dark ");
const selector = innerRule.selectorText;
const new_selector = selector
.replace(".dark", "")
.split(",")
.map(
(s) =>
`${
is_dark_rule ? ".dark" : ""
} ${gradio_css_infix} ${s.trim()} `
)
.join(",");
mediaCssString += innerRule.cssText.replace(selector, new_selector);
}
}
mediaCssString += "}";
css_string += mediaCssString;
} else if (rule instanceof CSSKeyframesRule) {
css_string += `@keyframes ${rule.name} {`;
for (let j = 0; j < rule.cssRules.length; j++) {
const innerRule = rule.cssRules[j];
if (innerRule instanceof CSSKeyframeRule) {
css_string += `${innerRule.keyText} { ${innerRule.style.cssText} }`;
}
}
css_string += "}";
} else if (rule instanceof CSSFontFaceRule) {
css_string += `@font-face { ${rule.style.cssText} }`;
}
}
css_string = importString + css_string;
style_element.textContent = css_string;
document.head.appendChild(style_element);

View File

@ -0,0 +1,71 @@
import { test, expect } from "@gradio/tootils";
test("renders the correct elements", async ({ page }) => {
await expect(page.getByTestId("markdown")).toHaveCount(2);
});
test("applies the custom CSS styles", async ({ page }) => {
// Test for CSSKeyframesRule
const animationName = await page
.locator(".cool-col")
.evaluate((node) => getComputedStyle(node).animationName);
expect(animationName).toBe("animation");
// Test for CSSMediaRule and CSSStyleRule
await page.setViewportSize({ width: 500, height: 720 });
await expect(page.locator(".markdown").nth(1)).toHaveCSS(
"background-color",
"rgb(0, 0, 255)"
);
await expect(page.locator(".markdown p")).toHaveCSS(
"color",
"rgb(173, 216, 230)"
);
await page.setViewportSize({ width: 1280, height: 720 });
await expect(page.locator(".markdown").nth(1)).toHaveCSS(
"background-color",
"rgb(173, 216, 230)"
);
await expect(page.locator(".markdown p")).toHaveCSS(
"color",
"rgb(65, 105, 225)"
);
});
test("applies the custom font family", async ({ page }) => {
await expect(
page.getByRole("heading", { name: "Gradio Demo with Custom CSS" })
).toHaveCSS("font-family", "test-font");
});
test("applies resources from the @import rule", async ({ page }) => {
await expect(page.getByText("Resize the browser window to")).toHaveCSS(
"font-family",
'"Protest Riot", sans-serif'
);
});
test(".dark styles are applied corrently", async ({ page }) => {
await page.emulateMedia({ colorScheme: "dark" });
await expect(page.locator(".markdown").nth(1)).toHaveCSS(
"background-color",
"rgb(255, 192, 203)"
);
await expect(page.locator(".darktest h3")).toHaveCSS(
"color",
"rgb(255, 255, 0)"
);
await page.emulateMedia({ colorScheme: "light" });
await expect(page.locator(".markdown").nth(1)).toHaveCSS(
"background-color",
"rgb(173, 216, 230)"
);
await expect(page.locator(".darktest h3")).toHaveCSS(
"color",
"rgb(31, 41, 55)"
);
});