Skip to content

Commit

Permalink
Upgrade react-markdown to version 6 (#4221)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ken McGrady committed Jan 14, 2022
1 parent 3fe96b2 commit be79768
Show file tree
Hide file tree
Showing 16 changed files with 795 additions and 503 deletions.
12 changes: 9 additions & 3 deletions e2e/specs/st_caption.spec.js
Expand Up @@ -21,19 +21,25 @@ describe("st.caption", () => {
});

it("displays correct number of elements", () => {
cy.get(".element-container .stMarkdown small").should("have.length", 6);
cy.get(
".element-container .stMarkdown [data-testid='stCaptionContainer']"
).should("have.length", 6);
});

it("matches snapshots", () => {
cy.get(".element-container .stMarkdown small").then(els => {
cy.get(
".element-container .stMarkdown [data-testid='stCaptionContainer']"
).then(els => {
cy.wrap(els.slice(1)).each((el, i) => {
return cy.wrap(el).matchThemedSnapshots(`caption-${i}`);
});
});
});

it("displays correct content inside caption", () => {
cy.get(".element-container .stMarkdown small").then(els => {
cy.get(
".element-container .stMarkdown [data-testid='stCaptionContainer']"
).then(els => {
expect(els[1].textContent).to.eq("This is a caption!");
expect(els[2].textContent).to.eq(
"This is a caption that contains markdown inside it!"
Expand Down
2 changes: 1 addition & 1 deletion e2e/specs/st_markdown.spec.js
Expand Up @@ -34,7 +34,7 @@ describe("st.markdown", () => {
expect(els[2].textContent).to.eq("This HTML tag is not escaped!");
expect(els[3].textContent).to.eq("[text]");
expect(els[4].textContent).to.eq("link");
expect(els[5].textContent).to.eq("");
expect(els[5].textContent).to.eq("[][]");
expect(els[6].textContent).to.eq("Inline math with KaTeX\\KaTeXKATE​X");
expect(els[7].textContent).to.eq(
"ax2+bx+c=0ax^2 + bx + c = 0ax2+bx+c=0"
Expand Down
6 changes: 3 additions & 3 deletions e2e/specs/st_tooltips.spec.js
Expand Up @@ -15,16 +15,16 @@
* limitations under the License.
*/

const defaultTooltip = `This is a really long tooltip.Lorem ipsum dolor sit am\
const defaultTooltip = `This is a really long tooltip.\nLorem ipsum dolor sit am\
et, consectetur adipiscing elit. Ut ut turpis vitae\njusto ornare venenatis a \
vitae leo. Donec mollis ornare ante, eu ultricies\ntellus ornare eu. Donec ero\
s risus, ultrices ut eleifend vel, auctor eu turpis.\nIn consectetur erat vel \
ante accumsan, a egestas urna aliquet. Nullam eget\nsapien eget diam euismod e\
leifend. Nulla purus enim, finibus ut velit eu,\nmalesuada dictum nulla. In no\
n arcu et risus maximus fermentum eget nec ante.`;

const tooltipCodeBlock1 = `This\nis\na\ncode\nblock!`;
const tooltipCodeBlock2 = `for i in range(10):\n x = i * 10\n print(x)`;
const tooltipCodeBlock1 = `This\nis\na\ncode\nblock!\n`;
const tooltipCodeBlock2 = `for i in range(10):\n x = i * 10\n print(x)\n`;

const tooltipTextBlock1 = `This is a regular text block!\nTest1\nTest2`;
const tooltipTextBlock2 = `thisisatooltipwithnoindents. It has some spaces but\
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 9 additions & 5 deletions frontend/package.json
Expand Up @@ -41,7 +41,6 @@
"@loaders.gl/core": "^2.2.8",
"@loaders.gl/csv": "^2.2.8",
"@loaders.gl/gltf": "^2.2.8",
"@matejmazur/react-katex": "^3.1.3",
"apache-arrow": "https://github.com/streamlit/arrow-js#af9f267084d7829033e7e10f79587b49df4f9806",
"axios": "^0.21.4",
"baseui": "9.106.0",
Expand All @@ -64,7 +63,7 @@
"immutable": "^4.0.0-rc.12",
"jest-fetch-mock": "^3.0.3",
"json5": "^2.2.0",
"katex": "^0.11.1",
"katex": "^0.13.24",
"lodash": "^4.17.21",
"mapbox-gl": "^1.11.1",
"moment": "^2.27.0",
Expand All @@ -88,14 +87,18 @@
"react-html-parser": "^2.0.2",
"react-json-view": "^1.19.1",
"react-map-gl": "^5.2.7",
"react-markdown": "^4.3.1",
"react-markdown": "^6.0.3",
"react-plotly.js": "^2.4.0",
"react-syntax-highlighter": "^15.4.5",
"react-transition-group": "^4.4.1",
"react-virtualized": "^9.21.2",
"react-webcam": "^6.0.0",
"react-window": "^1.8.5",
"remark-emoji": "^2.1.0",
"remark-math": "^2.0.1",
"rehype-katex": "^5.0.0",
"rehype-raw": "^5.1.0",
"remark-emoji": "^2.2.0",
"remark-gfm": "^1.0.0",
"remark-math": "^4.0.0",
"sass": "^1.43.4",
"sprintf-js": "^1.1.2",
"styletron-engine-atomic": "^1.4.6",
Expand Down Expand Up @@ -136,6 +139,7 @@
"@types/react-html-parser": "^2.0.1",
"@types/react-map-gl": "^5.2.6",
"@types/react-plotly.js": "^2.2.4",
"@types/react-syntax-highlighter": "^13.5.2",
"@types/react-transition-group": "^4.4.1",
"@types/react-virtualized": "^9.21.11",
"@types/react-window": "^1.8.3",
Expand Down
31 changes: 0 additions & 31 deletions frontend/src/components/core/Sidebar/styled-components.ts
Expand Up @@ -55,10 +55,6 @@ export interface StyledSidebarContentProps {
isCollapsed: boolean
}

function convertRemToEm(s: string): string {
return s.replace(/rem$/, "em")
}

export const StyledSidebarContent = styled.div<StyledSidebarContentProps>(
({ isCollapsed, theme }) => ({
backgroundColor: theme.colors.bgColor,
Expand Down Expand Up @@ -113,33 +109,6 @@ export const StyledSidebarContent = styled.div<StyledSidebarContentProps>(
fontSize: theme.fontSizes.twoSm,
fontWeight: 600,
},

small: {
color: theme.colors.gray,
fontSize: theme.fontSizes.sm,
"p, ol, ul, dl, li": {
fontSize: "inherit",
},

"h1, h2, h3, h4, h5, h6": {
color: "inherit",
},

// sizes taken from default styles, but using em instead of rem, so it
// inherits the <small>'s shrunk size
h1: {
fontSize: convertRemToEm(theme.fontSizes.xl),
},
h2: {
fontSize: convertRemToEm(theme.fontSizes.lg),
},
h3: {
fontSize: "1.125em",
},
"h4,h5,h6": {
fontSize: "1em",
},
},
})
)

Expand Down
73 changes: 34 additions & 39 deletions frontend/src/components/elements/CodeBlock/CodeBlock.test.tsx
Expand Up @@ -17,79 +17,74 @@

import React from "react"
import { mount, shallow } from "src/lib/test_util"
import { logWarning } from "src/lib/log"
import CodeBlock, { CodeBlockProps } from "./CodeBlock"
import CodeBlock, { CodeTag, CodeTagProps, CodeBlockProps } from "./CodeBlock"
import CopyButton from "./CopyButton"

jest.mock("src/lib/log", () => ({
logWarning: jest.fn(),
logMessage: jest.fn(),
}))
const getTagProps = (props: Partial<CodeTagProps> = {}): CodeTagProps => {
const defaultChildren = [
`
import streamlit as st
st.write("Hello")
`,
]
return {
node: { type: "element", tagName: "code", children: [] },
key: "example",
inline: false,
className: "language-python",
children: defaultChildren,
...props,
}
}

const getProps = (props: Partial<CodeBlockProps> = {}): CodeBlockProps => ({
value: `
const getBlockProps = (
props: Partial<CodeBlockProps> = {}
): CodeBlockProps => ({
children: [
`
import streamlit as st
st.write("Hello")
`,
],
...props,
})

describe("CodeBlock Element", () => {
it("should render without crashing", () => {
const props = getProps()
const props = getBlockProps()
const wrapper = shallow(<CodeBlock {...props} />)

expect(wrapper.find("StyledCodeBlock").length).toBe(1)
})

it("should render with language", () => {
const props = getProps({
language: "python",
})
const props = getTagProps()
const wrapper = mount(<CodeBlock {...props} />)

expect(wrapper.find("StyledCodeBlock").length).toBe(1)
expect(wrapper.find("code").prop("className")).toBe("language-python")
})

it("should default to python if no language specified", () => {
const props = getProps()
const wrapper = mount(<CodeBlock {...props} />)
expect(logWarning).toHaveBeenCalledWith(
"No language provided, defaulting to Python"
)
expect(wrapper.find("code").prop("className")).toBe("language-python")
const props = getTagProps()
const wrapper = mount(<CodeTag {...props} />)
expect(wrapper.find("SyntaxHighlighter").prop("language")).toBe("python")
})

it("should render copy button when code block has content", () => {
const props = getProps({
value: "i am not empty",
language: null,
const props = getTagProps({
children: ["i am not empty"],
})
const wrapper = mount(<CodeBlock {...props} />)
const wrapper = mount(<CodeTag {...props} />)
expect(wrapper.find(CopyButton)).toHaveLength(1)
})

it("should not render copy button when code block is empty", () => {
const props = getProps({
value: "",
const props = getBlockProps({
children: [""],
})
const wrapper = mount(<CodeBlock {...props} />)
expect(wrapper.find(CopyButton)).toHaveLength(0)
})

it("should warn if there's no highlight available", () => {
// @ts-ignore
global.console = { warn: jest.fn() }

const props = getProps({
language: "CoffeeScript",
})
const wrapper = mount(<CodeBlock {...props} />)
expect(logWarning).toHaveBeenCalledWith(
"No syntax highlighting for CoffeeScript."
)
expect(wrapper.find("code").prop("className")).toBeUndefined()
})
})
109 changes: 45 additions & 64 deletions frontend/src/components/elements/CodeBlock/CodeBlock.tsx
Expand Up @@ -15,23 +15,9 @@
* limitations under the License.
*/

import Prism, { Grammar } from "prismjs"
import React, { ReactElement } from "react"

// Prism language definition files.
// These must come after the prismjs import because they modify Prism.languages
import "prismjs/components/prism-bash"
import "prismjs/components/prism-c"
import "prismjs/components/prism-css"
import "prismjs/components/prism-json"
import "prismjs/components/prism-jsx"
import "prismjs/components/prism-python"
import "prismjs/components/prism-sql"
import "prismjs/components/prism-toml"
import "prismjs/components/prism-typescript"
import "prismjs/components/prism-yaml"

import { logWarning } from "src/lib/log"
import React, { ReactElement, ReactNode, FunctionComponent } from "react"
import { ReactMarkdownProps } from "react-markdown/src/ast-to-react"
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"

import CopyButton from "./CopyButton"
import {
Expand All @@ -40,64 +26,59 @@ import {
StyledCopyButtonContainer,
} from "./styled-components"

interface CodeTagProps {
language?: string | null
value: string
}
export type CodeTagProps = JSX.IntrinsicElements["code"] &
ReactMarkdownProps & { inline?: boolean }

export interface CodeBlockProps extends CodeTagProps {}
export interface CodeBlockProps {
node?: ReactNode
children: ReactNode
}

/**
* Renders code tag with highlighting based on requested language.
*/
function CodeTag({ language, value }: CodeTagProps): ReactElement {
// language is explicitly null; don't highlight
if (language === null) {
return <code>{value}</code>
}

// no language provided; we'll default to python
if (language === undefined) {
logWarning(`No language provided, defaulting to Python`)
}
export const CodeTag: FunctionComponent<CodeTagProps> = ({
node,
inline,
className,
children,
...props
}) => {
const match = /language-(\w+)/.exec(className || "")
const codeText = String(children)
.trim()
.replace(/\n$/, "")

const languageKey = (language || "python").toLowerCase()

// language provided, but not supported; don't highlight
const lang: Grammar = Prism.languages[languageKey]
if (!lang) {
logWarning(`No syntax highlighting for ${language}.`)
return <code>{value}</code>
}

// language provided & supported; return highlighted code
return (
<code
className={`language-${languageKey}`}
dangerouslySetInnerHTML={{
__html: value && Prism.highlight(value, lang, ""),
}}
/>
return !inline ? (
<>
{codeText && (
<StyledCopyButtonContainer>
<CopyButton text={codeText} />
</StyledCopyButtonContainer>
)}
<StyledPre>
<SyntaxHighlighter
language={(match && match[1]) || ""}
PreTag="div"
customStyle={{ backgroundColor: "transparent" }}
style={{}}
>
{codeText}
</SyntaxHighlighter>
</StyledPre>
</>
) : (
<code className={className} {...props}>
{children}
</code>
)
}

/**
* Renders a code block with syntax highlighting, via Prismjs
*/
export default function CodeBlock({
language,
value,
}: CodeBlockProps): ReactElement {
return (
<StyledCodeBlock className="stCodeBlock">
{value && (
<StyledCopyButtonContainer>
<CopyButton text={value} />
</StyledCopyButtonContainer>
)}
<StyledPre>
<CodeTag language={language} value={value} />
</StyledPre>
</StyledCodeBlock>
)
children,
}: Record<any, any>): ReactElement {
return <StyledCodeBlock className="stCodeBlock">{children}</StyledCodeBlock>
}

0 comments on commit be79768

Please sign in to comment.