Add code block handling

This commit is contained in:
Marcus Blättermann 2022-11-27 01:40:38 +01:00
parent 38f7f6bcbe
commit a17a8a5b1d
No known key found for this signature in database
GPG Key ID: A1E1F04008AC450D
5 changed files with 190 additions and 89 deletions

View File

@ -16,6 +16,7 @@
"@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",
"acorn": "^8.8.1",
"browser-monads": "^1.0.0", "browser-monads": "^1.0.0",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"eslint": "8.27.0", "eslint": "8.27.0",
@ -25,6 +26,7 @@
"md-attr-parser": "^1.3.0", "md-attr-parser": "^1.3.0",
"next": "13.0.2", "next": "13.0.2",
"next-mdx-remote": "^4.2.0", "next-mdx-remote": "^4.2.0",
"parse-numeric-range": "^1.3.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "18.2.0", "react": "18.2.0",
@ -4303,6 +4305,11 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/parse-numeric-range": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz",
"integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ=="
},
"node_modules/path-exists": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -8765,6 +8772,11 @@
"is-hexadecimal": "^2.0.0" "is-hexadecimal": "^2.0.0"
} }
}, },
"parse-numeric-range": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz",
"integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ=="
},
"path-exists": { "path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",

View File

@ -21,6 +21,7 @@
"@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",
"acorn": "^8.8.1",
"browser-monads": "^1.0.0", "browser-monads": "^1.0.0",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"eslint": "8.27.0", "eslint": "8.27.0",
@ -30,6 +31,7 @@
"md-attr-parser": "^1.3.0", "md-attr-parser": "^1.3.0",
"next": "13.0.2", "next": "13.0.2",
"next-mdx-remote": "^4.2.0", "next-mdx-remote": "^4.2.0",
"parse-numeric-range": "^1.3.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "18.2.0", "react": "18.2.0",

View File

@ -17,7 +17,8 @@ const getProps = (estree) => {
return {} return {}
} }
const estreeBodyFirstNode = estree.body[0] const estreeBodyFirstNode =
estree.body[0].type === 'BlockStatement' ? estree.body[0].body[0] : estree.body[0]
if (estreeBodyFirstNode.type !== 'ExpressionStatement' || !estreeBodyFirstNode.expression) { if (estreeBodyFirstNode.type !== 'ExpressionStatement' || !estreeBodyFirstNode.expression) {
return {} return {}

View File

@ -1,10 +1,12 @@
/** /**
* Support titles, line highlights and more for code blocks * Support titles, line highlights and more for code blocks
*/ */
import { Parser } from 'acorn'
import { visit } from 'unist-util-visit' import { visit } from 'unist-util-visit'
import parseAttr from 'md-attr-parser' import parseAttr from 'md-attr-parser'
import getProps from './getProps.mjs'
const defaultOptions = { const defaultOptions = {
defaultPrefix: '###', defaultPrefix: '###',
prefixMap: { prefixMap: {
@ -48,7 +50,13 @@ function remarkCodeBlocks(userOptions = {}) {
const data = node.data || (node.data = {}) const data = node.data || (node.data = {})
const hProps = data.hProperties || (data.hProperties = {}) const hProps = data.hProperties || (data.hProperties = {})
node.data.hProperties = Object.assign({}, hProps, attrs)
node.data.hProperties = Object.assign(
{},
hProps,
attrs,
getProps(Parser.parse(node.meta, { ecmaVersion: 'latest' }))
)
} }
}) })

View File

@ -1,4 +1,4 @@
import React, { Fragment } from 'react' import React, { Fragment, useEffect, useState } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import classNames from 'classnames' import classNames from 'classnames'
import highlightCode from 'gatsby-remark-prismjs/highlight-code.js' import highlightCode from 'gatsby-remark-prismjs/highlight-code.js'
@ -94,12 +94,48 @@ export const TypeAnnotation = ({ lang = 'python', link = true, children }) => {
) )
} }
function replacePrompt(line, prompt, isFirst = false) { const splitLines = (children) => {
let result = line const listChildrenPerLine = []
const hasPrompt = result.startsWith(`${prompt} `)
const showPrompt = hasPrompt || isFirst if (typeof children === 'string') {
if (hasPrompt) result = result.slice(2) listChildrenPerLine.push(...children.split('\n'))
return result && showPrompt ? `<span data-prompt="${prompt}">${result}</span>` : result } else {
listChildrenPerLine.push([])
let indexLine = 0
if (Array.isArray(children)) {
children.forEach((child) => {
if (typeof child === 'string' && child.includes('\n')) {
const listString = child.split('\n')
listString.forEach((string, index) => {
listChildrenPerLine[indexLine].push(string)
if (index !== listString.length - 1) {
indexLine += 1
listChildrenPerLine[indexLine] = []
}
})
} else {
listChildrenPerLine[indexLine].push(child)
}
})
} else {
listChildrenPerLine[indexLine].push(children)
indexLine += 1
listChildrenPerLine[indexLine] = []
}
}
const listLine = listChildrenPerLine[listChildrenPerLine.length - 1]
if (listLine === '' || (listLine.length === 1 && listLine[0] === '')) {
listChildrenPerLine.pop()
}
return listChildrenPerLine.map((childrenPerLine, index) => (
<>
{childrenPerLine}
{index !== listChildrenPerLine.length - 1 && '\n'}
</>
))
} }
function parseArgs(raw) { function parseArgs(raw) {
@ -123,21 +159,64 @@ function parseArgs(raw) {
return result return result
} }
function convertLine(line, i) { const flattenReact = (children) => {
const cliRegex = /^(\$ )?python -m spacy/ if (children === null || children === undefined || children === false) {
if (cliRegex.test(line)) { return []
const text = line.replace(cliRegex, '') }
if (typeof children === 'string') {
return [children]
}
if (children.props) {
return flattenReact(children.props.children)
}
return children.flatMap(flattenReact)
}
const checkoutForComment = (line) => {
const lineParts = line.split(' # ')
if (lineParts.length !== 2) {
return line
}
return (
<>
{lineParts[0]}
{` `}
<span class="token comment">
{`# `}
{lineParts[1]}
</span>
</>
)
}
const convertLine = (line, prompt) => {
const lineFlat = flattenReact(line).join('')
if (!lineFlat.startsWith(`${prompt} `)) {
return line
}
const lineWithoutPrompt = lineFlat.slice(prompt.length + 1)
const cliRegex = /^python -m spacy/
if (!cliRegex.test(lineWithoutPrompt)) {
return <span data-prompt={prompt}>{checkoutForComment(lineWithoutPrompt)}</span>
}
const text = lineWithoutPrompt.replace(cliRegex, '')
const args = parseArgs(text) const args = parseArgs(text)
const cmd = Object.keys(args).map((key, i) => { const cmd = Object.keys(args).map((key, i) => {
const value = args[key] const value = args[key]
return value === null || value === true || i === 0 ? key : `${key} ${value}` return value === null || value === true || i === 0 ? key : `${key} ${value}`
}) })
return ( return (
<Fragment key={line}> <span data-prompt={prompt}>
<span data-prompt={i === 0 ? '$' : null} className={classes['cli-arg-subtle']}> <span className={classes['cli-arg-subtle']}>python -m</span> <span>spacy</span>{' '}
python -m
</span>{' '}
<span>spacy</span>{' '}
{cmd.map((item, j) => { {cmd.map((item, j) => {
const isCmd = j === 0 const isCmd = j === 0
const url = isCmd ? `/api/cli#${item.replace(' ', '-')}` : null const url = isCmd ? `/api/cli#${item.replace(' ', '-')}` : null
@ -158,47 +237,50 @@ function convertLine(line, i) {
</Fragment> </Fragment>
) )
})} })}
</Fragment> </span>
) )
}
const htmlLine = replacePrompt(highlightCode('bash', line), '$')
return htmlToReact(htmlLine)
} }
function formatCode(html, lang, prompt) { const addLineHighlight = (children, highlight) => {
if (lang === 'cli') { if (!highlight) {
const lines = html return children
.trim() }
.split('\n') const listHighlight = rangeParser(highlight)
.map((line) =>
line if (listHighlight.length === 0) {
.split(' | ') return children
.map((l, i) => convertLine(l, i)) }
.map((l, j) => (
<Fragment key={j}> return children.map((child, index) => {
{j !== 0 && <span> | </span>} const isHighlight = listHighlight.includes(index + 1)
{l} return (
</Fragment> <span
)) className={classNames({
'gatsby-highlight-code-line': isHighlight,
})}
key={index}
>
{child}
</span>
) )
return lines.map((line, i) => (
<Fragment key={i}>
{i !== 0 && <br />}
{line}
</Fragment>
))
}
const result = html
.split('\n')
.map((line, i) => {
let newLine = prompt ? replacePrompt(line, prompt, i === 0) : line
if (lang === 'diff' && !line.startsWith('<')) {
newLine = highlightCode('python', line)
}
return newLine
}) })
.join('\n') }
return htmlToReact(result)
const CodeHighlighted = ({ children, highlight }) => {
const [html, setHtml] = useState()
useEffect(
() =>
setHtml(
addLineHighlight(
splitLines(children).map((line) => convertLine(line, '$')),
highlight
)
),
[children, highlight]
)
return <>{html}</>
} }
export class Code extends React.Component { export class Code extends React.Component {
@ -255,16 +337,12 @@ export class Code extends React.Component {
) )
} }
const codeText = Array.isArray(children) ? children.join('') : children || ''
const highlightRange = highlight ? rangeParser.parse(highlight).filter((n) => n > 0) : []
const rawHtml = ['none', 'cli'].includes(lang)
? codeText
: highlightCode(lang, codeText, highlightRange)
const html = formatCode(rawHtml, lang, prompt)
return ( return (
<> <>
{title && <h4 className={classes['title']}>{title}</h4>} {title && <h4 className={classes['title']}>{title}</h4>}
<code className={codeClassNames}>{html}</code> <code className={codeClassNames}>
<CodeHighlighted highlight={highlight}>{children}</CodeHighlighted>
</code>
</> </>
) )
} }