diff --git a/e2e/scripts/st_header.py b/e2e/scripts/st_header.py index 208c3935654f..5c601b905339 100644 --- a/e2e/scripts/st_header.py +++ b/e2e/scripts/st_header.py @@ -15,4 +15,3 @@ import streamlit as st st.header("This header is awesome!") -st.header("This header is awesome too!", anchor="awesome-header") diff --git a/e2e/scripts/st_markdown.py b/e2e/scripts/st_markdown.py index 626ef9b2fe60..3d7a1853d06c 100644 --- a/e2e/scripts/st_markdown.py +++ b/e2e/scripts/st_markdown.py @@ -35,7 +35,3 @@ $$ """ ) - -st.markdown("# Some header 1") -st.markdown("## Some header 2") -st.markdown("### Some header 3") diff --git a/e2e/scripts/st_subheader.py b/e2e/scripts/st_subheader.py index dc800a50229b..ac25ea566621 100644 --- a/e2e/scripts/st_subheader.py +++ b/e2e/scripts/st_subheader.py @@ -15,4 +15,3 @@ import streamlit as st st.subheader("This subheader is awesome!") -st.subheader("This subheader is awesome too!", anchor="awesome-subheader") diff --git a/e2e/scripts/st_title.py b/e2e/scripts/st_title.py index dbea889bcde6..6022610e9948 100644 --- a/e2e/scripts/st_title.py +++ b/e2e/scripts/st_title.py @@ -15,4 +15,3 @@ import streamlit as st st.title("This title is awesome!") -st.title("This title is awesome too!", anchor="awesome-title") diff --git a/e2e/specs/st_header.spec.js b/e2e/specs/st_header.spec.js index 0c7257ed9eb3..b7990b268cb7 100644 --- a/e2e/specs/st_header.spec.js +++ b/e2e/specs/st_header.spec.js @@ -20,21 +20,10 @@ describe("st.header", () => { cy.visit("http://localhost:3000/"); }); - it("displays correct number of elements", () => { - cy.get(".element-container .stMarkdown h2").should("have.length", 2); - }); - it("displays a header", () => { - cy.get(".element-container .stMarkdown h2").then(els => { - expect(els[0].textContent).to.eq("This header is awesome!"); - expect(els[1].textContent).to.eq("This header is awesome too!"); - }); - }); - - it("displays headers with anchors", () => { - cy.get(".element-container .stMarkdown h2").then(els => { - cy.wrap(els[0]).should("have.attr", "id", "this-header-is-awesome"); - cy.wrap(els[1]).should("have.attr", "id", "awesome-header"); - }); + cy.get(".element-container .stMarkdown h2").should( + "contain", + "This header is awesome!" + ); }); }); diff --git a/e2e/specs/st_markdown.spec.js b/e2e/specs/st_markdown.spec.js index 218c77588946..3e588214a400 100644 --- a/e2e/specs/st_markdown.spec.js +++ b/e2e/specs/st_markdown.spec.js @@ -20,11 +20,8 @@ describe("st.markdown", () => { cy.visit("http://localhost:3000/"); }); - it("displays correct number of elements", () => { - cy.get(".element-container .stMarkdown").should("have.length", 11); - }); - it("displays markdown", () => { + cy.get(".element-container .stMarkdown").should("have.length", 8); cy.get(".element-container .stMarkdown").then(els => { expect(els[0].textContent).to.eq("This markdown is awesome! 😎"); expect(els[1].textContent).to.eq("This HTML tag is escaped!"); @@ -36,30 +33,14 @@ describe("st.markdown", () => { expect(els[7].textContent).to.eq( "ax2+bx+c=0ax^2 + bx + c = 0ax2+bx+c=0" ); - expect(els[8].textContent).to.eq("Some header 1"); - expect(els[9].textContent).to.eq("Some header 2"); - expect(els[10].textContent).to.eq("Some header 3"); cy.wrap(els[3]) .find("a") .should("not.exist"); + cy.wrap(els[4]) .find("a") .should("have.attr", "href"); }); }); - - it("displays headers with anchors", () => { - cy.get(".element-container .stMarkdown").then(els => { - cy.wrap(els[8]) - .find("h1") - .should("have.attr", "id", "some-header-1"); - cy.wrap(els[9]) - .find("h2") - .should("have.attr", "id", "some-header-2"); - cy.wrap(els[10]) - .find("h3") - .should("have.attr", "id", "some-header-3"); - }); - }); }); diff --git a/e2e/specs/st_subheader.spec.js b/e2e/specs/st_subheader.spec.js index 1ca61c7657d0..de6b03ea6d02 100644 --- a/e2e/specs/st_subheader.spec.js +++ b/e2e/specs/st_subheader.spec.js @@ -20,21 +20,10 @@ describe("st.subheader", () => { cy.visit("http://localhost:3000/"); }); - it("displays correct number of elements", () => { - cy.get(".element-container .stMarkdown h3").should("have.length", 2); - }); - it("displays a subheader", () => { - cy.get(".element-container .stMarkdown h3").then(els => { - expect(els[0].textContent).to.eq("This subheader is awesome!"); - expect(els[1].textContent).to.eq("This subheader is awesome too!"); - }); - }); - - it("displays subheaders with anchors", () => { - cy.get(".element-container .stMarkdown h3").then(els => { - cy.wrap(els[0]).should("have.attr", "id", "this-subheader-is-awesome"); - cy.wrap(els[1]).should("have.attr", "id", "awesome-subheader"); - }); + cy.get(".element-container .stMarkdown h3").should( + "contain", + "This subheader is awesome!" + ); }); }); diff --git a/e2e/specs/st_title.spec.js b/e2e/specs/st_title.spec.js index f69302383de8..1d72b0d95e14 100644 --- a/e2e/specs/st_title.spec.js +++ b/e2e/specs/st_title.spec.js @@ -20,21 +20,10 @@ describe("st.title", () => { cy.visit("http://localhost:3000/"); }); - it("displays correct number of elements", () => { - cy.get(".element-container .stMarkdown h1").should("have.length", 2); - }); - it("displays a title", () => { - cy.get(".element-container .stMarkdown h1").then(els => { - expect(els[0].textContent).to.eq("This title is awesome!"); - expect(els[1].textContent).to.eq("This title is awesome too!"); - }); - }); - - it("displays title with anchors", () => { - cy.get(".element-container .stMarkdown h1").then(els => { - cy.wrap(els[0]).should("have.attr", "id", "this-title-is-awesome"); - cy.wrap(els[1]).should("have.attr", "id", "awesome-title"); - }); + cy.get(".element-container .stMarkdown h1").should( + "contain", + "This title is awesome!" + ); }); }); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 524bbb2a12f5..a4f9c6b6b611 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -58,7 +58,6 @@ import { SessionState, Config, } from "autogen/proto" -import { without, concat } from "lodash" import { RERUN_PROMPT_MODAL_DIALOG } from "lib/baseconsts" import { SessionInfo } from "lib/SessionInfo" @@ -102,7 +101,6 @@ interface State { layout: PageConfig.Layout initialSidebarState: PageConfig.SidebarState allowRunOnSave: boolean - reportFinishedHandlers: (() => void)[] deployParams?: IDeployParams | null } @@ -160,7 +158,6 @@ export class App extends PureComponent { layout: PageConfig.Layout.CENTERED, initialSidebarState: PageConfig.SidebarState.AUTO, allowRunOnSave: true, - reportFinishedHandlers: [], deployParams: null, } @@ -557,12 +554,6 @@ export class App extends PureComponent { */ handleReportFinished(status: ForwardMsg.ReportFinishedStatus): void { if (status === ForwardMsg.ReportFinishedStatus.FINISHED_SUCCESSFULLY) { - // Notify any subscribers of this event (and do it on the next cycle of - // the event loop) - window.setTimeout(() => { - this.state.reportFinishedHandlers.map(handler => handler()) - }, 0) - // Clear any stale elements left over from the previous run. // (We don't do this if our script had a compilation error and didn't // finish successfully.) @@ -904,18 +895,6 @@ export class App extends PureComponent { this.setState({ isFullScreen }) } - addReportFinishedHandler = (func: () => void): void => { - this.setState({ - reportFinishedHandlers: concat(this.state.reportFinishedHandlers, func), - }) - } - - removeReportFinishedHandler = (func: () => void): void => { - this.setState({ - reportFinishedHandlers: without(this.state.reportFinishedHandlers, func), - }) - } - render(): JSX.Element { const { allowRunOnSave, @@ -956,8 +935,6 @@ export class App extends PureComponent { embedded: isEmbeddedInIFrame(), isFullScreen, setFullScreen: this.handleFullScreen, - addReportFinishedHandler: this.addReportFinishedHandler, - removeReportFinishedHandler: this.removeReportFinishedHandler, }} > {}, - addReportFinishedHandler: (func: () => void) => {}, - removeReportFinishedHandler: (func: () => void) => {}, }) diff --git a/frontend/src/components/shared/StreamlitMarkdown/StreamlitMarkdown.test.tsx b/frontend/src/components/shared/StreamlitMarkdown/StreamlitMarkdown.test.tsx index 63fa3f35bf9f..fd05e8ffccb1 100644 --- a/frontend/src/components/shared/StreamlitMarkdown/StreamlitMarkdown.test.tsx +++ b/frontend/src/components/shared/StreamlitMarkdown/StreamlitMarkdown.test.tsx @@ -22,7 +22,6 @@ import { mount } from "enzyme" import { linkWithTargetBlank, linkReferenceHasParens, - createAnchorFromText, } from "./StreamlitMarkdown" // Fixture Generator @@ -34,20 +33,6 @@ const getMarkdownElement = (body: string): ReactElement => { return } -describe("createAnchorFromText", () => { - it("generates slugs correctly", () => { - const cases = [ - ["some header", "some-header"], - ["some -24$35-9824 header", "some-24-35-9824-header"], - ["blah___blah___blah", "blah-blah-blah"], - ] - - cases.forEach(([s, want]) => { - expect(createAnchorFromText(s)).toEqual(want) - }) - }) -}) - describe("linkReference", () => { it("renders a link with _blank target", () => { const body = "Some random URL like [Streamlit](https://streamlit.io/)" diff --git a/frontend/src/components/shared/StreamlitMarkdown/StreamlitMarkdown.tsx b/frontend/src/components/shared/StreamlitMarkdown/StreamlitMarkdown.tsx index b08ff48a66b8..b2b8fe4ee2ec 100644 --- a/frontend/src/components/shared/StreamlitMarkdown/StreamlitMarkdown.tsx +++ b/frontend/src/components/shared/StreamlitMarkdown/StreamlitMarkdown.tsx @@ -17,23 +17,16 @@ import React, { ReactElement, ReactNode, Fragment, PureComponent } from "react" import ReactMarkdown from "react-markdown" -import { once } from "lodash" // @ts-ignore import htmlParser from "react-markdown/plugins/html-parser" // @ts-ignore import { BlockMath, InlineMath } from "react-katex" // @ts-ignore import RemarkMathPlugin from "remark-math" -import { Link as LinkIcon } from "react-feather" // @ts-ignore import RemarkEmoji from "remark-emoji" -import PageLayoutContext from "components/core/PageLayoutContext" import CodeBlock from "components/elements/CodeBlock/" -import { - StyledStreamlitMarkdown, - StyledLinkIconContainer, - StyledLinkIcon, -} from "./styled-components" +import { StyledStreamlitMarkdown } from "./styled-components" import "katex/dist/katex.min.css" @@ -50,128 +43,6 @@ export interface Props { allowHTML: boolean } -/** - * Creates a slug suitable for use as an anchor given a string. - * Splits the string on non-alphanumeric characters, and joins with a dash. - */ -export function createAnchorFromText(text: string | null): string { - const newAnchor = text - ?.toLowerCase() - .split(/[^A-Za-z0-9]/) - .filter(Boolean) - .join("-") - return newAnchor || "" -} - -// wrapping in `once` ensures we only scroll once -const scrollNodeIntoView = once((node: HTMLElement): void => { - node.scrollIntoView(true) -}) - -interface HeadingWithAnchorProps { - tag: string - anchor?: string - children: [ReactElement] -} - -interface CustomHeadingProps { - level: string | number - children: [ReactElement] -} - -interface CustomParsedHtmlProps { - type: ReactElement - element: { - type: string - props: { - "data-anchor": string - children: [ReactElement] - } - } -} - -function HeadingWithAnchor({ - tag, - anchor: propsAnchor, - children, -}: HeadingWithAnchorProps): ReactElement { - const [elementId, setElementId] = React.useState(propsAnchor) - const [target, setTarget] = React.useState(null) - - const { - addReportFinishedHandler, - removeReportFinishedHandler, - } = React.useContext(PageLayoutContext) - - const onReportFinished = React.useCallback(() => { - if (target !== null) { - // wait a bit for everything on page to finish loading - window.setTimeout(() => { - scrollNodeIntoView(target) - }, 300) - } - }, [target]) - - React.useEffect(() => { - addReportFinishedHandler(onReportFinished) - return () => { - removeReportFinishedHandler(onReportFinished) - } - }, [addReportFinishedHandler, removeReportFinishedHandler, onReportFinished]) - - const ref = React.useCallback( - node => { - if (node === null) { - return - } - - const anchor = propsAnchor || createAnchorFromText(node.textContent) - setElementId(anchor) - if (window.location.hash.slice(1) === anchor) { - setTarget(node) - } - }, - [propsAnchor] - ) - - return React.createElement( - tag, - { ref, id: elementId }, - - {elementId && ( - - - - )} - {children} - - ) -} - -function CustomHeading({ level, children }: CustomHeadingProps): ReactElement { - return {children} -} - -function CustomParsedHtml(props: CustomParsedHtmlProps): ReactElement { - const { - element: { type, props: elementProps }, - } = props - - const headingElements = ["h1", "h2", "h3", "h4", "h5", "h6"] - if (!headingElements.includes(type)) { - // casting to any because ReactMarkdown's types are funky - // but this just means "call the original renderer provided by ReactMarkdown" - return (ReactMarkdown.renderers.parsedHtml as any)(props) - } - - const { "data-anchor": anchor, children } = elementProps - return ( - - {children} - - ) -} - /** * Wraps the component to include our standard * renderers and AST plugins (for syntax highlighting, HTML support, etc). @@ -200,8 +71,6 @@ class StreamlitMarkdown extends PureComponent { math: (props: { value: string }): ReactElement => ( {props.value} ), - heading: CustomHeading, - parsedHtml: CustomParsedHtml, } const plugins = [RemarkMathPlugin, RemarkEmoji] @@ -236,13 +105,8 @@ interface LinkReferenceProps { // Using target="_blank" without rel="noopener noreferrer" is a security risk: // see https://mathiasbynens.github.io/rel-noopener export function linkWithTargetBlank(props: LinkProps): ReactElement { - // if it's a #hash link, don't open in new tab - if (props.href.startsWith("#")) { - const { children, ...rest } = props - return {children} - } - const { href, title, children } = props + return ( {children} diff --git a/frontend/src/components/shared/StreamlitMarkdown/styled-components.ts b/frontend/src/components/shared/StreamlitMarkdown/styled-components.ts index e60b858feb4d..4636326fd8c7 100644 --- a/frontend/src/components/shared/StreamlitMarkdown/styled-components.ts +++ b/frontend/src/components/shared/StreamlitMarkdown/styled-components.ts @@ -23,23 +23,3 @@ export const StyledStreamlitMarkdown = styled.div(({ theme }) => ({ border: `1px solid ${theme.colors.lightGray}`, }, })) - -export const StyledLinkIconContainer = styled.div(({ theme }) => ({ - position: "relative", - left: "-30px", - paddingLeft: "30px", - a: { - display: "none", - }, - ":hover": { - a: { - display: "inline-block", - }, - }, -})) - -export const StyledLinkIcon = styled.a(({ theme }) => ({ - position: "absolute", - top: "-2px", - left: 0, -})) diff --git a/lib/streamlit/elements/markdown.py b/lib/streamlit/elements/markdown.py index ec74c1f574fc..6b57b3096ba3 100644 --- a/lib/streamlit/elements/markdown.py +++ b/lib/streamlit/elements/markdown.py @@ -78,7 +78,7 @@ def markdown(self, body, unsafe_allow_html=False): return self.dg._enqueue("markdown", markdown_proto) - def header(self, body, anchor=None): + def header(self, body): """Display text in header formatting. Parameters @@ -86,10 +86,6 @@ def header(self, body, anchor=None): body : str The text to display. - anchor : str - The anchor name of the header that can be accessed with #anchor - in the URL. If omitted, it generates an anchor using the body. - Example ------- >>> st.header('This is a header') @@ -100,14 +96,10 @@ def header(self, body, anchor=None): """ header_proto = MarkdownProto() - if anchor is None: - header_proto.body = f"## {clean_text(body)}" - else: - header_proto.body = f'

