feat: consolidate syntax highlighting code into standalone plugin (#9783)

This commit is contained in:
Vladimír Gorej
2024-04-05 21:12:25 +02:00
committed by GitHub
parent f844319188
commit 7260005bd8
20 changed files with 231 additions and 113 deletions

View File

@@ -1,32 +1,30 @@
import React from "react"
import PropTypes from "prop-types"
import { CopyToClipboard } from "react-copy-to-clipboard"
import {SyntaxHighlighter, getStyle} from "core/syntax-highlighting"
import get from "lodash/get"
import { requestSnippetGenerator_curl_bash } from "../plugins/request-snippets/fn"
export default class Curl extends React.Component {
static propTypes = {
getConfigs: PropTypes.func.isRequired,
getComponent: PropTypes.func.isRequired,
request: PropTypes.object.isRequired
}
render() {
let { request, getConfigs } = this.props
let { request, getConfigs, getComponent } = this.props
let curl = requestSnippetGenerator_curl_bash(request)
const config = getConfigs()
const SyntaxHighlighter = getComponent("SyntaxHighlighter", true)
const curlBlock = get(config, "syntaxHighlight.activated")
? <SyntaxHighlighter
language="bash"
className="curl microlight"
style={getStyle(get(config, "syntaxHighlight.theme"))}
>
{curl}
</SyntaxHighlighter>
:
const curlBlock = get(config, "syntaxHighlight.activated") ? (
<SyntaxHighlighter language="bash" className="curl microlight">
{curl}
</SyntaxHighlighter>
) : (
<textarea readOnly={true} className="curl" value={curl}></textarea>
)
return (
<div className="curl-command">

View File

@@ -11,9 +11,9 @@ export default function Example(props) {
const { example, showValue, getComponent, getConfigs } = props
const Markdown = getComponent("Markdown", true)
const HighlightCode = getComponent("highlightCode")
const HighlightCode = getComponent("HighlightCode", true)
if(!example) return null
if (!example) return null
return (
<div className="example">
@@ -28,7 +28,10 @@ export default function Example(props) {
{showValue && example.has("value") ? (
<section className="example__section">
<div className="example__section-header">Example Value</div>
<HighlightCode getConfigs={ getConfigs } value={stringify(example.get("value"))} />
<HighlightCode
getConfigs={getConfigs}
value={stringify(example.get("value"))}
/>
</section>
) : null}
</div>

View File

@@ -69,13 +69,14 @@ export default class LiveResponse extends React.Component {
const hasHeaders = returnObject.length !== 0
const Markdown = getComponent("Markdown", true)
const RequestSnippets = getComponent("RequestSnippets", true)
const Curl = getComponent("curl")
const Curl = getComponent("curl", true)
return (
<div>
{ curlRequest && (requestSnippetsEnabled === true || requestSnippetsEnabled === "true"
? <RequestSnippets request={ curlRequest }/>
: <Curl request={ curlRequest } getConfigs={ getConfigs } />) }
: <Curl request={ curlRequest } />
)}
{ url && <div>
<div className="request-url">
<h4>Request URL</h4>

View File

@@ -50,7 +50,7 @@ const ModelExample = ({
}) => {
const { defaultModelRendering, defaultModelExpandDepth } = getConfigs()
const ModelWrapper = getComponent("ModelWrapper")
const HighlightCode = getComponent("highlightCode")
const HighlightCode = getComponent("HighlightCode", true)
const exampleTabId = randomBytes(5).toString("base64")
const examplePanelId = randomBytes(5).toString("base64")
const modelTabId = randomBytes(5).toString("base64")

View File

@@ -104,7 +104,7 @@ export default class ParamBody extends PureComponent {
const Button = getComponent("Button")
const TextArea = getComponent("TextArea")
const HighlightCode = getComponent("highlightCode")
const HighlightCode = getComponent("HighlightCode", true)
const ContentType = getComponent("contentType")
// for domains where specSelectors not passed
let parameter = specSelectors ? specSelectors.parameterWithMetaByIdentity(pathMethod, param) : param
@@ -148,7 +148,7 @@ export default class ParamBody extends PureComponent {
contentTypes={ consumes }
onChange={onChangeConsumes}
className="body-param-content-type"
ariaLabel="Parameter content type"
ariaLabel="Parameter content type"
controlId={controlId}
/>
</label>

View File

@@ -53,7 +53,7 @@ export default class ResponseBody extends React.PureComponent {
render() {
let { content, contentType, url, headers={}, getConfigs, getComponent } = this.props
const { parsedContent } = this.state
const HighlightCode = getComponent("highlightCode")
const HighlightCode = getComponent("HighlightCode", true)
const downloadName = "response_" + new Date().getTime()
let body, bodyEl
url = url || ""

View File

@@ -98,7 +98,7 @@ export default class Response extends React.Component {
let links = response.get("links")
const ResponseExtension = getComponent("ResponseExtension")
const Headers = getComponent("headers")
const HighlightCode = getComponent("highlightCode")
const HighlightCode = getComponent("HighlightCode", true)
const ModelExample = getComponent("modelExample")
const Markdown = getComponent("Markdown", true)
const OperationLink = getComponent("operationLink")

View File

@@ -26,6 +26,7 @@ import UtilPlugin from "./plugins/util"
import ViewPlugin from "./plugins/view"
import ViewLegacyPlugin from "core/plugins/view-legacy"
import DownloadUrlPlugin from "./plugins/download-url"
import SyntaxHighlightingPlugin from "core/plugins/syntax-highlighting"
import SafeRenderPlugin from "./plugins/safe-render"
import { parseSearch } from "./utils"
@@ -271,5 +272,6 @@ SwaggerUI.plugins = {
View: ViewPlugin,
ViewLegacy: ViewLegacyPlugin,
DownloadUrl: DownloadUrlPlugin,
SyntaxHighlighting: SyntaxHighlightingPlugin,
SafeRender: SafeRenderPlugin,
}

View File

@@ -71,7 +71,7 @@ const RequestBody = ({
const Markdown = getComponent("Markdown", true)
const ModelExample = getComponent("modelExample")
const RequestBodyEditor = getComponent("RequestBodyEditor")
const HighlightCode = getComponent("highlightCode")
const HighlightCode = getComponent("HighlightCode", true)
const ExamplesSelectValueRetainer = getComponent("ExamplesSelectValueRetainer")
const Example = getComponent("Example")
const ParameterIncludeEmpty = getComponent("ParameterIncludeEmpty")
@@ -152,7 +152,7 @@ const RequestBody = ({
{
Map.isMap(bodyProperties) && bodyProperties.entrySeq().map(([key, schema]) => {
if (schema.get("readOnly")) return
const oneOf = schema.get("oneOf")?.get(0)?.toJS()
const anyOf = schema.get("anyOf")?.get(0)?.toJS()
schema = fromJS(fn.mergeJsonSchema(schema.toJS(), oneOf ?? anyOf ?? {}))
@@ -169,7 +169,7 @@ const RequestBody = ({
let initialValue = fn.getSampleSchema(schema, false, {
includeWriteOnly: true
})
if (initialValue === false) {
initialValue = "false"
}

View File

@@ -3,7 +3,6 @@ import PropTypes from "prop-types"
import get from "lodash/get"
import isFunction from "lodash/isFunction"
import { CopyToClipboard } from "react-copy-to-clipboard"
import { SyntaxHighlighter, getStyle } from "core/syntax-highlighting"
const style = {
cursor: "pointer",
@@ -42,6 +41,7 @@ const RequestSnippets = ({ request, requestSnippetsSelectors, getConfigs, getCom
const ArrowIcon = getComponent("ArrowUpIcon")
const ArrowDownIcon = getComponent("ArrowDownIcon")
const SyntaxHighlighter = getComponent("SyntaxHighlighter", true)
const [activeLanguage, setActiveLanguage] = useState(requestSnippetsSelectors.getSnippetGenerators()?.keySeq().first())
const [isExpanded, setIsExpanded] = useState(requestSnippetsSelectors?.getDefaultExpanded())
@@ -99,16 +99,16 @@ const RequestSnippets = ({ request, requestSnippetsSelectors, getConfigs, getCom
}
}
const SnippetComponent = canSyntaxHighlight
? <SyntaxHighlighter
const SnippetComponent = canSyntaxHighlight ? (
<SyntaxHighlighter
language={activeGenerator.get("syntax")}
className="curl microlight"
style={getStyle(get(config, "syntaxHighlight.theme"))}
>
{snippet}
</SyntaxHighlighter>
:
) : (
<textarea readOnly={true} className="curl" value={snippet}></textarea>
)
return (
<div className="request-snippets" ref={rootRef}>
@@ -147,7 +147,7 @@ const RequestSnippets = ({ request, requestSnippetsSelectors, getConfigs, getCom
</div>
}
</div>
)
)
}
RequestSnippets.propTypes = {

View File

@@ -0,0 +1,25 @@
/**
* @prettier
*/
import SyntaxHighlighter from "react-syntax-highlighter/dist/esm/light"
import js from "react-syntax-highlighter/dist/esm/languages/hljs/javascript"
import json from "react-syntax-highlighter/dist/esm/languages/hljs/json"
import xml from "react-syntax-highlighter/dist/esm/languages/hljs/xml"
import bash from "react-syntax-highlighter/dist/esm/languages/hljs/bash"
import yaml from "react-syntax-highlighter/dist/esm/languages/hljs/yaml"
import http from "react-syntax-highlighter/dist/esm/languages/hljs/http"
import powershell from "react-syntax-highlighter/dist/esm/languages/hljs/powershell"
import javascript from "react-syntax-highlighter/dist/esm/languages/hljs/javascript"
const afterLoad = () => {
SyntaxHighlighter.registerLanguage("json", json)
SyntaxHighlighter.registerLanguage("js", js)
SyntaxHighlighter.registerLanguage("xml", xml)
SyntaxHighlighter.registerLanguage("yaml", yaml)
SyntaxHighlighter.registerLanguage("http", http)
SyntaxHighlighter.registerLanguage("bash", bash)
SyntaxHighlighter.registerLanguage("powershell", powershell)
SyntaxHighlighter.registerLanguage("javascript", javascript)
}
export default afterLoad

View File

@@ -1,30 +1,30 @@
/**
* @prettier
*/
import React, { useRef, useEffect } from "react"
import PropTypes from "prop-types"
import cx from "classnames"
import {SyntaxHighlighter, getStyle} from "core/syntax-highlighting"
import classNames from "classnames"
import get from "lodash/get"
import isFunction from "lodash/isFunction"
import saveAs from "js-file-download"
import { CopyToClipboard } from "react-copy-to-clipboard"
const HighlightCode = ({value, fileName = "response.txt", className, downloadable, getConfigs, canCopy, language}) => {
const config = isFunction(getConfigs) ? getConfigs() : null
const canSyntaxHighlight = get(config, "syntaxHighlight") !== false && get(config, "syntaxHighlight.activated", true)
const HighlightCode = ({
value,
fileName = "response.txt",
className,
downloadable,
getConfigs,
getComponent,
canCopy,
language,
}) => {
const config = getConfigs()
const canSyntaxHighlight =
get(config, "syntaxHighlight") !== false &&
get(config, "syntaxHighlight.activated", true)
const rootRef = useRef(null)
useEffect(() => {
const childNodes = Array
.from(rootRef.current.childNodes)
.filter(node => !!node.nodeType && node.classList.contains("microlight"))
// 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))
}
}, [value, className, language])
const SyntaxHighlighter = getComponent("SyntaxHighlighter", true)
const handleDownload = () => {
saveAs(value, fileName)
@@ -32,7 +32,11 @@ const HighlightCode = ({value, fileName = "response.txt", className, downloadabl
const handlePreventYScrollingBeyondElement = (e) => {
const { target, deltaY } = e
const { scrollHeight: contentHeight, offsetHeight: visibleHeight, scrollTop } = target
const {
scrollHeight: contentHeight,
offsetHeight: visibleHeight,
scrollTop,
} = target
const scrollOffset = visibleHeight + scrollTop
const isElementScrollable = contentHeight > visibleHeight
const isScrollingPastTop = scrollTop === 0 && deltaY < 0
@@ -43,31 +47,57 @@ const HighlightCode = ({value, fileName = "response.txt", className, downloadabl
}
}
useEffect(() => {
const childNodes = Array.from(rootRef.current.childNodes).filter(
(node) => !!node.nodeType && node.classList.contains("microlight")
)
// 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
)
)
}
}, [value, className, language])
return (
<div className="highlight-code" ref={rootRef}>
{canCopy && (
<div className="copy-to-clipboard">
<CopyToClipboard text={value}><button/></CopyToClipboard>
<CopyToClipboard text={value}>
<button />
</CopyToClipboard>
</div>
)}
{!downloadable ? null :
{!downloadable ? null : (
<button className="download-contents" onClick={handleDownload}>
Download
</button>
}
)}
{canSyntaxHighlight
? <SyntaxHighlighter
{canSyntaxHighlight ? (
<SyntaxHighlighter
language={language}
className={cx(className, "microlight")}
style={getStyle(get(config, "syntaxHighlight.theme", "agate"))}
className={classNames(className, "microlight")}
>
{value}
</SyntaxHighlighter>
: <pre className={cx(className, "microlight")}>{value}</pre>
}
) : (
<pre className={classNames(className, "microlight")}>{value}</pre>
)}
</div>
)
}
@@ -75,11 +105,12 @@ const HighlightCode = ({value, fileName = "response.txt", className, downloadabl
HighlightCode.propTypes = {
value: PropTypes.string.isRequired,
getConfigs: PropTypes.func.isRequired,
getComponent: PropTypes.func.isRequired,
className: PropTypes.string,
downloadable: PropTypes.bool,
fileName: PropTypes.string,
language: PropTypes.string,
canCopy: PropTypes.bool
canCopy: PropTypes.bool,
}
export default HighlightCode

View File

@@ -0,0 +1,43 @@
/**
* @prettier
*/
import React from "react"
import PropTypes from "prop-types"
import ReactSyntaxHighlighter from "react-syntax-highlighter/dist/esm/light"
import get from "lodash/get"
const SyntaxHighlighter = ({
language,
className = "",
getConfigs,
syntaxHighlighting = {},
children = null,
}) => {
const configs = getConfigs()
const theme = get(configs, "syntaxHighlight.theme")
const { styles, defaultStyle } = syntaxHighlighting
const style = styles?.[theme] ?? defaultStyle
return (
<ReactSyntaxHighlighter
language={language}
className={className}
style={style}
>
{children}
</ReactSyntaxHighlighter>
)
}
SyntaxHighlighter.propTypes = {
language: PropTypes.string.isRequired,
className: PropTypes.string,
getConfigs: PropTypes.func.isRequired,
syntaxHighlighting: PropTypes.shape({
styles: PropTypes.object,
defaultStyle: PropTypes.object,
}),
children: PropTypes.node,
}
export default SyntaxHighlighter

View File

@@ -0,0 +1,20 @@
/**
* @prettier
*/
import afterLoad from "./after-load"
import { styles, defaultStyle } from "./root-injects"
import SyntaxHighlighter from "./components/SyntaxHighlighter"
import HighlightCode from "./components/HighlightCode"
const SyntaxHighlightingPlugin = () => ({
afterLoad,
rootInjects: {
syntaxHighlighting: { styles, defaultStyle },
},
components: {
SyntaxHighlighter,
HighlightCode,
},
})
export default SyntaxHighlightingPlugin

View File

@@ -0,0 +1,22 @@
/**
* @prettier
*/
import agate from "react-syntax-highlighter/dist/esm/styles/hljs/agate"
import arta from "react-syntax-highlighter/dist/esm/styles/hljs/arta"
import monokai from "react-syntax-highlighter/dist/esm/styles/hljs/monokai"
import nord from "react-syntax-highlighter/dist/esm/styles/hljs/nord"
import obsidian from "react-syntax-highlighter/dist/esm/styles/hljs/obsidian"
import tomorrowNight from "react-syntax-highlighter/dist/esm/styles/hljs/tomorrow-night"
import idea from "react-syntax-highlighter/dist/esm/styles/hljs/idea"
export const styles = {
agate,
arta,
monokai,
nord,
obsidian,
"tomorrow-night": tomorrowNight,
idea,
}
export const defaultStyle = agate

View File

@@ -18,6 +18,7 @@ import UtilPlugin from "core/plugins/util"
import ViewPlugin from "core/plugins/view"
import ViewLegacyPlugin from "core/plugins/view-legacy"
import DownloadUrlPlugin from "core/plugins/download-url"
import SyntaxHighlightingPlugin from "core/plugins/syntax-highlighting"
import SafeRenderPlugin from "core/plugins/safe-render"
// ad-hoc plugins
import CoreComponentsPlugin from "core/presets/base/plugins/core-components"
@@ -45,6 +46,7 @@ const BasePreset = () => [
FilterPlugin,
OnCompletePlugin,
RequestSnippetsPlugin,
SyntaxHighlightingPlugin,
SafeRenderPlugin(),
]

View File

@@ -27,7 +27,6 @@ import OperationSummaryMethod from "core/components/operation-summary-method"
import OperationSummaryPath from "core/components/operation-summary-path"
import OperationExt from "core/components/operation-extensions"
import OperationExtRow from "core/components/operation-extension-row"
import HighlightCode from "core/components/highlight-code"
import Responses from "core/components/responses"
import Response from "core/components/response"
import ResponseExtension from "core/components/response-extension"
@@ -103,7 +102,6 @@ const CoreComponentsPlugin = () => ({
OperationSummary,
OperationSummaryMethod,
OperationSummaryPath,
highlightCode: HighlightCode,
responses: Responses,
response: Response,
ResponseExtension: ResponseExtension,

View File

@@ -1,39 +0,0 @@
import SyntaxHighlighter from "react-syntax-highlighter/dist/esm/light"
import js from "react-syntax-highlighter/dist/esm/languages/hljs/javascript"
import json from "react-syntax-highlighter/dist/esm/languages/hljs/json"
import xml from "react-syntax-highlighter/dist/esm/languages/hljs/xml"
import bash from "react-syntax-highlighter/dist/esm/languages/hljs/bash"
import yaml from "react-syntax-highlighter/dist/esm/languages/hljs/yaml"
import http from "react-syntax-highlighter/dist/esm/languages/hljs/http"
import powershell from "react-syntax-highlighter/dist/esm/languages/hljs/powershell"
import javascript from "react-syntax-highlighter/dist/esm/languages/hljs/javascript"
import agate from "react-syntax-highlighter/dist/esm/styles/hljs/agate"
import arta from "react-syntax-highlighter/dist/esm/styles/hljs/arta"
import monokai from "react-syntax-highlighter/dist/esm/styles/hljs/monokai"
import nord from "react-syntax-highlighter/dist/esm/styles/hljs/nord"
import obsidian from "react-syntax-highlighter/dist/esm/styles/hljs/obsidian"
import tomorrowNight from "react-syntax-highlighter/dist/esm/styles/hljs/tomorrow-night"
import idea from "react-syntax-highlighter/dist/esm/styles/hljs/idea"
SyntaxHighlighter.registerLanguage("json", json)
SyntaxHighlighter.registerLanguage("js", js)
SyntaxHighlighter.registerLanguage("xml", xml)
SyntaxHighlighter.registerLanguage("yaml", yaml)
SyntaxHighlighter.registerLanguage("http", http)
SyntaxHighlighter.registerLanguage("bash", bash)
SyntaxHighlighter.registerLanguage("powershell", powershell)
SyntaxHighlighter.registerLanguage("javascript", javascript)
const styles = {agate, arta, monokai, nord, obsidian, "tomorrow-night": tomorrowNight, idea}
export const availableStyles = Object.keys(styles)
export const getStyle = name => {
if (!availableStyles.includes(name)) {
console.warn(`Request style '${name}' is not available, returning default instead`)
return agate
}
return styles[name]
}
export {SyntaxHighlighter, styles}

View File

@@ -1,26 +1,39 @@
import React from "react"
import expect from "expect"
import { shallow, mount } from "enzyme"
import HighlightCode from "core/components/highlight-code"
import HighlightCode from "core/plugins/syntax-highlighting/components/HighlightCode"
import SyntaxHighlighter from "core/plugins/syntax-highlighting/components/SyntaxHighlighter"
const fakeGetConfigs = () => ({syntaxHighlight: {activated: true, theme: "agate"}})
const fakeGetConfigs = () => ({ syntaxHighlight: { activated: true, theme: "agate" }})
const fakeGetComponent = (name, isContainer) => {
const components = { HighlightCode, SyntaxHighlighter }
const Component = components[name]
if (isContainer) {
return ({ ...props }) => {
return <Component getConfigs={fakeGetConfigs} getComponent={fakeGetComponent} {...props} />
}
}
return Component
}
describe("<HighlightCode />", () => {
it("should render a Download button if downloadable", () => {
const props = {downloadable: true, getConfigs: fakeGetConfigs }
const props = { downloadable: true, getConfigs: fakeGetConfigs, getComponent: fakeGetComponent }
const wrapper = shallow(<HighlightCode {...props} />)
expect(wrapper.find(".download-contents").length).toEqual(1)
})
it("should render a Copy To Clipboard button if copyable", () => {
const props = {canCopy: true, getConfigs: fakeGetConfigs }
const props = { canCopy: true, getConfigs: fakeGetConfigs, getComponent: fakeGetComponent }
const wrapper = shallow(<HighlightCode {...props} />)
expect(wrapper.find("CopyToClipboard").length).toEqual(1)
})
it("should render values in a preformatted element", () => {
const value = "test text"
const props = {value: value, getConfigs: fakeGetConfigs}
const props = { value, getConfigs: fakeGetConfigs, getComponent: fakeGetComponent }
const wrapper = mount(<HighlightCode {...props} />)
const preTag = wrapper.find("pre")

View File

@@ -3,9 +3,8 @@ import { shallow } from "enzyme"
import ResponseBody from "core/components/response-body"
describe("<ResponseBody />", function () {
const highlightCodeComponent = () => null
const components = {
highlightCode: highlightCodeComponent
HighlightCode: () => null
}
const props = {
getComponent: c => components[c],
@@ -15,27 +14,27 @@ describe("<ResponseBody />", function () {
props.contentType = "application/json"
props.content = "{\"key\": \"a test value\"}"
const wrapper = shallow(<ResponseBody {...props} />)
expect(wrapper.find("highlightCodeComponent").length).toEqual(1)
expect(wrapper.find("HighlightCode").length).toEqual(1)
})
it("renders ResponseBody as 'text/html'", function () {
props.contentType = "application/json"
props.content = "<b>Result</b>"
const wrapper = shallow(<ResponseBody {...props} />)
expect(wrapper.find("highlightCodeComponent").length).toEqual(1)
expect(wrapper.find("HighlightCode").length).toEqual(1)
})
it("renders ResponseBody as 'image/svg'", function () {
props.contentType = "image/svg"
const wrapper = shallow(<ResponseBody {...props} />)
expect(wrapper.find("highlightCodeComponent").length).toEqual(0)
expect(wrapper.find("HighlightCode").length).toEqual(0)
})
it("should render a copyable highlightCodeComponent for text types", function () {
props.contentType = "text/plain"
props.content = "test text"
const wrapper = shallow(<ResponseBody {...props} />)
expect(wrapper.find("highlightCodeComponent[canCopy]").length).toEqual(1)
expect(wrapper.find("HighlightCode[canCopy]").length).toEqual(1)
})
it("should render Download file link for non-empty Blob response", function () {