Fix Juniper component

This commit is contained in:
Marcus Blättermann 2022-12-14 17:47:28 +01:00
parent 0b50883a96
commit 2a5d9a0d01
No known key found for this signature in database
GPG Key ID: A1E1F04008AC450D
5 changed files with 975 additions and 96 deletions

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,9 @@
"prettier": "prettier . --write" "prettier": "prettier . --write"
}, },
"dependencies": { "dependencies": {
"@codemirror/lang-python": "^6.1.0",
"@jupyterlab/services": "^3.2.1",
"@lezer/highlight": "^1.1.3",
"@mapbox/rehype-prism": "^0.8.0", "@mapbox/rehype-prism": "^0.8.0",
"@mdx-js/loader": "^2.1.5", "@mdx-js/loader": "^2.1.5",
"@mdx-js/react": "^2.1.5", "@mdx-js/react": "^2.1.5",
@ -20,6 +23,8 @@
"@types/node": "18.11.9", "@types/node": "18.11.9",
"@types/react": "18.0.25", "@types/react": "18.0.25",
"@types/react-dom": "18.0.8", "@types/react-dom": "18.0.8",
"@uiw/codemirror-themes": "^4.19.3",
"@uiw/react-codemirror": "^4.19.3",
"acorn": "^8.8.1", "acorn": "^8.8.1",
"browser-monads": "^1.0.0", "browser-monads": "^1.0.0",
"classnames": "^2.3.2", "classnames": "^2.3.2",
@ -32,6 +37,7 @@
"next-mdx-remote": "^4.2.0", "next-mdx-remote": "^4.2.0",
"next-pwa": "^5.6.0", "next-pwa": "^5.6.0",
"next-sitemap": "^3.1.32", "next-sitemap": "^3.1.32",
"node-fetch": "^2.6.7",
"parse-numeric-range": "^1.3.0", "parse-numeric-range": "^1.3.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"prismjs": "^1.29.0", "prismjs": "^1.29.0",
@ -48,7 +54,8 @@
"remark-unwrap-images": "^3.0.1", "remark-unwrap-images": "^3.0.1",
"sass": "^1.56.1", "sass": "^1.56.1",
"typescript": "4.8.4", "typescript": "4.8.4",
"unist-util-visit": "^4.1.1" "unist-util-visit": "^4.1.1",
"ws": "^8.11.0"
}, },
"engine": 18 "engine": 18
} }

View File

