refactor(syntax-highlighting): use component wrapping for syntax highlighting activation (#9784)

This commit is contained in:
Vladimír Gorej
2024-04-06 00:16:00 +02:00
committed by GitHub
parent 7260005bd8
commit ac0d2a3cc8
15 changed files with 143 additions and 112 deletions

View File

@@ -1,31 +1,19 @@
import React from "react" import React from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
import { CopyToClipboard } from "react-copy-to-clipboard" import { CopyToClipboard } from "react-copy-to-clipboard"
import get from "lodash/get"
import { requestSnippetGenerator_curl_bash } from "../plugins/request-snippets/fn" import { requestSnippetGenerator_curl_bash } from "../plugins/request-snippets/fn"
export default class Curl extends React.Component { export default class Curl extends React.Component {
static propTypes = { static propTypes = {
getConfigs: PropTypes.func.isRequired,
getComponent: PropTypes.func.isRequired, getComponent: PropTypes.func.isRequired,
request: PropTypes.object.isRequired request: PropTypes.object.isRequired
} }
render() { render() {
let { request, getConfigs, getComponent } = this.props const { request, getComponent } = this.props
let curl = requestSnippetGenerator_curl_bash(request) const curl = requestSnippetGenerator_curl_bash(request)
const config = getConfigs()
const SyntaxHighlighter = getComponent("SyntaxHighlighter", true) const SyntaxHighlighter = getComponent("SyntaxHighlighter", true)
const curlBlock = get(config, "syntaxHighlight.activated") ? (
<SyntaxHighlighter language="bash" className="curl microlight">
{curl}
</SyntaxHighlighter>
) : (
<textarea readOnly={true} className="curl" value={curl}></textarea>
)
return ( return (
<div className="curl-command"> <div className="curl-command">
<h4>Curl</h4> <h4>Curl</h4>
@@ -33,7 +21,15 @@ export default class Curl extends React.Component {
<CopyToClipboard text={curl}><button/></CopyToClipboard> <CopyToClipboard text={curl}><button/></CopyToClipboard>
</div> </div>
<div> <div>
{curlBlock} <SyntaxHighlighter
language="bash"
className="curl microlight"
renderPlainText={({ children, PlainTextViewer }) => (
<PlainTextViewer className="curl">{children}</PlainTextViewer>
)}
>
{curl}
</SyntaxHighlighter>
</div> </div>
</div> </div>
) )

View File

@@ -8,7 +8,7 @@ import ImPropTypes from "react-immutable-proptypes"
import { stringify } from "core/utils" import { stringify } from "core/utils"
export default function Example(props) { export default function Example(props) {
const { example, showValue, getComponent, getConfigs } = props const { example, showValue, getComponent } = props
const Markdown = getComponent("Markdown", true) const Markdown = getComponent("Markdown", true)
const HighlightCode = getComponent("HighlightCode", true) const HighlightCode = getComponent("HighlightCode", true)
@@ -28,10 +28,7 @@ export default function Example(props) {
{showValue && example.has("value") ? ( {showValue && example.has("value") ? (
<section className="example__section"> <section className="example__section">
<div className="example__section-header">Example Value</div> <div className="example__section-header">Example Value</div>
<HighlightCode <HighlightCode>{stringify(example.get("value"))}</HighlightCode>
getConfigs={getConfigs}
value={stringify(example.get("value"))}
/>
</section> </section>
) : null} ) : null}
</div> </div>
@@ -42,5 +39,4 @@ Example.propTypes = {
example: ImPropTypes.map.isRequired, example: ImPropTypes.map.isRequired,
showValue: PropTypes.bool, showValue: PropTypes.bool,
getComponent: PropTypes.func.isRequired, getComponent: PropTypes.func.isRequired,
getConfigs: PropTypes.func.getConfigs,
} }

View File

@@ -113,10 +113,7 @@ const ModelExample = ({
{example ? ( {example ? (
example example
) : ( ) : (
<HighlightCode <HighlightCode>(no example available</HighlightCode>
value="(no example available)"
getConfigs={getConfigs}
/>
)} )}
</div> </div>
)} )}

View File

@@ -15,7 +15,6 @@ export default class ParamBody extends PureComponent {
consumes: PropTypes.object, consumes: PropTypes.object,
consumesValue: PropTypes.string, consumesValue: PropTypes.string,
fn: PropTypes.object.isRequired, fn: PropTypes.object.isRequired,
getConfigs: PropTypes.func.isRequired,
getComponent: PropTypes.func.isRequired, getComponent: PropTypes.func.isRequired,
isExecute: PropTypes.bool, isExecute: PropTypes.bool,
specSelectors: PropTypes.object.isRequired, specSelectors: PropTypes.object.isRequired,
@@ -98,7 +97,6 @@ export default class ParamBody extends PureComponent {
isExecute, isExecute,
specSelectors, specSelectors,
pathMethod, pathMethod,
getConfigs,
getComponent, getComponent,
} = this.props } = this.props
@@ -127,10 +125,7 @@ export default class ParamBody extends PureComponent {
{ {
isEditBox && isExecute isEditBox && isExecute
? <TextArea className={ "body-param__text" + ( errors.count() ? " invalid" : "")} value={value} onChange={ this.handleOnChange }/> ? <TextArea className={ "body-param__text" + ( errors.count() ? " invalid" : "")} value={value} onChange={ this.handleOnChange }/>
: (value && <HighlightCode className="body-param__example" : (value && <HighlightCode className="body-param__example" language={ language }>{value}</HighlightCode>)
language={ language }
getConfigs={ getConfigs }
value={ value }/>)
} }
<div className="body-param-options"> <div className="body-param-options">
{ {

View File

@@ -14,7 +14,6 @@ export default class ResponseBody extends React.PureComponent {
static propTypes = { static propTypes = {
content: PropTypes.any.isRequired, content: PropTypes.any.isRequired,
contentType: PropTypes.string, contentType: PropTypes.string,
getConfigs: PropTypes.func.isRequired,
getComponent: PropTypes.func.isRequired, getComponent: PropTypes.func.isRequired,
headers: PropTypes.object, headers: PropTypes.object,
url: PropTypes.string url: PropTypes.string
@@ -51,7 +50,7 @@ export default class ResponseBody extends React.PureComponent {
} }
render() { render() {
let { content, contentType, url, headers={}, getConfigs, getComponent } = this.props let { content, contentType, url, headers={}, getComponent } = this.props
const { parsedContent } = this.state const { parsedContent } = this.state
const HighlightCode = getComponent("HighlightCode", true) const HighlightCode = getComponent("HighlightCode", true)
const downloadName = "response_" + new Date().getTime() const downloadName = "response_" + new Date().getTime()
@@ -108,7 +107,7 @@ export default class ResponseBody extends React.PureComponent {
body = "can't parse JSON. Raw result:\n\n" + content body = "can't parse JSON. Raw result:\n\n" + content
} }
bodyEl = <HighlightCode language={language} downloadable fileName={`${downloadName}.json`} value={ body } getConfigs={ getConfigs } canCopy /> bodyEl = <HighlightCode language={language} downloadable fileName={`${downloadName}.json`} canCopy>{body}</HighlightCode>
// XML // XML
} else if (/xml/i.test(contentType)) { } else if (/xml/i.test(contentType)) {
@@ -116,15 +115,15 @@ export default class ResponseBody extends React.PureComponent {
textNodesOnSameLine: true, textNodesOnSameLine: true,
indentor: " " indentor: " "
}) })
bodyEl = <HighlightCode downloadable fileName={`${downloadName}.xml`} value={ body } getConfigs={ getConfigs } canCopy /> bodyEl = <HighlightCode downloadable fileName={`${downloadName}.xml`} canCopy>{body}</HighlightCode>
// HTML or Plain Text // HTML or Plain Text
} else if (toLower(contentType) === "text/html" || /text\/plain/.test(contentType)) { } else if (toLower(contentType) === "text/html" || /text\/plain/.test(contentType)) {
bodyEl = <HighlightCode downloadable fileName={`${downloadName}.html`} value={ content } getConfigs={ getConfigs } canCopy /> bodyEl = <HighlightCode downloadable fileName={`${downloadName}.html`} canCopy>{content}</HighlightCode>
// CSV // CSV
} else if (toLower(contentType) === "text/csv" || /text\/csv/.test(contentType)) { } else if (toLower(contentType) === "text/csv" || /text\/csv/.test(contentType)) {
bodyEl = <HighlightCode downloadable fileName={`${downloadName}.csv`} value={ content } getConfigs={ getConfigs } canCopy /> bodyEl = <HighlightCode downloadable fileName={`${downloadName}.csv`} canCopy>{content}</HighlightCode>
// Image // Image
} else if (/^image\//i.test(contentType)) { } else if (/^image\//i.test(contentType)) {
@@ -138,7 +137,7 @@ export default class ResponseBody extends React.PureComponent {
} else if (/^audio\//i.test(contentType)) { } else if (/^audio\//i.test(contentType)) {
bodyEl = <pre className="microlight"><audio controls key={ url }><source src={ url } type={ contentType } /></audio></pre> bodyEl = <pre className="microlight"><audio controls key={ url }><source src={ url } type={ contentType } /></audio></pre>
} else if (typeof content === "string") { } else if (typeof content === "string") {
bodyEl = <HighlightCode downloadable fileName={`${downloadName}.txt`} value={ content } getConfigs={ getConfigs } canCopy /> bodyEl = <HighlightCode downloadable fileName={`${downloadName}.txt`} canCopy>{content}</HighlightCode>
} else if ( content.size > 0 ) { } else if ( content.size > 0 ) {
// We don't know the contentType, but there was some content returned // We don't know the contentType, but there was some content returned
if(parsedContent) { if(parsedContent) {
@@ -148,7 +147,7 @@ export default class ResponseBody extends React.PureComponent {
<p className="i"> <p className="i">
Unrecognized response type; displaying content as text. Unrecognized response type; displaying content as text.
</p> </p>
<HighlightCode downloadable fileName={`${downloadName}.txt`} value={ parsedContent } getConfigs={ getConfigs } canCopy /> <HighlightCode downloadable fileName={`${downloadName}.txt`} canCopy>{parsedContent}</HighlightCode>
</div> </div>
} else { } else {

View File

@@ -7,7 +7,7 @@ import { getExtensions, fromJSOrdered, stringify } from "core/utils"
import { getKnownSyntaxHighlighterLanguage } from "core/utils/jsonParse" import { getKnownSyntaxHighlighterLanguage } from "core/utils/jsonParse"
const getExampleComponent = ( sampleResponse, HighlightCode, getConfigs ) => { const getExampleComponent = ( sampleResponse, HighlightCode ) => {
if (sampleResponse == null) return null if (sampleResponse == null) return null
const testValueForJson = getKnownSyntaxHighlighterLanguage(sampleResponse) const testValueForJson = getKnownSyntaxHighlighterLanguage(sampleResponse)
@@ -15,7 +15,7 @@ const getExampleComponent = ( sampleResponse, HighlightCode, getConfigs ) => {
return ( return (
<div> <div>
<HighlightCode className="example" getConfigs={ getConfigs } language={ language } value={ stringify(sampleResponse) } /> <HighlightCode className="example" language={language}>{stringify(sampleResponse)}</HighlightCode>
</div> </div>
) )
} }
@@ -167,7 +167,7 @@ export default class Response extends React.Component {
shouldOverrideSchemaExample ? mediaTypeExample : undefined shouldOverrideSchemaExample ? mediaTypeExample : undefined
) )
const example = getExampleComponent( sampleResponse, HighlightCode, getConfigs ) const example = getExampleComponent( sampleResponse, HighlightCode )
return ( return (
<tr className={ "response " + ( className || "") } data-code={code}> <tr className={ "response " + ( className || "") } data-code={code}>

View File

@@ -289,12 +289,9 @@ const RequestBody = ({
schema={mediaTypeValue.get("schema")} schema={mediaTypeValue.get("schema")}
specPath={specPath.push("content", contentType)} specPath={specPath.push("content", contentType)}
example={ example={
<HighlightCode <HighlightCode className="body-param__example" language={language}>
className="body-param__example" {stringify(requestBodyValue) || sampleRequestBody}
getConfigs={getConfigs} </HighlightCode>
language={language}
value={stringify(requestBodyValue) || sampleRequestBody}
/>
} }
includeWriteOnly={true} includeWriteOnly={true}
/> />

View File

@@ -1,7 +1,5 @@
import React, { useRef, useEffect, useState } from "react" import React, { useRef, useEffect, useState } from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
import get from "lodash/get"
import isFunction from "lodash/isFunction"
import { CopyToClipboard } from "react-copy-to-clipboard" import { CopyToClipboard } from "react-copy-to-clipboard"
const style = { const style = {
@@ -34,9 +32,7 @@ const activeStyle = {
borderBottom: "none" borderBottom: "none"
} }
const RequestSnippets = ({ request, requestSnippetsSelectors, getConfigs, getComponent }) => { const RequestSnippets = ({ request, requestSnippetsSelectors, getComponent }) => {
const config = isFunction(getConfigs) ? getConfigs() : null
const canSyntaxHighlight = get(config, "syntaxHighlight") !== false && get(config, "syntaxHighlight.activated", true)
const rootRef = useRef(null) const rootRef = useRef(null)
const ArrowIcon = getComponent("ArrowUpIcon") const ArrowIcon = getComponent("ArrowUpIcon")
@@ -45,24 +41,6 @@ const RequestSnippets = ({ request, requestSnippetsSelectors, getConfigs, getCom
const [activeLanguage, setActiveLanguage] = useState(requestSnippetsSelectors.getSnippetGenerators()?.keySeq().first()) const [activeLanguage, setActiveLanguage] = useState(requestSnippetsSelectors.getSnippetGenerators()?.keySeq().first())
const [isExpanded, setIsExpanded] = useState(requestSnippetsSelectors?.getDefaultExpanded()) const [isExpanded, setIsExpanded] = useState(requestSnippetsSelectors?.getDefaultExpanded())
useEffect(() => {
const doIt = () => {
}
doIt()
}, [])
useEffect(() => {
const childNodes = Array
.from(rootRef.current.childNodes)
.filter(node => !!node.nodeType && node.classList?.contains("curl-command"))
// eslint-disable-next-line no-use-before-define
childNodes.forEach(node => node.addEventListener("mousewheel", handlePreventYScrollingBeyondElement, { passive: false }))
return () => {
// eslint-disable-next-line no-use-before-define
childNodes.forEach(node => node.removeEventListener("mousewheel", handlePreventYScrollingBeyondElement))
}
}, [request])
const snippetGenerators = requestSnippetsSelectors.getSnippetGenerators() const snippetGenerators = requestSnippetsSelectors.getSnippetGenerators()
const activeGenerator = snippetGenerators.get(activeLanguage) const activeGenerator = snippetGenerators.get(activeLanguage)
@@ -99,16 +77,25 @@ const RequestSnippets = ({ request, requestSnippetsSelectors, getConfigs, getCom
} }
} }
const SnippetComponent = canSyntaxHighlight ? ( useEffect(() => {
<SyntaxHighlighter const doIt = () => {
language={activeGenerator.get("syntax")}
className="curl microlight" }
> doIt()
{snippet} }, [])
</SyntaxHighlighter>
) : ( useEffect(() => {
<textarea readOnly={true} className="curl" value={snippet}></textarea> const childNodes = Array
) .from(rootRef.current.childNodes)
.filter(node => !!node.nodeType && node.classList?.contains("curl-command"))
// eslint-disable-next-line no-use-before-define
childNodes.forEach(node => node.addEventListener("mousewheel", handlePreventYScrollingBeyondElement, { passive: false }))
return () => {
// eslint-disable-next-line no-use-before-define
childNodes.forEach(node => node.removeEventListener("mousewheel", handlePreventYScrollingBeyondElement))
}
}, [request])
return ( return (
<div className="request-snippets" ref={rootRef}> <div className="request-snippets" ref={rootRef}>
@@ -142,7 +129,15 @@ const RequestSnippets = ({ request, requestSnippetsSelectors, getConfigs, getCom
</CopyToClipboard> </CopyToClipboard>
</div> </div>
<div> <div>
{SnippetComponent} <SyntaxHighlighter
language={activeGenerator.get("syntax")}
className="curl microlight"
renderPlainText={({ children, PlainTextViewer }) => (
<PlainTextViewer className="curl">{children}</PlainTextViewer>
)}
>
{snippet}
</SyntaxHighlighter>
</div> </div>
</div> </div>
} }
@@ -153,7 +148,6 @@ const RequestSnippets = ({ request, requestSnippetsSelectors, getConfigs, getCom
RequestSnippets.propTypes = { RequestSnippets.propTypes = {
request: PropTypes.object.isRequired, request: PropTypes.object.isRequired,
requestSnippetsSelectors: PropTypes.object.isRequired, requestSnippetsSelectors: PropTypes.object.isRequired,
getConfigs: PropTypes.object.isRequired,
getComponent: PropTypes.func.isRequired, getComponent: PropTypes.func.isRequired,
requestSnippetsActions: PropTypes.object, requestSnippetsActions: PropTypes.object,
} }

View File

@@ -4,30 +4,23 @@
import React, { useRef, useEffect } from "react" import React, { useRef, useEffect } from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
import classNames from "classnames" import classNames from "classnames"
import get from "lodash/get"
import saveAs from "js-file-download" import saveAs from "js-file-download"
import { CopyToClipboard } from "react-copy-to-clipboard" import { CopyToClipboard } from "react-copy-to-clipboard"
const HighlightCode = ({ const HighlightCode = ({
value,
fileName = "response.txt", fileName = "response.txt",
className, className,
downloadable, downloadable,
getConfigs,
getComponent, getComponent,
canCopy, canCopy,
language, language,
children,
}) => { }) => {
const config = getConfigs()
const canSyntaxHighlight =
get(config, "syntaxHighlight") !== false &&
get(config, "syntaxHighlight.activated", true)
const rootRef = useRef(null) const rootRef = useRef(null)
const SyntaxHighlighter = getComponent("SyntaxHighlighter", true) const SyntaxHighlighter = getComponent("SyntaxHighlighter", true)
const handleDownload = () => { const handleDownload = () => {
saveAs(value, fileName) saveAs(children, fileName)
} }
const handlePreventYScrollingBeyondElement = (e) => { const handlePreventYScrollingBeyondElement = (e) => {
@@ -70,13 +63,13 @@ const HighlightCode = ({
) )
) )
} }
}, [value, className, language]) }, [children, className, language])
return ( return (
<div className="highlight-code" ref={rootRef}> <div className="highlight-code" ref={rootRef}>
{canCopy && ( {canCopy && (
<div className="copy-to-clipboard"> <div className="copy-to-clipboard">
<CopyToClipboard text={value}> <CopyToClipboard text={children}>
<button /> <button />
</CopyToClipboard> </CopyToClipboard>
</div> </div>
@@ -88,29 +81,27 @@ const HighlightCode = ({
</button> </button>
)} )}
{canSyntaxHighlight ? (
<SyntaxHighlighter <SyntaxHighlighter
language={language} language={language}
className={classNames(className, "microlight")} className={classNames(className, "microlight")}
> renderPlainText={({ children, PlainTextViewer }) => (
{value} <PlainTextViewer className={className}>{children}</PlainTextViewer>
</SyntaxHighlighter>
) : (
<pre className={classNames(className, "microlight")}>{value}</pre>
)} )}
>
{children}
</SyntaxHighlighter>
</div> </div>
) )
} }
HighlightCode.propTypes = { HighlightCode.propTypes = {
value: PropTypes.string.isRequired,
getConfigs: PropTypes.func.isRequired,
getComponent: PropTypes.func.isRequired, getComponent: PropTypes.func.isRequired,
className: PropTypes.string, className: PropTypes.string,
downloadable: PropTypes.bool, downloadable: PropTypes.bool,
fileName: PropTypes.string, fileName: PropTypes.string,
language: PropTypes.string, language: PropTypes.string,
canCopy: PropTypes.bool, canCopy: PropTypes.bool,
children: PropTypes.string.isRequired,
} }
export default HighlightCode export default HighlightCode

View File

@@ -0,0 +1,17 @@
/**
* @prettier
*/
import React from "react"
import PropTypes from "prop-types"
import classNames from "classnames"
const PlainTextViewer = ({ className = "", children }) => (
<pre className={classNames("microlight", className)}>{children}</pre>
)
PlainTextViewer.propTypes = {
className: PropTypes.string,
children: PropTypes.string.isRequired,
}
export default PlainTextViewer

View File

@@ -11,7 +11,7 @@ const SyntaxHighlighter = ({
className = "", className = "",
getConfigs, getConfigs,
syntaxHighlighting = {}, syntaxHighlighting = {},
children = null, children = "",
}) => { }) => {
const configs = getConfigs() const configs = getConfigs()
const theme = get(configs, "syntaxHighlight.theme") const theme = get(configs, "syntaxHighlight.theme")
@@ -37,7 +37,8 @@ SyntaxHighlighter.propTypes = {
styles: PropTypes.object, styles: PropTypes.object,
defaultStyle: PropTypes.object, defaultStyle: PropTypes.object,
}), }),
children: PropTypes.node, renderPlainText: PropTypes.func,
children: PropTypes.string,
} }
export default SyntaxHighlighter export default SyntaxHighlighter

View File

@@ -5,8 +5,10 @@ import afterLoad from "./after-load"
import { styles, defaultStyle } from "./root-injects" import { styles, defaultStyle } from "./root-injects"
import SyntaxHighlighter from "./components/SyntaxHighlighter" import SyntaxHighlighter from "./components/SyntaxHighlighter"
import HighlightCode from "./components/HighlightCode" import HighlightCode from "./components/HighlightCode"
import PlainTextViewer from "./components/PlainTextViewer"
import SyntaxHighlighterWrapper from "./wrap-components/SyntaxHighlighter"
const SyntaxHighlightingPlugin = () => ({ const SyntaxHighlightingPlugin1 = () => ({
afterLoad, afterLoad,
rootInjects: { rootInjects: {
syntaxHighlighting: { styles, defaultStyle }, syntaxHighlighting: { styles, defaultStyle },
@@ -14,7 +16,19 @@ const SyntaxHighlightingPlugin = () => ({
components: { components: {
SyntaxHighlighter, SyntaxHighlighter,
HighlightCode, HighlightCode,
PlainTextViewer,
}, },
}) })
const SyntaxHighlightingPlugin2 = () => ({
wrapComponents: {
SyntaxHighlighter: SyntaxHighlighterWrapper,
},
})
const SyntaxHighlightingPlugin = () => [
SyntaxHighlightingPlugin1,
SyntaxHighlightingPlugin2,
]
export default SyntaxHighlightingPlugin export default SyntaxHighlightingPlugin

View File

@@ -0,0 +1,33 @@
/**
* @prettier
*/
import React from "react"
import PropTypes from "prop-types"
import get from "lodash/get"
const SyntaxHighlighterWrapper = (Original, system) => {
const SyntaxHighlighter = ({ renderPlainText, children, ...rest }) => {
const configs = system.getConfigs()
const canSyntaxHighlight = !!get(configs, "syntaxHighlight.activated")
const PlainTextViewer = system.getComponent("PlainTextViewer")
if (!canSyntaxHighlight && typeof renderPlainText === "function") {
return renderPlainText({ children, PlainTextViewer })
}
if (!canSyntaxHighlight) {
return <PlainTextViewer>{children}</PlainTextViewer>
}
return <Original {...rest}>{children}</Original>
}
SyntaxHighlighter.propTypes = {
...Original.propTypes,
renderPlainText: PropTypes.func,
children: PropTypes.string.isRequired,
}
return SyntaxHighlighter
}
export default SyntaxHighlighterWrapper

View File

@@ -654,8 +654,9 @@
.curl .curl
{ {
white-space: normal; overflow-y: auto;
max-height: 400px; max-height: 400px;
min-height: 6em;
} }
} }

View File

@@ -33,7 +33,7 @@ describe("<HighlightCode />", () => {
it("should render values in a preformatted element", () => { it("should render values in a preformatted element", () => {
const value = "test text" const value = "test text"
const props = { value, getConfigs: fakeGetConfigs, getComponent: fakeGetComponent } const props = { children: value , getConfigs: fakeGetConfigs, getComponent: fakeGetComponent }
const wrapper = mount(<HighlightCode {...props} />) const wrapper = mount(<HighlightCode {...props} />)
const preTag = wrapper.find("pre") const preTag = wrapper.find("pre")