Skip to content

Commit

Permalink
Add support for material icons in markdown (streamlit#8889)
Browse files Browse the repository at this point in the history
## Describe your changes

Adds support for rendering material icons within markdown
(`st.markdown`, labels, headers, ...). Icons must be specified via this
pattern: `:material/<icon_name>` The icon name is equivalent to the icon
names from [here](https://fonts.google.com/icons?icon.style=Rounded),
e.g.: `:material/search:` to display a search icon.

<img width="562" alt="image"
src="https://github.com/streamlit/streamlit/assets/2852129/581e5b03-4bc2-4aa0-a18b-d402a081fbc6">

- [Demo](https://icons-in-markdown.streamlit.app/)

- See the filled version here:
streamlit#8898

## GitHub Issue Link (if applicable)

- Closes streamlit#8726
- Closes streamlit#8783
- Closes streamlit#5150
- Related streamlit#7300

## Testing Plan

- Added unit tests (JS), Python unit tets are not needed since these
icons are not processed on the backend.
- Added e2e tests

---

**Contribution License Agreement**

By submitting this pull request you agree that all contributions to this
project are made under the Apache 2.0 license.
  • Loading branch information
lukasmasuch authored Jul 1, 2024
1 parent d16db4a commit 942f76e
Show file tree
Hide file tree
Showing 150 changed files with 140 additions and 68 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
3 changes: 2 additions & 1 deletion e2e_playwright/label_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import streamlit as st

valid_label = "**Bold Text** *Italicized* ~Strikethough~ `Code Block` 🐶 :joy:"
valid_label = ":material/check_circle: **Bold Text** *Italicized* ~Strikethough~ `Code Block` 🐶 :joy:"

color_label = (
"Colored Text - :red[red] :blue[blue] :green[green] :violet[violet] :orange[orange]"
Expand Down Expand Up @@ -115,6 +115,7 @@
"`Code Block`",
"🐶",
":joy:",
":material/check_circle: Icon",
]
)

Expand Down
4 changes: 3 additions & 1 deletion e2e_playwright/st_button.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ def on_click(x, y):
"button 6 (container_width + help)", use_container_width=True, help="help text"
)

st.button("_button 7_ (**styled** :green[label])")
st.button(
":material/search: _button 7_ (**styled** :green[label]) :material/arrow_forward:"
)

cols = st.columns(3)

Expand Down
2 changes: 1 addition & 1 deletion e2e_playwright/st_caption.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
st.caption("This is a caption with a help tooltip", help="This is some help tooltip!")

st.caption(
"""This is a caption that contains a bunch of interesting markdown:
""":material/chevron_right: This is a caption that contains a bunch of interesting markdown:
# heading 1
Expand Down
12 changes: 8 additions & 4 deletions e2e_playwright/st_heading.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,25 @@

import streamlit as st

st.title("This title is awesome!")
st.title(":material/info: This title is awesome!")
st.title("This title is awesome too!", help="Some help tooltip", anchor="awesome-title")
st.title("`Code` - Title with hidden Anchor", anchor=False)
st.title("a [link](#test)")
# Foreign language titles and anchors
st.title("日本語タイトル")
st.title("その他の邦題", anchor="アンカー")

st.header("This header is awesome!")
st.header(":material/info: This header is awesome!")
st.header("This header is awesome too!", anchor="awesome-header")
st.header("This header with hidden anchor is awesome tooooo!", anchor=False)
st.header("header with help", help="Some help tooltip")
st.header("header with help and hidden anchor", help="Some help tooltip", anchor=False)
st.header(
"header with help and hidden anchor",
help="Some help tooltip",
anchor=False,
)

st.subheader("This subheader is awesome!")
st.subheader(":material/info: This subheader is awesome!")
st.subheader("This subheader is awesome too!", anchor="awesome-subheader")
st.subheader("`Code` - Subheader without Anchor")
st.subheader(
Expand Down
20 changes: 10 additions & 10 deletions e2e_playwright/st_heading_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def test_correct_number_and_content_of_title_elements(app: Page):
titles = _get_title_elements(app)
expect(titles).to_have_count(6)

expect(titles.nth(0)).to_have_text("This title is awesome!")
expect(titles.nth(0)).to_have_text("info This title is awesome!")
expect(titles.nth(1)).to_have_text("This title is awesome too!")
expect(titles.nth(2)).to_have_text("Code - Title with hidden Anchor")
expect(titles.nth(3)).to_have_text("a link")
Expand All @@ -58,7 +58,7 @@ def test_correct_number_and_content_of_header_elements(app: Page):
headers = _get_header_elements(app).filter(has_not_text=_header_divider_filter_text)
expect(headers).to_have_count(5)

expect(headers.nth(0)).to_have_text("This header is awesome!")
expect(headers.nth(0)).to_have_text("info This header is awesome!")
expect(headers.nth(1)).to_have_text("This header is awesome too!")
expect(headers.nth(2)).to_have_text(
"This header with hidden anchor is awesome tooooo!"
Expand All @@ -72,7 +72,7 @@ def test_correct_number_and_content_of_subheader_elements(app: Page):
)
expect(subheaders).to_have_count(7)

expect(subheaders.nth(0)).to_have_text("This subheader is awesome!")
expect(subheaders.nth(0)).to_have_text("info This subheader is awesome!")
expect(subheaders.nth(1)).to_have_text("This subheader is awesome too!")
expect(subheaders.nth(2)).to_have_text("Code - Subheader without Anchor")
expect(subheaders.nth(3)).to_have_text("Code - Subheader with Anchor test_link")
Expand All @@ -82,7 +82,7 @@ def test_correct_number_and_content_of_subheader_elements(app: Page):
def test_display_titles_with_anchors(app: Page):
titles = _get_title_elements(app)

expect(titles.nth(0)).to_have_id("this-title-is-awesome")
expect(titles.nth(0)).to_have_id("info-this-title-is-awesome")
expect(titles.nth(1)).to_have_id("awesome-title")
expect(titles.nth(2)).to_have_id("code-title-with-hidden-anchor")
expect(titles.nth(3)).to_have_id("a-link")
Expand All @@ -95,10 +95,10 @@ def test_display_headers_with_anchors_and_style_icons(app: Page):
headers = _get_header_elements(app)

first_header = headers.nth(0)
expect(first_header).to_have_id("this-header-is-awesome")
expect(first_header).to_have_id("info-this-header-is-awesome")
expect(first_header.locator("svg")).to_be_attached()
expect(first_header.locator("a")).to_have_attribute(
"href", "#this-header-is-awesome"
"href", "#info-this-header-is-awesome"
)

second_header = headers.nth(1)
Expand All @@ -115,10 +115,10 @@ def test_display_subheaders_with_anchors_and_style_icons(app: Page):
headers = _get_subheader_elements(app)

first_header = headers.nth(0)
expect(first_header).to_have_id("this-subheader-is-awesome")
expect(first_header).to_have_id("info-this-subheader-is-awesome")
expect(first_header.locator("svg")).to_be_attached()
expect(first_header.locator("a")).to_have_attribute(
"href", "#this-subheader-is-awesome"
"href", "#info-this-subheader-is-awesome"
)

second_header = headers.nth(1)
Expand All @@ -138,9 +138,9 @@ def test_clicking_on_anchor_changes_url(app: Page):
first_header = headers.nth(0)
first_header.hover()
link = first_header.locator("a")
expect(link).to_have_attribute("href", "#this-header-is-awesome")
expect(link).to_have_attribute("href", "#info-this-header-is-awesome")
link.click()
expect(app).to_have_url(re.compile(".*#this-header-is-awesome"))
expect(app).to_have_url(re.compile(".*#info-this-header-is-awesome"))


def test_headers_snapshot_match(
Expand Down
32 changes: 12 additions & 20 deletions e2e_playwright/st_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,6 @@

st.markdown("[][]")

st.markdown(r"Inline math with $\KaTeX$")

st.markdown(
"""
$$
ax^2 + bx + c = 0
$$
"""
)

st.markdown(
"""
| Col1 | Col2 |
Expand All @@ -66,15 +56,16 @@
ax^2 + bx + c = 0
$$
# Some header 1
## :material/home: Some header
| Col1 | Col2 |
| --------- | ----------- |
| Some | Data |
| Some | :material/description: Data |
Some text
- :blue[blue], :green[green], :red[red], :violet[violet], :orange[orange], :gray[gray], :grey[grey], :rainbow[rainbow]
- :blue-background[blue], :green-background[green], :red-background[red], :violet-background[violet], :orange-background[orange], :gray-background[gray], :grey-background[grey], :rainbow-background[rainbow]
- :material/chevron_right: Markdown can contain material icons :red[:material/local_fire_department:] :green-background[:material/celebration: Yay]
:blue-background[**Bold text within blue background**], :red-background[*Italic text within red background*]
Expand Down Expand Up @@ -182,6 +173,15 @@ def draw_header_test(join_output):

st.latex(r"\LaTeX")

st.latex(
r"""
a + ar + a r^2 + a r^3 + \cdots + a r^{n-1} =
\sum_{k=0}^{n-1} ar^k =
a \left(\frac{1-r^{n}}{1-r}\right)
""",
help="This is example tooltip displayed on latex.",
)

try:
import sympy

Expand All @@ -192,14 +192,6 @@ def draw_header_test(join_output):

st.latex(out)

st.latex(
r"""
a + ar + a r^2 + a r^3 + \cdots + a r^{n-1} =
\sum_{k=0}^{n-1} ar^k =
a \left(\frac{1-r^{n}}{1-r}\right)
""",
help="This is example tooltip displayed on latex.",
)

st.markdown(
"Images in markdown should stay inside the container width:\n\n![image](./app/static/streamlit-logo.png)"
Expand Down
24 changes: 12 additions & 12 deletions e2e_playwright/st_markdown_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ def test_different_markdown_elements_in_one_block_displayed(

markdown_elements = themed_app.get_by_test_id("stMarkdown")

expect(markdown_elements).to_have_count(54)
expect(markdown_elements).to_have_count(52)

# Snapshot one big markdown block containing a variety of elements to reduce number of snapshots
multi_markdown_format_container = markdown_elements.nth(14)
multi_markdown_format_container = markdown_elements.nth(12)
multi_markdown_format_container.scroll_into_view_if_needed()
assert_snapshot(
multi_markdown_format_container,
Expand All @@ -52,8 +52,6 @@ def test_displays_individual_markdowns(app: Page):
"[text]",
"link",
"[][]",
"Inline math with KaTeX\\KaTeXKATE​X",
"ax2+bx+c=0ax^2 + bx + c = 0ax2+bx+c=0",
"Col1Col2SomeData",
"Bold text within blue background",
"Italic text within red background",
Expand All @@ -65,10 +63,10 @@ def test_displays_individual_markdowns(app: Page):
expect(markdown_elements.nth(i)).to_have_text(text[i])

# Check that the style contains the correct background color
blue_background = markdown_elements.nth(9).locator("span").first
red_background = markdown_elements.nth(10).locator("span").first
rainbow_background = markdown_elements.nth(11).locator("span").first
green_background = markdown_elements.nth(12).locator("span").first
blue_background = markdown_elements.nth(7).locator("span").first
red_background = markdown_elements.nth(8).locator("span").first
rainbow_background = markdown_elements.nth(9).locator("span").first
green_background = markdown_elements.nth(10).locator("span").first

expect(blue_background).to_have_css("background-color", "rgba(28, 131, 225, 0.1)")
expect(red_background).to_have_css("background-color", "rgba(255, 43, 43, 0.1)")
Expand Down Expand Up @@ -192,12 +190,14 @@ def test_help_tooltip_works(app: Page):


def test_latex_elements(themed_app: Page, assert_snapshot: ImageCompareFunction):
expect(themed_app.get_by_test_id("stMarkdown").nth(50)).to_contain_text("LATE​X")
expect(themed_app.get_by_test_id("stMarkdown").nth(51)).to_contain_text("a + b")
latex_elements = themed_app.get_by_test_id("stMarkdown")
assert_snapshot(latex_elements.nth(48), name="st_latex-latex")
expect(themed_app.get_by_test_id("stMarkdown").nth(48)).to_contain_text("LATE​X")

for i in range(50, 53):
assert_snapshot(latex_elements.nth(i), name=f"st_latex-{i}")
assert_snapshot(latex_elements.nth(49), name="st_latex-formula")

expect(themed_app.get_by_test_id("stMarkdown").nth(50)).to_contain_text("a + b")
assert_snapshot(latex_elements.nth(50), name="st_latex-sympy")


def test_large_image_in_markdown(app: Page, assert_snapshot: ImageCompareFunction):
Expand Down
1 change: 1 addition & 0 deletions frontend/lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"lodash": "^4.17.21",
"mapbox-gl": "^1.13.2",
"marked": "^4.2.12",
"mdast-util-find-and-replace": "^2.2.2",
"moment": "^2.29.4",
"moment-duration-format": "^2.3.2",
"moment-timezone": "^0.5.40",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ describe("StreamlitMarkdown", () => {
{ input: "[Link Text](www.example.com)", tag: "a", expected: "Link Text" },
{ input: "🐶", tag: "p", expected: "🐶" },
{ input: ":joy:", tag: "p", expected: "😂" },
{ input: ":material/search:", tag: "span", expected: "search" },
]

test.each(validCases)(
Expand Down Expand Up @@ -350,6 +351,25 @@ describe("StreamlitMarkdown", () => {
})
})

it("properly adds custom material icon", () => {
const source = `:material/search: Icon`
render(<StreamlitMarkdown source={source} allowHTML={false} />)
const markdown = screen.getByText("search")
const tagName = markdown.nodeName.toLowerCase()
expect(tagName).toBe("span")
expect(markdown).toHaveStyle(`font-family: Material Symbols Rounded`)
expect(markdown).toHaveStyle(`user-select: none`)
expect(markdown).toHaveStyle(`vertical-align: bottom`)
expect(markdown).toHaveStyle(`font-weight: normal`)
})

it("does not remove unknown directive", () => {
const source = `test :foo test:test :`
render(<StreamlitMarkdown source={source} allowHTML={false} />)
const markdown = screen.getByText("test :foo test:test :")
expect(markdown).toBeInTheDocument()
})

it("properly adds background colors", () => {
const redbg = transparentize(colors.red80, 0.9)
const orangebg = transparentize(colors.yellow70, 0.9)
Expand Down
Loading

0 comments on commit 942f76e

Please sign in to comment.