{clean_text(body)}

' - header_proto.allow_html = True + header_proto.body = "## %s" % clean_text(body) return self.dg._enqueue("markdown", header_proto) - def subheader(self, body, anchor=None): + def subheader(self, body): """Display text in subheader formatting. Parameters @@ -115,10 +107,6 @@ def subheader(self, body, anchor=None): body : str The text to display. - anchor : str - The anchor name of the header that can be accessed with #anchor - in the URL. If omitted, it generates an anchor using the body. - Example ------- >>> st.subheader('This is a subheader') @@ -129,12 +117,7 @@ def subheader(self, body, anchor=None): """ subheader_proto = MarkdownProto() - if anchor is None: - subheader_proto.body = f"### {clean_text(body)}" - else: - subheader_proto.body = f'

{clean_text(body)}

' - subheader_proto.allow_html = True - + subheader_proto.body = "### %s" % clean_text(body) return self.dg._enqueue("markdown", subheader_proto) def code(self, body, language="python"): @@ -170,7 +153,7 @@ def code(self, body, language="python"): code_proto.body = clean_text(markdown) return self.dg._enqueue("markdown", code_proto) - def title(self, body, anchor=None): + def title(self, body): """Display text in title formatting. Each document should have a single `st.title()`, although this is not @@ -181,10 +164,6 @@ def title(self, body, anchor=None): body : str The text to display. - anchor : str - The anchor name of the header that can be accessed with #anchor - in the URL. If omitted, it generates an anchor using the body. - Example ------- >>> st.title('This is a title') @@ -195,11 +174,7 @@ def title(self, body, anchor=None): """ title_proto = MarkdownProto() - if anchor is None: - title_proto.body = f"# {clean_text(body)}" - else: - title_proto.body = f'

