mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-03-06 13:59:47 +01:00
265 lines
5.4 KiB
JavaScript
265 lines
5.4 KiB
JavaScript
import { Editor } from 'slate-react'
|
|
import { Value } from 'slate'
|
|
|
|
import Prism from 'prismjs'
|
|
import React from 'react'
|
|
import initialValue from './value.json'
|
|
|
|
/**
|
|
* Define our code components.
|
|
*
|
|
* @param {Object} props
|
|
* @return {Element}
|
|
*/
|
|
|
|
function CodeBlock(props) {
|
|
const { editor, node } = props
|
|
const language = node.data.get('language')
|
|
|
|
function onChange(event) {
|
|
editor.change(c =>
|
|
c.setNodeByKey(node.key, { data: { language: event.target.value } })
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div style={{ position: 'relative' }}>
|
|
<pre>
|
|
<code {...props.attributes}>{props.children}</code>
|
|
</pre>
|
|
<div
|
|
contentEditable={false}
|
|
style={{ position: 'absolute', top: '5px', right: '5px' }}
|
|
>
|
|
<select value={language} onChange={onChange}>
|
|
<option value="css">CSS</option>
|
|
<option value="js">JavaScript</option>
|
|
<option value="html">HTML</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function CodeBlockLine(props) {
|
|
return <div {...props.attributes}>{props.children}</div>
|
|
}
|
|
|
|
/**
|
|
* A helper function to return the content of a Prism `token`.
|
|
*
|
|
* @param {Object} token
|
|
* @return {String}
|
|
*/
|
|
|
|
function getContent(token) {
|
|
if (typeof token == 'string') {
|
|
return token
|
|
} else if (typeof token.content == 'string') {
|
|
return token.content
|
|
} else {
|
|
return token.content.map(getContent).join('')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The code highlighting example.
|
|
*
|
|
* @type {Component}
|
|
*/
|
|
|
|
class CodeHighlighting extends React.Component {
|
|
/**
|
|
* Deserialize the raw initial value.
|
|
*
|
|
* @type {Object}
|
|
*/
|
|
|
|
state = {
|
|
value: Value.fromJSON(initialValue),
|
|
}
|
|
|
|
/**
|
|
* Render.
|
|
*
|
|
* @return {Component}
|
|
*/
|
|
|
|
render() {
|
|
return (
|
|
<Editor
|
|
placeholder="Write some code..."
|
|
value={this.state.value}
|
|
onChange={this.onChange}
|
|
onKeyDown={this.onKeyDown}
|
|
renderNode={this.renderNode}
|
|
renderMark={this.renderMark}
|
|
decorateNode={this.decorateNode}
|
|
/>
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Render a Slate node.
|
|
*
|
|
* @param {Object} props
|
|
* @return {Element}
|
|
*/
|
|
|
|
renderNode = (props, next) => {
|
|
switch (props.node.type) {
|
|
case 'code':
|
|
return <CodeBlock {...props} />
|
|
case 'code_line':
|
|
return <CodeBlockLine {...props} />
|
|
default:
|
|
return next()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render a Slate mark.
|
|
*
|
|
* @param {Object} props
|
|
* @return {Element}
|
|
*/
|
|
|
|
renderMark = (props, next) => {
|
|
const { children, mark, attributes } = props
|
|
|
|
switch (mark.type) {
|
|
case 'comment':
|
|
return (
|
|
<span {...attributes} style={{ opacity: '0.33' }}>
|
|
{children}
|
|
</span>
|
|
)
|
|
case 'keyword':
|
|
return (
|
|
<span {...attributes} style={{ fontWeight: 'bold' }}>
|
|
{children}
|
|
</span>
|
|
)
|
|
case 'tag':
|
|
return (
|
|
<span {...attributes} style={{ fontWeight: 'bold' }}>
|
|
{children}
|
|
</span>
|
|
)
|
|
case 'punctuation':
|
|
return (
|
|
<span {...attributes} style={{ opacity: '0.75' }}>
|
|
{children}
|
|
</span>
|
|
)
|
|
default:
|
|
return next()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* On change, save the new value.
|
|
*
|
|
* @param {Change} change
|
|
*/
|
|
|
|
onChange = ({ value }) => {
|
|
this.setState({ value })
|
|
}
|
|
|
|
/**
|
|
* On key down inside code blocks, insert soft new lines.
|
|
*
|
|
* @param {Event} event
|
|
* @param {Change} change
|
|
* @param {Function} next
|
|
* @return {Change}
|
|
*/
|
|
|
|
onKeyDown = (event, change, next) => {
|
|
const { value } = change
|
|
const { startBlock } = value
|
|
|
|
if (event.key === 'Enter' && startBlock.type === 'code') {
|
|
change.insertText('\n')
|
|
return
|
|
}
|
|
|
|
next()
|
|
}
|
|
|
|
/**
|
|
* Decorate code blocks with Prism.js highlighting.
|
|
*
|
|
* @param {Node} node
|
|
* @return {Array}
|
|
*/
|
|
|
|
decorateNode = (node, next) => {
|
|
const others = next() || []
|
|
if (node.type != 'code') return others
|
|
|
|
const language = node.data.get('language')
|
|
const texts = node.getTexts().toArray()
|
|
const string = texts.map(t => t.text).join('\n')
|
|
const grammar = Prism.languages[language]
|
|
const tokens = Prism.tokenize(string, grammar)
|
|
const decorations = []
|
|
let startText = texts.shift()
|
|
let endText = startText
|
|
let startOffset = 0
|
|
let endOffset = 0
|
|
let start = 0
|
|
|
|
for (const token of tokens) {
|
|
startText = endText
|
|
startOffset = endOffset
|
|
|
|
const content = getContent(token)
|
|
const newlines = content.split('\n').length - 1
|
|
const length = content.length - newlines
|
|
const end = start + length
|
|
|
|
let available = startText.text.length - startOffset
|
|
let remaining = length
|
|
|
|
endOffset = startOffset + remaining
|
|
|
|
while (available < remaining && texts.length > 0) {
|
|
endText = texts.shift()
|
|
remaining = length - available
|
|
available = endText.text.length
|
|
endOffset = remaining
|
|
}
|
|
|
|
if (typeof token != 'string') {
|
|
const dec = {
|
|
anchor: {
|
|
key: startText.key,
|
|
offset: startOffset,
|
|
},
|
|
focus: {
|
|
key: endText.key,
|
|
offset: endOffset,
|
|
},
|
|
mark: {
|
|
type: token.type,
|
|
},
|
|
}
|
|
|
|
decorations.push(dec)
|
|
}
|
|
|
|
start = end
|
|
}
|
|
|
|
return [...others, ...decorations]
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Export.
|
|
*/
|
|
|
|
export default CodeHighlighting
|