@ -18,6 +18,7 @@ import CUSTOM_TYPES from '../../meta/type-annotations.json'
import { isString, htmlToReact } from './util' import { isString, htmlToReact } from './util'
import Link, { OptionalLink } from './link' import Link, { OptionalLink } from './link'
import GitHubCode from './github' import GitHubCode from './github'
import Juniper from './juniper'
import classes from '../styles/code.module.sass' import classes from '../styles/code.module.sass'
import siteMetadata from '../../meta/site.json' import siteMetadata from '../../meta/site.json'
import { binderBranch } from '../../meta/dynamicMeta.mjs' import { binderBranch } from '../../meta/dynamicMeta.mjs'
@ -305,8 +306,6 @@ export const CodeHighlighted = ({ children, highlight, lang }) => {
} }
export class Code extends React.Component { export class Code extends React.Component {
state = { Juniper: null }
static defaultProps = { static defaultProps = {
lang: 'none', lang: 'none',
executable: null, executable: null,
@ -323,20 +322,6 @@ export class Code extends React.Component {
children: PropTypes.node, children: PropTypes.node,
} }
updateJuniper() {
if (this.state.Juniper == null && window.Juniper !== null) {
this.setState({ Juniper: window.Juniper })
}
}
componentDidMount() {
this.updateJuniper()
}
componentDidUpdate() {
this.updateJuniper()
}
render() { render() {
const { lang, title, executable, github, wrap, highlight, className, children } = this.props const { lang, title, executable, github, wrap, highlight, className, children } = this.props
const codeClassNames = classNames(classes['code'], className, `language-${lang}`, { const codeClassNames = classNames(classes['code'], className, `language-${lang}`, {
@ -344,14 +329,13 @@ export class Code extends React.Component {
[classes['cli']]: lang === 'cli', [classes['cli']]: lang === 'cli',
}) })
const ghClassNames = classNames(codeClassNames, classes['max-height']) const ghClassNames = classNames(codeClassNames, classes['max-height'])
const { Juniper } = this.state
if (github) { if (github) {
return <GitHubCode url={github} className={ghClassNames} lang={lang} /> return <GitHubCode url={github} className={ghClassNames} lang={lang} />
} }
if (!!executable && Juniper) { if (!!executable) {
return ( return (
<JuniperWrapper Juniper={Juniper} title={title} lang={lang}> <JuniperWrapper title={title} lang={lang}>
{children} {children}
</JuniperWrapper> </JuniperWrapper>
) )
@ -370,7 +354,7 @@ export class Code extends React.Component {
} }
} }
const JuniperWrapper = ({ Juniper, title, lang, children }) => { const JuniperWrapper = ({ title, lang, children }) => {
const { binderUrl, binderVersion } = siteMetadata const { binderUrl, binderVersion } = siteMetadata
const juniperTitle = title || 'Editable Code' const juniperTitle = title || 'Editable Code'
return ( return (

View File

@ -1,37 +1,44 @@
import React from 'react' import React, { useEffect } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import CodeMirror from 'codemirror' import CodeMirror from '@uiw/react-codemirror'
import python from 'codemirror/mode/python/python' // eslint-disable-line no-unused-vars import { createTheme } from '@uiw/codemirror-themes'
import { Widget } from '@phosphor/widgets' import { tags as t } from '@lezer/highlight'
import { python } from '@codemirror/lang-python'
import { Kernel, ServerConnection } from '@jupyterlab/services' import { Kernel, ServerConnection } from '@jupyterlab/services'
import { OutputArea, OutputAreaModel } from '@jupyterlab/outputarea'
import { RenderMimeRegistry, standardRendererFactories } from '@jupyterlab/rendermime'
import { window } from 'browser-monads' import { window } from 'browser-monads'
import classes from '../styles/code.module.sass'
const spacyTheme = createTheme({
theme: 'dark',
settings: {
background: 'var(--color-front)',
foreground: 'var(--color-subtle)',
caret: 'var(--color-theme-dark)',
selection: 'var(--color-theme)',
selectionMatch: 'var(--color-theme)',
gutterBackground: 'var(--color-front)',
gutterForeground: 'var(--color-subtle)',
fontFamily: 'var(--font-code)',
},
styles: [
{ tag: t.comment, color: 'var(--syntax-comment)' },
{ tag: t.variableName, color: 'var(--color-subtle)' },
{ tag: [t.string, t.special(t.brace)], color: '#fff' },
{ tag: t.number, color: 'var(--syntax-number)' },
{ tag: t.string, color: 'var(--syntax-selector)' },
{ tag: t.bool, color: 'var(--syntax-keyword)' },
{ tag: t.keyword, color: 'var(--syntax-keyword)' },
{ tag: t.operator, color: 'var(--syntax-operator)' },
],
})
export default class Juniper extends React.Component { export default class Juniper extends React.Component {
outputRef = null state = {
inputRef = null kernel: null,
state = { kernel: null, renderers: null, fromStorage: null } renderers: null,
fromStorage: null,
componentDidMount() { output: null,
const renderers = standardRendererFactories.filter((factory) => code: this.props.children,
factory.mimeTypes.includes('text/latex') ? window.MathJax : true
)
const outputArea = new OutputArea({
model: new OutputAreaModel({ trusted: true }),
rendermime: new RenderMimeRegistry({ initialFactories: renderers }),
})
const cm = new CodeMirror(this.inputRef, {
value: this.props.children.trim(),
mode: this.props.lang,
theme: this.props.theme,
})
const runCode = () => this.execute(outputArea, cm.getValue())
cm.setOption('extraKeys', { 'Shift-Enter': runCode })
Widget.attach(outputArea, this.outputRef)
this.setState({ runCode })
} }
log(logFunction) { log(logFunction) {
@ -127,14 +134,25 @@ export default class Juniper extends React.Component {
* @param {OutputArea} outputArea - The cell's output area. * @param {OutputArea} outputArea - The cell's output area.
* @param {string} code - The code to execute. * @param {string} code - The code to execute.
*/ */
renderResponse(outputArea, code) { async renderResponse(kernel) {
outputArea.future = this.state.kernel.requestExecute({ code }) if (this.state.code === null || this.state.code === '') {
outputArea.model.add({ this.state.output = 'No code entered'
output_type: 'stream', return
name: 'loading', }
text: this.props.msgLoading,
const response = kernel.requestExecute({
code: this.state.code,
}) })
outputArea.model.clear(true)
this.state.output = this.props.msgLoading
response.handleMsg = (message) => {
if (message.content && message.content.name === 'stdout') {
this.setState({
output: message.content.text,
})
}
}
} }
/** /**
@ -142,41 +160,31 @@ export default class Juniper extends React.Component {
* @param {OutputArea} - outputArea - The cell's output area. * @param {OutputArea} - outputArea - The cell's output area.
* @param {string} code - The code to execute. * @param {string} code - The code to execute.
*/ */
execute(outputArea, code) { runCode() {
this.log(() => console.info('executing')) this.log(() => console.info('executing'))
if (this.state.kernel) { if (this.state.kernel) {
if (this.props.isolateCells) { if (this.props.isolateCells) {
this.state.kernel this.state.kernel
.restart() .restart()
.then(() => this.renderResponse(outputArea, code)) .then(() => this.renderResponse(this.state.kernel))
.catch((err) => { .catch((err) => {
this.log(() => console.error('failed', err)) this.log(() => console.error('faileder', err))
this.setState({ kernel: null }) this.setState({ kernel: null })
outputArea.model.clear() this.setState({ output: this.props.msgError })
outputArea.model.add({
output_type: 'stream',
name: 'failure',
text: this.props.msgError,
})
}) })
return return
} }
this.renderResponse(outputArea, code) this.renderResponse(this.state.kernel)
return return
} }
this.log(() => console.info('requesting kernel')) this.log(() => console.info('requesting kernel'))
const url = this.props.url.split('//')[1] const url = this.props.url.split('//')[1]
const action = !this.state.fromStorage ? 'Launching' : 'Reconnecting to' const action = !this.state.fromStorage ? 'Launching' : 'Reconnecting to'
outputArea.model.clear() this.setState({ output: `${action} Docker container on ${url}...` })
outputArea.model.add({ this.getKernel()
output_type: 'stream',
name: 'stdout',
text: `${action} Docker container on ${url}...`,
})
new Promise((resolve, reject) => this.getKernel().then(resolve).catch(reject))
.then((kernel) => { .then((kernel) => {
this.setState({ kernel }) this.setState({ kernel })
this.renderResponse(outputArea, code) this.renderResponse(kernel)
}) })
.catch((err) => { .catch((err) => {
this.log(() => console.error('failed', err)) this.log(() => console.error('failed', err))
@ -185,33 +193,40 @@ export default class Juniper extends React.Component {
this.setState({ fromStorage: false }) this.setState({ fromStorage: false })
window.localStorage.removeItem(this.props.storageKey) window.localStorage.removeItem(this.props.storageKey)
} }
outputArea.model.clear() this.setState({ output: this.props.msgError })
outputArea.model.add({
output_type: 'stream',
name: 'failure',
text: this.props.msgError,
})
}) })
} }
render() { render() {
return ( return (
<div className={this.props.classNames.cell}> <div className={this.props.classNames.cell}>
<div {this.state.code && (
className={this.props.classNames.input} <CodeMirror
ref={(x) => { value={this.state.code}
this.inputRef = x extensions={[python()]}
}} theme={spacyTheme}
/> basicSetup={{
<button className={this.props.classNames.button} onClick={this.state.runCode}> lineNumbers: false,
foldGutter: false,
highlightActiveLine: false,
highlightSelectionMatches: false,
}}
className={classes['juniper-input']}
onChange={(value) => {
this.setState({ code: value })
}}
/>
)}
<button className={this.props.classNames.button} onClick={() => this.runCode()}>
{this.props.msgButton} {this.props.msgButton}
</button> </button>
<div {this.state.output !== null && (
ref={(x) => { <pre
this.outputRef = x className={`${this.props.classNames.output} ${classes['juniper-input']} ${classes.wrap}`}
}} >
className={this.props.classNames.output} {this.state.output}
/> </pre>
)}
</div> </div>
) )
} }

View File

@ -1,7 +1,7 @@
import Link from './components/link' import Link from './components/link'
import Section, { Hr } from './components/section' import Section, { Hr } from './components/section'
import { Table, Tr, Th, Tx, Td } from './components/table' import { Table, Tr, Th, Tx, Td } from './components/table'
import { Pre, Code, InlineCode, TypeAnnotation } from './components/code' import CodeBlock, { Pre, Code, InlineCode, TypeAnnotation } from './components/code'
import { Ol, Ul, Li } from './components/list' import { Ol, Ul, Li } from './components/list'
import { H2, H3, H4, H5, P, Abbr, Help, Label } from './components/typography' import { H2, H3, H4, H5, P, Abbr, Help, Label } from './components/typography'
import Accordion from './components/accordion' import Accordion from './components/accordion'
@ -76,6 +76,7 @@ export const remarkComponents = {
Tag, Tag,
Accordion, Accordion,
Grid, Grid,
CodeBlock,
InlineCode, InlineCode,
Project, Project,
Integration, Integration,