feat: syntax highlighting of code section (#6236)
Co-authored-by: AdrieanKhisbe <adriean.khisbe@live.fr>
This commit is contained in:
@@ -1,26 +1,42 @@
|
||||
import React from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import curlify from "core/curlify"
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard"
|
||||
import {SyntaxHighlighter, getStyle} from "core/syntax-highlighting"
|
||||
import get from "lodash/get"
|
||||
|
||||
export default class Curl extends React.Component {
|
||||
static propTypes = {
|
||||
getConfigs: PropTypes.func.isRequired,
|
||||
request: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
handleFocus(e) {
|
||||
e.target.select()
|
||||
document.execCommand("copy")
|
||||
}
|
||||
|
||||
render() {
|
||||
let { request } = this.props
|
||||
let { request, getConfigs } = this.props
|
||||
let curl = curlify(request)
|
||||
|
||||
const config = getConfigs()
|
||||
|
||||
const curlBlock = get(config, "syntaxHighlight.activated")
|
||||
? <SyntaxHighlighter
|
||||
language="bash"
|
||||
className="curl microlight"
|
||||
onWheel={this.preventYScrollingBeyondElement}
|
||||
style={getStyle(get(config, "syntaxHighlight.theme"))}
|
||||
>
|
||||
{curl}
|
||||
</SyntaxHighlighter>
|
||||
:
|
||||
<textarea readOnly={true} className="curl" value={curl}></textarea>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="curl-command">
|
||||
<h4>Curl</h4>
|
||||
<div className="copy-paste">
|
||||
<textarea onFocus={this.handleFocus} readOnly={true} className="curl" value={curl}></textarea>
|
||||
<div className="copy-to-clipboard">
|
||||
<CopyToClipboard text={curl}><button/></CopyToClipboard>
|
||||
</div>
|
||||
<div>
|
||||
{curlBlock}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,30 +1,20 @@
|
||||
import React, { Component } from "react"
|
||||
import PropTypes from "prop-types"
|
||||
import { highlight } from "core/utils"
|
||||
import {SyntaxHighlighter, getStyle} from "core/syntax-highlighting"
|
||||
import get from "lodash/get"
|
||||
import saveAs from "js-file-download"
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard"
|
||||
|
||||
export default class HighlightCode extends Component {
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
getConfigs: PropTypes.func.isRequired,
|
||||
className: PropTypes.string,
|
||||
downloadable: PropTypes.bool,
|
||||
fileName: PropTypes.string,
|
||||
canCopy: PropTypes.bool
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
highlight(this.el)
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
highlight(this.el)
|
||||
}
|
||||
|
||||
initializeComponent = (c) => {
|
||||
this.el = c
|
||||
}
|
||||
|
||||
downloadText = () => {
|
||||
saveAs(this.props.value, this.props.fileName || "response.txt")
|
||||
}
|
||||
@@ -49,9 +39,22 @@ export default class HighlightCode extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
let { value, className, downloadable, canCopy } = this.props
|
||||
let { value, className, downloadable, getConfigs, canCopy } = this.props
|
||||
|
||||
const config = getConfigs ? getConfigs() : {syntaxHighlight: {activated: true, theme: "agate"}}
|
||||
|
||||
className = className || ""
|
||||
|
||||
const codeBlock = get(config, "syntaxHighlight.activated")
|
||||
? <SyntaxHighlighter
|
||||
className={className + " microlight"}
|
||||
onWheel={this.preventYScrollingBeyondElement}
|
||||
style={getStyle(get(config, "syntaxHighlight.theme"))}
|
||||
>
|
||||
{value}
|
||||
</SyntaxHighlighter>
|
||||
: <pre onWheel={this.preventYScrollingBeyondElement} className={className + " microlight"}>{value}</pre>
|
||||
|
||||
return (
|
||||
<div className="highlight-code">
|
||||
{ !downloadable ? null :
|
||||
@@ -66,12 +69,7 @@ export default class HighlightCode extends Component {
|
||||
</div>
|
||||
}
|
||||
|
||||
<pre
|
||||
ref={this.initializeComponent}
|
||||
onWheel={this.preventYScrollingBeyondElement}
|
||||
className={className + " microlight"}>
|
||||
{value}
|
||||
</pre>
|
||||
{ codeBlock }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ export default class LiveResponse extends React.Component {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ curlRequest && <Curl request={ curlRequest }/> }
|
||||
{ curlRequest && <Curl request={ curlRequest } getConfigs={ getConfigs } /> }
|
||||
{ url && <div>
|
||||
<h4>Request URL</h4>
|
||||
<div className="request-url">
|
||||
@@ -110,6 +110,7 @@ export default class LiveResponse extends React.Component {
|
||||
contentType={ contentType }
|
||||
url={ url }
|
||||
headers={ headers }
|
||||
getConfigs={ getConfigs }
|
||||
getComponent={ getComponent }/>
|
||||
: null
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export default class ParamBody extends PureComponent {
|
||||
consumes: PropTypes.object,
|
||||
consumesValue: PropTypes.string,
|
||||
fn: PropTypes.object.isRequired,
|
||||
getConfigs: PropTypes.func.isRequired,
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
isExecute: PropTypes.bool,
|
||||
specSelectors: PropTypes.object.isRequired,
|
||||
@@ -96,7 +97,7 @@ export default class ParamBody extends PureComponent {
|
||||
isExecute,
|
||||
specSelectors,
|
||||
pathMethod,
|
||||
|
||||
getConfigs,
|
||||
getComponent,
|
||||
} = this.props
|
||||
|
||||
@@ -118,6 +119,7 @@ export default class ParamBody extends PureComponent {
|
||||
isEditBox && isExecute
|
||||
? <TextArea className={ "body-param__text" + ( errors.count() ? " invalid" : "")} value={value} onChange={ this.handleOnChange }/>
|
||||
: (value && <HighlightCode className="body-param__example"
|
||||
getConfigs={ getConfigs }
|
||||
value={ value }/>)
|
||||
}
|
||||
<div className="body-param-options">
|
||||
|
||||
@@ -191,6 +191,7 @@ export default class ParameterRow extends Component {
|
||||
let inType = param.get("in")
|
||||
let bodyParam = inType !== "body" ? null
|
||||
: <ParamBody getComponent={getComponent}
|
||||
getConfigs={ getConfigs }
|
||||
fn={fn}
|
||||
param={param}
|
||||
consumes={ specSelectors.consumesOptionsFor(pathMethod) }
|
||||
|
||||
@@ -13,6 +13,7 @@ export default class ResponseBody extends React.PureComponent {
|
||||
static propTypes = {
|
||||
content: PropTypes.any.isRequired,
|
||||
contentType: PropTypes.string,
|
||||
getConfigs: PropTypes.func.isRequired,
|
||||
getComponent: PropTypes.func.isRequired,
|
||||
headers: PropTypes.object,
|
||||
url: PropTypes.string
|
||||
@@ -49,7 +50,7 @@ export default class ResponseBody extends React.PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
let { content, contentType, url, headers={}, getComponent } = this.props
|
||||
let { content, contentType, url, headers={}, getConfigs, getComponent } = this.props
|
||||
const { parsedContent } = this.state
|
||||
const HighlightCode = getComponent("highlightCode")
|
||||
const downloadName = "response_" + new Date().getTime()
|
||||
@@ -99,7 +100,7 @@ export default class ResponseBody extends React.PureComponent {
|
||||
body = "can't parse JSON. Raw result:\n\n" + content
|
||||
}
|
||||
|
||||
bodyEl = <HighlightCode downloadable fileName={`${downloadName}.json`} value={ body } canCopy />
|
||||
bodyEl = <HighlightCode downloadable fileName={`${downloadName}.json`} value={ body } getConfigs={ getConfigs } canCopy />
|
||||
|
||||
// XML
|
||||
} else if (/xml/i.test(contentType)) {
|
||||
@@ -107,11 +108,11 @@ export default class ResponseBody extends React.PureComponent {
|
||||
textNodesOnSameLine: true,
|
||||
indentor: " "
|
||||
})
|
||||
bodyEl = <HighlightCode downloadable fileName={`${downloadName}.xml`} value={ body } canCopy />
|
||||
bodyEl = <HighlightCode downloadable fileName={`${downloadName}.xml`} value={ body } getConfigs={ getConfigs } canCopy />
|
||||
|
||||
// HTML or Plain Text
|
||||
} else if (toLower(contentType) === "text/html" || /text\/plain/.test(contentType)) {
|
||||
bodyEl = <HighlightCode downloadable fileName={`${downloadName}.html`} value={ content } canCopy />
|
||||
bodyEl = <HighlightCode downloadable fileName={`${downloadName}.html`} value={ content } getConfigs={ getConfigs } canCopy />
|
||||
|
||||
// Image
|
||||
} else if (/^image\//i.test(contentType)) {
|
||||
@@ -125,7 +126,7 @@ export default class ResponseBody extends React.PureComponent {
|
||||
} else if (/^audio\//i.test(contentType)) {
|
||||
bodyEl = <pre className="microlight"><audio controls><source src={ url } type={ contentType } /></audio></pre>
|
||||
} else if (typeof content === "string") {
|
||||
bodyEl = <HighlightCode downloadable fileName={`${downloadName}.txt`} value={ content } canCopy />
|
||||
bodyEl = <HighlightCode downloadable fileName={`${downloadName}.txt`} value={ content } getConfigs={ getConfigs } canCopy />
|
||||
} else if ( content.size > 0 ) {
|
||||
// We don't know the contentType, but there was some content returned
|
||||
if(parsedContent) {
|
||||
@@ -135,7 +136,7 @@ export default class ResponseBody extends React.PureComponent {
|
||||
<p className="i">
|
||||
Unrecognized response type; displaying content as text.
|
||||
</p>
|
||||
<HighlightCode downloadable fileName={`${downloadName}.txt`} value={ parsedContent } canCopy />
|
||||
<HighlightCode downloadable fileName={`${downloadName}.txt`} value={ parsedContent } getConfigs={ getConfigs } canCopy />
|
||||
</div>
|
||||
|
||||
} else {
|
||||
|
||||
@@ -5,12 +5,12 @@ import cx from "classnames"
|
||||
import { fromJS, Seq, Iterable, List, Map } from "immutable"
|
||||
import { getSampleSchema, fromJSOrdered, stringify } from "core/utils"
|
||||
|
||||
const getExampleComponent = ( sampleResponse, HighlightCode ) => {
|
||||
const getExampleComponent = ( sampleResponse, HighlightCode, getConfigs ) => {
|
||||
if (
|
||||
sampleResponse !== undefined &&
|
||||
sampleResponse !== null
|
||||
) { return <div>
|
||||
<HighlightCode className="example" value={ stringify(sampleResponse) } />
|
||||
<HighlightCode className="example" getConfigs={ getConfigs } value={ stringify(sampleResponse) } />
|
||||
</div>
|
||||
}
|
||||
return null
|
||||
@@ -150,7 +150,7 @@ export default class Response extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
let example = getExampleComponent( sampleResponse, HighlightCode )
|
||||
let example = getExampleComponent( sampleResponse, HighlightCode, getConfigs )
|
||||
|
||||
return (
|
||||
<tr className={ "response " + ( className || "") } data-code={code}>
|
||||
|
||||
@@ -78,6 +78,11 @@ export default function SwaggerUI(opts) {
|
||||
// Inline Plugin
|
||||
fn: { },
|
||||
components: { },
|
||||
|
||||
syntaxHighlight: {
|
||||
activated: true,
|
||||
theme: "agate"
|
||||
}
|
||||
}
|
||||
|
||||
let queryConfig = parseSearch()
|
||||
|
||||
35
src/core/syntax-highlighting.js
Normal file
35
src/core/syntax-highlighting.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Light as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||
|
||||
import js from "react-syntax-highlighter/dist/cjs/languages/hljs/javascript"
|
||||
import json from "react-syntax-highlighter/dist/cjs/languages/hljs/json"
|
||||
import xml from "react-syntax-highlighter/dist/cjs/languages/hljs/xml"
|
||||
import bash from "react-syntax-highlighter/dist/cjs/languages/hljs/bash"
|
||||
import yaml from "react-syntax-highlighter/dist/cjs/languages/hljs/yaml"
|
||||
import http from "react-syntax-highlighter/dist/cjs/languages/hljs/http"
|
||||
|
||||
import agate from "react-syntax-highlighter/dist/cjs/styles/hljs/agate"
|
||||
import arta from "react-syntax-highlighter/dist/cjs/styles/hljs/arta"
|
||||
import monokai from "react-syntax-highlighter/dist/cjs/styles/hljs/monokai"
|
||||
import nord from "react-syntax-highlighter/dist/cjs/styles/hljs/nord"
|
||||
import obsidian from "react-syntax-highlighter/dist/cjs/styles/hljs/obsidian"
|
||||
import tomorrowNight from "react-syntax-highlighter/dist/cjs/styles/hljs/tomorrow-night"
|
||||
|
||||
SyntaxHighlighter.registerLanguage("json", json)
|
||||
SyntaxHighlighter.registerLanguage("js", js)
|
||||
SyntaxHighlighter.registerLanguage("xml", xml)
|
||||
SyntaxHighlighter.registerLanguage("yaml", yaml)
|
||||
SyntaxHighlighter.registerLanguage("http", http)
|
||||
SyntaxHighlighter.registerLanguage("bash", bash)
|
||||
|
||||
const styles = {agate, arta, monokai, nord, obsidian, "tomorrow-night": tomorrowNight}
|
||||
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}
|
||||
@@ -220,166 +220,6 @@ export function getList(iterable, keys) {
|
||||
return Im.List.isList(val) ? val : Im.List()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapted from http://github.com/asvd/microlight
|
||||
* @copyright 2016 asvd <heliosframework@gmail.com>
|
||||
*/
|
||||
export function highlight (el) {
|
||||
const MAX_LENGTH = 5000
|
||||
var
|
||||
_document = document,
|
||||
appendChild = "appendChild",
|
||||
test = "test"
|
||||
|
||||
if (!el) return ""
|
||||
if (el.textContent.length > MAX_LENGTH) { return el.textContent }
|
||||
|
||||
var reset = function(el) {
|
||||
var text = el.textContent,
|
||||
pos = 0, // current position
|
||||
next1 = text[0], // next character
|
||||
chr = 1, // current character
|
||||
prev1, // previous character
|
||||
prev2, // the one before the previous
|
||||
token = // current token content
|
||||
el.innerHTML = "", // (and cleaning the node)
|
||||
|
||||
// current token type:
|
||||
// 0: anything else (whitespaces / newlines)
|
||||
// 1: operator or brace
|
||||
// 2: closing braces (after which '/' is division not regex)
|
||||
// 3: (key)word
|
||||
// 4: regex
|
||||
// 5: string starting with "
|
||||
// 6: string starting with '
|
||||
// 7: xml comment <!-- -->
|
||||
// 8: multiline comment /* */
|
||||
// 9: single-line comment starting with two slashes //
|
||||
// 10: single-line comment starting with hash #
|
||||
tokenType = 0,
|
||||
|
||||
// kept to determine between regex and division
|
||||
lastTokenType,
|
||||
// flag determining if token is multi-character
|
||||
multichar,
|
||||
node
|
||||
|
||||
// running through characters and highlighting
|
||||
while (prev2 = prev1,
|
||||
// escaping if needed (with except for comments)
|
||||
// previous character will not be therefore
|
||||
// recognized as a token finalize condition
|
||||
prev1 = tokenType < 7 && prev1 == "\\" ? 1 : chr
|
||||
) {
|
||||
chr = next1
|
||||
next1=text[++pos]
|
||||
multichar = token.length > 1
|
||||
|
||||
// checking if current token should be finalized
|
||||
if (!chr || // end of content
|
||||
// types 9-10 (single-line comments) end with a
|
||||
// newline
|
||||
(tokenType > 8 && chr == "\n") ||
|
||||
[ // finalize conditions for other token types
|
||||
// 0: whitespaces
|
||||
/\S/[test](chr), // merged together
|
||||
// 1: operators
|
||||
1, // consist of a single character
|
||||
// 2: braces
|
||||
1, // consist of a single character
|
||||
// 3: (key)word
|
||||
!/[$\w]/[test](chr),
|
||||
// 4: regex
|
||||
(prev1 == "/" || prev1 == "\n") && multichar,
|
||||
// 5: string with "
|
||||
prev1 == "\"" && multichar,
|
||||
// 6: string with '
|
||||
prev1 == "'" && multichar,
|
||||
// 7: xml comment
|
||||
text[pos-4]+prev2+prev1 == "-->",
|
||||
// 8: multiline comment
|
||||
prev2+prev1 == "*/"
|
||||
][tokenType]
|
||||
) {
|
||||
// appending the token to the result
|
||||
if (token) {
|
||||
// remapping token type into style
|
||||
// (some types are highlighted similarly)
|
||||
el[appendChild](
|
||||
node = _document.createElement("span")
|
||||
).setAttribute("class", [
|
||||
// 0: not formatted
|
||||
"token-not-formatted",
|
||||
// 1: keywords
|
||||
"",
|
||||
// 2: punctuation
|
||||
"",
|
||||
// 3: strings and regexps
|
||||
"token-string",
|
||||
// 4: comments
|
||||
""
|
||||
][
|
||||
// not formatted
|
||||
!tokenType ? 0 :
|
||||
// punctuation
|
||||
tokenType < 3 ? 2 :
|
||||
// comments
|
||||
tokenType > 6 ? 4 :
|
||||
// regex and strings
|
||||
tokenType > 3 ? 3 :
|
||||
// otherwise tokenType == 3, (key)word
|
||||
// (1 if regexp matches, 0 otherwise)
|
||||
+ /^(a(bstract|lias|nd|rguments|rray|s(m|sert)?|uto)|b(ase|egin|ool(ean)?|reak|yte)|c(ase|atch|har|hecked|lass|lone|ompl|onst|ontinue)|de(bugger|cimal|clare|f(ault|er)?|init|l(egate|ete)?)|do|double|e(cho|ls?if|lse(if)?|nd|nsure|num|vent|x(cept|ec|p(licit|ort)|te(nds|nsion|rn)))|f(allthrough|alse|inal(ly)?|ixed|loat|or(each)?|riend|rom|unc(tion)?)|global|goto|guard|i(f|mp(lements|licit|ort)|n(it|clude(_once)?|line|out|stanceof|t(erface|ernal)?)?|s)|l(ambda|et|ock|ong)|m(icrolight|odule|utable)|NaN|n(amespace|ative|ext|ew|il|ot|ull)|o(bject|perator|r|ut|verride)|p(ackage|arams|rivate|rotected|rotocol|ublic)|r(aise|e(adonly|do|f|gister|peat|quire(_once)?|scue|strict|try|turn))|s(byte|ealed|elf|hort|igned|izeof|tatic|tring|truct|ubscript|uper|ynchronized|witch)|t(emplate|hen|his|hrows?|ransient|rue|ry|ype(alias|def|id|name|of))|u(n(checked|def(ined)?|ion|less|signed|til)|se|sing)|v(ar|irtual|oid|olatile)|w(char_t|hen|here|hile|ith)|xor|yield)$/[test](token)
|
||||
])
|
||||
|
||||
node[appendChild](_document.createTextNode(token))
|
||||
}
|
||||
|
||||
// saving the previous token type
|
||||
// (skipping whitespaces and comments)
|
||||
lastTokenType =
|
||||
(tokenType && tokenType < 7) ?
|
||||
tokenType : lastTokenType
|
||||
|
||||
// initializing a new token
|
||||
token = ""
|
||||
|
||||
// determining the new token type (going up the
|
||||
// list until matching a token type start
|
||||
// condition)
|
||||
tokenType = 11
|
||||
while (![
|
||||
1, // 0: whitespace
|
||||
// 1: operator or braces
|
||||
/[\/{}[(\-+*=<>:;|\\.,?!&@~]/[test](chr), // eslint-disable-line no-useless-escape
|
||||
/[\])]/[test](chr), // 2: closing brace
|
||||
/[$\w]/[test](chr), // 3: (key)word
|
||||
chr == "/" && // 4: regex
|
||||
// previous token was an
|
||||
// opening brace or an
|
||||
// operator (otherwise
|
||||
// division, not a regex)
|
||||
(lastTokenType < 2) &&
|
||||
// workaround for xml
|
||||
// closing tags
|
||||
prev1 != "<",
|
||||
chr == "\"", // 5: string with "
|
||||
chr == "'", // 6: string with '
|
||||
// 7: xml comment
|
||||
chr+next1+text[pos+1]+text[pos+2] == "<!--",
|
||||
chr+next1 == "/*", // 8: multiline comment
|
||||
chr+next1 == "//", // 9: single-line comment
|
||||
chr == "#" // 10: hash-style comment
|
||||
][--tokenType]);
|
||||
}
|
||||
|
||||
token += chr
|
||||
}
|
||||
}
|
||||
|
||||
return reset(el)
|
||||
}
|
||||
|
||||
/**
|
||||
* Take an immutable map, and convert to a list.
|
||||
* Where the keys are merged with the value objects
|
||||
|
||||
Reference in New Issue
Block a user