{clean_text(body)}

' - title_proto.allow_html = True + title_proto.body = "# %s" % clean_text(body) return self.dg._enqueue("markdown", title_proto) def latex(self, body): diff --git a/lib/tests/streamlit/streamlit_test.py b/lib/tests/streamlit/streamlit_test.py index a708d186d667..fd55b570a0df 100644 --- a/lib/tests/streamlit/streamlit_test.py +++ b/lib/tests/streamlit/streamlit_test.py @@ -292,15 +292,6 @@ def test_st_header(self): el = self.get_delta_from_queue().new_element self.assertEqual(el.markdown.body, "## some header") - def test_st_header_with_anchor(self): - """Test st.header with anchor.""" - st.header("some header", anchor="some-anchor") - - el = self.get_delta_from_queue().new_element - self.assertEqual( - el.markdown.body, '

some header

' - ) - def test_st_help(self): """Test st.help.""" st.help(st.header) @@ -312,7 +303,7 @@ def test_st_help(self): el.doc_string.doc_string.startswith("Display text in header formatting.") ) self.assertEqual(el.doc_string.type, "") - self.assertEqual(el.doc_string.signature, "(body, anchor=None)") + self.assertEqual(el.doc_string.signature, "(body)") def test_st_image_PIL_image(self): """Test st.image with PIL image.""" @@ -591,15 +582,6 @@ def test_st_subheader(self): el = self.get_delta_from_queue().new_element self.assertEqual(el.markdown.body, "### some subheader") - def test_st_subheader_with_anchor(self): - """Test st.subheader with anchor.""" - st.subheader("some subheader", anchor="some-anchor") - - el = self.get_delta_from_queue().new_element - self.assertEqual( - el.markdown.body, '

some subheader

' - ) - def test_st_success(self): """Test st.success.""" st.success("some success") @@ -635,15 +617,6 @@ def test_st_title(self): el = self.get_delta_from_queue().new_element self.assertEqual(el.markdown.body, "# some title") - def test_st_title_with_anchor(self): - """Test st.title with anchor.""" - st.title("some title", anchor="some-anchor") - - el = self.get_delta_from_queue().new_element - self.assertEqual( - el.markdown.body, '

some title

' - ) - def test_st_vega_lite_chart(self): """Test st.vega_lite_chart.""" pass