forked from streamlit/streamlit
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This PR adds a new command st.html which allows for insertion of arbitrary HTML into a Streamlit app. The goal of this feature is a nicer interface for adding simple HTML / CSS in-line. Please note this HTML is sanitized via dompurify (ex: no <script>, tags) and is similar to using the current st.markdown with unsafe_allow_html.
- Loading branch information
1 parent
b573f80
commit 4c55b35
Showing
45 changed files
with
1,089 additions
and
207 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file added
BIN
+3.64 KB
...__snapshots__/linux/st_html_test/st_html-inline_styles[dark_theme-chromium].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+4.59 KB
.../__snapshots__/linux/st_html_test/st_html-inline_styles[dark_theme-firefox].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+2.78 KB
...t/__snapshots__/linux/st_html_test/st_html-inline_styles[dark_theme-webkit].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+2.93 KB
..._snapshots__/linux/st_html_test/st_html-inline_styles[light_theme-chromium].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+4.11 KB
...__snapshots__/linux/st_html_test/st_html-inline_styles[light_theme-firefox].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+2.38 KB
.../__snapshots__/linux/st_html_test/st_html-inline_styles[light_theme-webkit].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+2.95 KB
...t/__snapshots__/linux/st_html_test/st_html-script_tags[dark_theme-chromium].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+4.43 KB
...ht/__snapshots__/linux/st_html_test/st_html-script_tags[dark_theme-firefox].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+2.78 KB
...ght/__snapshots__/linux/st_html_test/st_html-script_tags[dark_theme-webkit].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+2.99 KB
.../__snapshots__/linux/st_html_test/st_html-script_tags[light_theme-chromium].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+4.32 KB
...t/__snapshots__/linux/st_html_test/st_html-script_tags[light_theme-firefox].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+2.79 KB
...ht/__snapshots__/linux/st_html_test/st_html-script_tags[light_theme-webkit].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+11.6 KB
...apshots__/linux/st_html_test/st_html-style_tag_spacing[dark_theme-chromium].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+16.7 KB
...napshots__/linux/st_html_test/st_html-style_tag_spacing[dark_theme-firefox].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+10.2 KB
...snapshots__/linux/st_html_test/st_html-style_tag_spacing[dark_theme-webkit].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+10.8 KB
...pshots__/linux/st_html_test/st_html-style_tag_spacing[light_theme-chromium].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+15.8 KB
...apshots__/linux/st_html_test/st_html-style_tag_spacing[light_theme-firefox].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+9.71 KB
...napshots__/linux/st_html_test/st_html-style_tag_spacing[light_theme-webkit].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+1.7 KB
...ht/__snapshots__/linux/st_html_test/st_html-style_tags[dark_theme-chromium].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+2.68 KB
...ght/__snapshots__/linux/st_html_test/st_html-style_tags[dark_theme-firefox].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+1.6 KB
...ight/__snapshots__/linux/st_html_test/st_html-style_tags[dark_theme-webkit].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+1.52 KB
...t/__snapshots__/linux/st_html_test/st_html-style_tags[light_theme-chromium].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+2.34 KB
...ht/__snapshots__/linux/st_html_test/st_html-style_tags[light_theme-firefox].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+1.5 KB
...ght/__snapshots__/linux/st_html_test/st_html-style_tags[light_theme-webkit].png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024) | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
import streamlit as st | ||
|
||
# Test that we can render HTML with in-line styles | ||
st.html( | ||
""" | ||
<div style="font-family: 'Comic Sans MS'; color: orange"> | ||
This is a div with some inline styles. | ||
</div> | ||
""" | ||
) | ||
|
||
# Test that script tags are sanitized | ||
st.html( | ||
""" | ||
<i> This is a i tag </i> | ||
<script> | ||
alert('BEWARE - the script tag is scripting'); | ||
</script> | ||
<strong> This is a strong tag </strong> | ||
""" | ||
) | ||
|
||
# Test that style tags are applied | ||
st.html( | ||
""" | ||
<style> | ||
#corgi { | ||
color:blue; | ||
} | ||
</style> | ||
<div id="corgi">This text should be blue</div> | ||
""" | ||
) | ||
|
||
# Test that non-rendered HTML doesn't cause extra spacing | ||
st.write("Before tag:") | ||
st.html( | ||
""" | ||
<style> | ||
#random { | ||
color:blue; | ||
} | ||
</style> | ||
""" | ||
) | ||
st.write("After tag") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024) | ||
# | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# | ||
# http://www.apache.org/licenses/LICENSE-2.0 | ||
# | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
from playwright.sync_api import Page, expect | ||
|
||
from e2e_playwright.conftest import ImageCompareFunction | ||
|
||
|
||
def test_html_in_line_styles(themed_app: Page, assert_snapshot: ImageCompareFunction): | ||
"""Test that html renders correctly using snapshot testing.""" | ||
html_elements = themed_app.get_by_test_id("stHtml") | ||
expect(html_elements).to_have_count(4) | ||
first_html = html_elements.nth(0) | ||
|
||
expect(first_html).to_have_text("This is a div with some inline styles.") | ||
|
||
styled_div = first_html.locator("div") | ||
expect(styled_div).to_have_css("color", "rgb(255, 165, 0)") | ||
assert_snapshot(first_html, name="st_html-inline_styles") | ||
|
||
|
||
def test_html_sanitization(themed_app: Page, assert_snapshot: ImageCompareFunction): | ||
"""Test that html sanitizes script tags correctly.""" | ||
html_elements = themed_app.get_by_test_id("stHtml") | ||
expect(html_elements).to_have_count(4) | ||
second_html = html_elements.nth(1) | ||
|
||
expect(second_html).to_contain_text("This is a i tag") | ||
expect(second_html).to_contain_text("This is a strong tag") | ||
expect(second_html.locator("script")).to_have_count(0) | ||
assert_snapshot(second_html, name="st_html-script_tags") | ||
|
||
|
||
def test_html_style_tags(themed_app: Page, assert_snapshot: ImageCompareFunction): | ||
"""Test that html style tags are applied correctly.""" | ||
html_elements = themed_app.get_by_test_id("stHtml") | ||
expect(html_elements).to_have_count(4) | ||
third_html = html_elements.nth(2) | ||
|
||
expect(third_html).to_have_text("This text should be blue") | ||
expect(third_html.locator("div")).to_have_css("color", "rgb(0, 0, 255)") | ||
assert_snapshot(third_html, name="st_html-style_tags") | ||
|
||
|
||
def test_html_style_tag_spacing( | ||
themed_app: Page, assert_snapshot: ImageCompareFunction | ||
): | ||
"""Test that non-rendered html doesn't cause unnecessary spacing.""" | ||
html_elements = themed_app.get_by_test_id("stHtml") | ||
expect(html_elements).to_have_count(4) | ||
|
||
assert_snapshot( | ||
themed_app.get_by_test_id("stVerticalBlock"), name="st_html-style_tag_spacing" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
/** | ||
* Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024) | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import React from "react" | ||
import "@testing-library/jest-dom" | ||
import { screen } from "@testing-library/react" | ||
import { render } from "@streamlit/lib/src/test_util" | ||
import { Html as HtmlProto } from "@streamlit/lib/src/proto" | ||
import Html, { HtmlProps } from "./Html" | ||
|
||
const getProps = (elementProps: Partial<HtmlProto> = {}): HtmlProps => ({ | ||
element: HtmlProto.create({ | ||
body: "<div>Test Html</div>", | ||
...elementProps, | ||
}), | ||
width: 100, | ||
}) | ||
|
||
describe("HTML element", () => { | ||
it("renders the element as expected", () => { | ||
const props = getProps() | ||
render(<Html {...props} />) | ||
const html = screen.getByTestId("stHtml") | ||
expect(html).toHaveTextContent("Test Html") | ||
expect(html).toHaveStyle("width: 100px") | ||
}) | ||
|
||
it("handles <style> tags - applies style", () => { | ||
const props = getProps({ | ||
body: ` | ||
<style> | ||
#random { color: orange; } | ||
</style> | ||
<div id="random">Test Html</div> | ||
`, | ||
}) | ||
render(<Html {...props} />) | ||
const html = screen.getByTestId("stHtml") | ||
expect(html).toHaveTextContent("Test Html") | ||
// Check that the style tag is applied to the div | ||
expect(screen.getByText("Test Html")).toHaveStyle("color: orange") | ||
// Check that the unnecessary spacing handling by hiding parent | ||
// eslint-disable-next-line testing-library/no-node-access | ||
expect(html.parentElement).toHaveClass("empty-html") | ||
}) | ||
|
||
it("sanitizes <script> tags", () => { | ||
const props = getProps({ | ||
body: `<script> alert('BEWARE - the script tag is scripting'); </script>`, | ||
}) | ||
render(<Html {...props} />) | ||
expect(screen.queryByTestId("stHtml")).not.toBeInTheDocument() | ||
}) | ||
|
||
it("sanitizes <svg> tags", () => { | ||
const props = getProps({ | ||
body: ` | ||
<svg width="100" height="100"> | ||
<circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" /> | ||
</svg> | ||
`, | ||
}) | ||
render(<Html {...props} />) | ||
expect(screen.getByTestId("stHtml")).toHaveTextContent("") | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
/** | ||
* Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024) | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import React, { ReactElement, useState, useEffect, useRef } from "react" | ||
import DOMPurify from "dompurify" | ||
|
||
import { Html as HtmlProto } from "@streamlit/lib/src/proto" | ||
|
||
export interface HtmlProps { | ||
width: number | ||
element: HtmlProto | ||
} | ||
|
||
const sanitizeString = (html: string): string => { | ||
const sanitizationOptions = { | ||
// Default to permit HTML, SVG and MathML, this limits to HTML only | ||
USE_PROFILES: { html: true }, | ||
// glue elements like style, script or others to document.body and prevent unintuitive browser behavior in several edge-cases | ||
FORCE_BODY: true, | ||
} | ||
return DOMPurify.sanitize(html, sanitizationOptions) | ||
} | ||
|
||
/** | ||
* HTML code to insert into the page. | ||
*/ | ||
export default function Html({ element, width }: HtmlProps): ReactElement { | ||
const { body } = element | ||
const [sanitizedHtml, setSanitizedHtml] = useState(sanitizeString(body)) | ||
const htmlRef = useRef<HTMLDivElement | null>(null) | ||
|
||
useEffect(() => { | ||
if (sanitizeString(body) !== sanitizedHtml) { | ||
setSanitizedHtml(sanitizeString(body)) | ||
} | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [body]) | ||
|
||
useEffect(() => { | ||
if ( | ||
htmlRef.current?.clientHeight === 0 && | ||
htmlRef.current.parentElement?.childElementCount === 1 | ||
) { | ||
// div has no rendered content - hide to avoid unnecessary spacing | ||
htmlRef.current.parentElement.classList.add("empty-html") | ||
} | ||
}) | ||
|
||
return ( | ||
<> | ||
{sanitizedHtml && ( | ||
<div | ||
className="stHtml" | ||
data-testid="stHtml" | ||
ref={htmlRef} | ||
style={{ width: width }} | ||
dangerouslySetInnerHTML={{ __html: sanitizedHtml }} | ||
/> | ||
)} | ||
</> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
/** | ||
* Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024) | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
export { default } from "./Html" |
Oops, something went wrong.