1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-09-01 03:11:44 +02:00

add rendering of decorators from schema

This commit is contained in:
Ian Storm Taylor
2016-08-13 19:38:59 -07:00
parent cb1d641e43
commit dbcb9e531f
8 changed files with 189 additions and 169 deletions

View File

@@ -55,7 +55,39 @@ function CodeBlock(props) {
const schema = {
nodes: {
code: CodeBlock
code: {
component: CodeBlock,
decorator: (block, text) => {
let characters = text.characters.asMutable()
const language = block.data.get('language')
const string = text.text
const grammar = Prism.languages[language]
const tokens = Prism.tokenize(string, grammar)
let offset = 0
for (const token of tokens) {
if (typeof token == 'string') {
offset += token.length
continue
}
const length = offset + token.content.length
const type = `highlight-${token.type}`
for (let i = offset; i < length; i++) {
let char = characters.get(i)
let { marks } = char
marks = marks.add(Mark.create({ type }))
char = char.merge({ marks })
characters = characters.set(i, char)
}
offset = length
}
return characters.asImmutable()
}
}
},
marks: {
'highlight-comment': {
@@ -131,7 +163,6 @@ class CodeHighlighting extends React.Component {
<Editor
schema={schema}
state={this.state.state}
renderDecorations={this.renderDecorations}
onKeyDown={this.onKeyDown}
onChange={this.onChange}
/>
@@ -139,47 +170,6 @@ class CodeHighlighting extends React.Component {
)
}
/**
* Render decorations on `text` nodes inside code blocks.
*
* @param {Text} text
* @param {Block} block
* @return {Characters}
*/
renderDecorations = (text, block) => {
if (block.type != 'code') return text.characters
let characters = text.characters.asMutable()
const language = block.data.get('language')
const string = text.text
const grammar = Prism.languages[language]
const tokens = Prism.tokenize(string, grammar)
let offset = 0
for (const token of tokens) {
if (typeof token == 'string') {
offset += token.length
continue
}
const length = offset + token.content.length
const type = `highlight-${token.type}`
for (let i = offset; i < length; i++) {
let char = characters.get(i)
let { marks } = char
marks = marks.add(Mark.create({ type }))
char = char.merge({ marks })
characters = characters.set(i, char)
}
offset = length
}
return characters.asImmutable()
}
}
/**

View File

@@ -55,7 +55,6 @@ class Content extends React.Component {
onPaste: React.PropTypes.func.isRequired,
onSelect: React.PropTypes.func.isRequired,
readOnly: React.PropTypes.bool.isRequired,
renderDecorations: React.PropTypes.func.isRequired,
schema: React.PropTypes.object,
spellCheck: React.PropTypes.bool.isRequired,
state: React.PropTypes.object.isRequired,
@@ -141,27 +140,18 @@ class Content extends React.Component {
*/
getPoint(element, offset) {
const { state, editor } = this.props
const { document } = state
const schema = editor.getSchema()
const offsetKey = OffsetKey.findKey(element, offset)
const ranges = this.getRanges(offsetKey.key)
const { key } = offsetKey
const node = document.getDescendant(key)
const decorators = document.getDescendantDecorators(key, schema)
const ranges = node.getRanges(decorators)
const point = OffsetKey.findPoint(offsetKey, ranges)
return point
}
/**
* Get the ranges for a text node by `key`.
*
* @param {String} key
* @return {List}
*/
getRanges(key) {
const { state, renderDecorations } = this.props
const node = state.document.getDescendant(key)
const block = state.document.getClosestBlock(node)
const ranges = node.getDecoratedRanges(block, renderDecorations)
return ranges
}
/**
* On before input, bubble up.
*
@@ -372,7 +362,7 @@ class Content extends React.Component {
e.preventDefault()
const window = getWindow(e.target)
const { state, renderDecorations } = this.props
const { state } = this.props
const { selection } = state
const { dataTransfer, x, y } = e.nativeEvent
const transfer = new Transfer(dataTransfer)
@@ -429,14 +419,23 @@ class Content extends React.Component {
debug('onInput')
const window = getWindow(e.target)
let { state, renderDecorations } = this.props
const { selection } = state
// Get the selection point.
const native = window.getSelection()
const { anchorNode, anchorOffset, focusOffset } = native
const point = this.getPoint(anchorNode, anchorOffset)
const { key, index, start, end } = point
const ranges = this.getRanges(key)
// Get the range in question.
const { state, editor } = this.props
const { document, selection } = state
const schema = editor.getSchema()
const decorators = document.getDescendantDecorators(key, schema)
const node = document.getDescendant(key)
const ranges = node.getRanges(decorators)
const range = ranges.get(index)
// Get the text information.
const isLast = index == ranges.size - 1
const { text, marks } = range
let { textContent } = anchorNode
@@ -457,7 +456,7 @@ class Content extends React.Component {
const after = selection.collapseToEnd().moveForward(delta)
// Create an updated state with the text replaced.
state = state
const next = state
.transform()
.moveTo({
anchorKey: key,
@@ -471,7 +470,7 @@ class Content extends React.Component {
.apply()
// Change the current state.
this.onChange(state)
this.onChange(next)
}
/**
@@ -562,7 +561,7 @@ class Content extends React.Component {
if (isNonEditable(e)) return
const window = getWindow(e.target)
const { state, renderDecorations } = this.props
const { state } = this.props
let { document, selection } = state
const native = window.getSelection()
const data = {}
@@ -683,7 +682,7 @@ class Content extends React.Component {
*/
renderNode = (node) => {
const { editor, renderDecorations, schema, state } = this.props
const { editor, schema, state } = this.props
return (
<Node
@@ -692,7 +691,6 @@ class Content extends React.Component {
schema={schema}
state={state}
editor={editor}
renderDecorations={renderDecorations}
/>
)
}

View File

@@ -62,7 +62,6 @@ class Editor extends React.Component {
placeholderStyle: React.PropTypes.object,
plugins: React.PropTypes.array,
readOnly: React.PropTypes.bool,
renderDecorations: React.PropTypes.func,
schema: React.PropTypes.object,
spellCheck: React.PropTypes.bool,
state: React.PropTypes.object.isRequired,
@@ -264,7 +263,6 @@ class Editor extends React.Component {
editor={this}
onChange={this.onChange}
readOnly={this.props.readOnly}
renderDecorations={this.renderDecorations}
schema={this.state.schema}
spellCheck={this.props.spellCheck}
state={this.state.state}
@@ -273,24 +271,6 @@ class Editor extends React.Component {
)
}
/**
* Render the decorations for a `text`, cascading through the plugins.
*
* @param {Block} text
* @param {Block} block
* @return {Object}
*/
renderDecorations = (text, block) => {
for (const plugin of this.state.plugins) {
if (!plugin.renderDecorations) continue
const style = plugin.renderDecorations(text, block, this.state.state, this)
if (style) return style
}
return text.characters
}
/**
* Resolve the editor's current plugins from `props` when they change.
*

View File

@@ -31,13 +31,11 @@ class Node extends React.Component {
*/
static propTypes = {
block: React.PropTypes.object,
editor: React.PropTypes.object.isRequired,
node: React.PropTypes.object.isRequired,
renderDecorations: React.PropTypes.func.isRequired,
schema: React.PropTypes.object.isRequired,
state: React.PropTypes.object.isRequired
};
}
/**
* Constructor.
@@ -122,8 +120,8 @@ class Node extends React.Component {
props.node.kind == 'text' &&
props.block != this.props.block
) {
const nextRanges = props.node.getDecoratedRanges(props.block, props.renderDecorations)
const ranges = this.props.node.getDecoratedRanges(this.props.block, this.props.renderDecorations)
const nextRanges = props.node.getRanges(props.decorators)
const ranges = this.props.node.getRanges(this.props.decorators)
if (!ranges.equals(nextRanges)) return true
}
@@ -213,17 +211,13 @@ class Node extends React.Component {
*/
renderNode = (child) => {
const { editor, node, renderDecorations, schema, state } = this.props
const block = node.kind == 'block' ? node : this.props.block
return (
<Node
key={child.key}
block={block}
node={child}
schema={schema}
state={state}
editor={editor}
renderDecorations={renderDecorations}
editor={this.props.editor}
schema={this.props.schema}
state={this.props.state}
/>
)
}
@@ -279,8 +273,10 @@ class Node extends React.Component {
*/
renderText = () => {
const { node, block, renderDecorations } = this.props
const ranges = node.getDecoratedRanges(block, renderDecorations)
const { node, schema, state } = this.props
const { document } = state
const decorators = document.getDescendantDecorators(node.key, schema)
const ranges = node.getRanges(decorators)
let offset = 0
const leaves = ranges.map((range, i, original) => {

View File

@@ -349,6 +349,39 @@ const Node = {
return schema.__getComponent(this)
},
/**
* Get the decorations for the node from a `schema`.
*
* @param {Schema} schema
* @return {Array}
*/
getDecorators(schema) {
return schema.__getDecorators(this)
},
/**
* Get the decorations for a descendant by `key` given a `schema`.
*
* @param {String} key
* @param {Schema} schema
* @return {Array}
*/
getDescendantDecorators(key, schema) {
const descendant = this.assertDescendant(key)
let child = this.getHighestChild(key)
let decorators = []
while (child != descendant) {
decorators = decorators.concat(child.getDecorators(schema))
child = child.getHighestChild(key)
}
decorators = decorators.concat(descendant.getDecorators(schema))
return decorators
},
/**
* Get a descendant node by `key`.
*
@@ -1104,9 +1137,9 @@ const Node = {
memoize(Node, [
'assertChild',
'assertDescendant',
'findDescendant',
'filterDescendants',
'filterDescendantsDeep',
'findDescendant',
'getBlocks',
'getBlocksAtRange',
'getCharactersAtRange',
@@ -1121,8 +1154,10 @@ memoize(Node, [
'getClosestBlock',
'getClosestInline',
'getComponent',
'getDescendant',
'getDecorators',
'getDepth',
'getDescendant',
'getDescendantDecorators',
'getFragmentAtRange',
'getFurthest',
'getFurthestBlock',
@@ -1137,9 +1172,9 @@ memoize(Node, [
'getOffset',
'getOffsetAtRange',
'getParent',
'getPreviousBlock',
'getPreviousSibling',
'getPreviousText',
'getPreviousBlock',
'getTextAtOffset',
'getTextDirection',
'getTexts',

View File

@@ -155,6 +155,27 @@ class Schema extends new Record(DEFAULTS) {
return match.component
}
/**
* Return the decorators for an `object`.
*
* This method is private, because it should always be called on one of the
* often-changing immutable objects instead, since it will be memoized for
* much better performance.
*
* @param {Mixed} object
* @return {Array}
*/
__getDecorators(object) {
return this.rules
.filter(rule => rule.match(object) && rule.decorator)
.map((rule) => {
return (text) => {
return rule.decorator(object, text)
}
})
}
/**
* Validate an `object` against the schema, returning the failing rule and
* reason if the object is invalid, or void if it's valid.

View File

@@ -99,84 +99,85 @@ class Text extends new Record(DEFAULTS) {
}
/**
* Get the decorated characters.
* Derive a set of decorated characters with `decorators`.
*
* @param {Block} block
* @param {Function} decorator
* @return {List} characters
* @param {Array} decorators
* @return {List}
*/
getDecoratedCharacters(block, decorator) {
return decorator(this, block)
getDecorations(decorators) {
const node = this
let { characters } = node
if (characters.size == 0) return characters
for (const decorator of decorators) {
const decorateds = decorator(node)
characters = characters.merge(decorateds)
}
return characters
}
/**
* Get the decorated characters grouped by marks.
* Get the decorations for the node from a `schema`.
*
* @param {Block} block
* @param {Function} decorator
* @return {List} ranges
* @param {Schema} schema
* @return {Array}
*/
getDecoratedRanges(block, decorator) {
const decorations = this.getDecoratedCharacters(block, decorator)
return this.getRangesForCharacters(decorations)
}
/**
* Get the characters grouped by marks.
*
* @return {List} ranges
*/
getRanges() {
return this.getRangesForCharacters(this.characters)
getDecorators(schema) {
return schema.__getDecorators(this)
}
/**
* Derive the ranges for a list of `characters`.
*
* @param {List} characters
* @param {Array || Void} decorators (optional)
* @return {List}
*/
getRangesForCharacters(characters) {
getRanges(decorators = []) {
const node = this
const list = new List()
let characters = this.getDecorations(decorators)
// If there are no characters, return one empty range.
if (characters.size == 0) {
let ranges = new List()
ranges = ranges.push(new Range())
return ranges
return list.push(new Range())
}
return characters
.toList()
.reduce((ranges, char, i) => {
const { marks, text } = char
// Convert the now-decorated characters into ranges.
const ranges = characters.reduce((memo, char, i) => {
const { marks, text } = char
// The first one can always just be created.
if (i == 0) {
return ranges.push(new Range({ text, marks }))
}
// The first one can always just be created.
if (i == 0) {
return memo.push(new Range({ text, marks }))
}
// Otherwise, compare to the previous and see if a new range should be
// created, or whether the text should be added to the previous range.
const previous = characters.get(i - 1)
const prevMarks = previous.marks
const added = marks.filterNot(mark => prevMarks.includes(mark))
const removed = prevMarks.filterNot(mark => marks.includes(mark))
const isSame = !added.size && !removed.size
// Otherwise, compare to the previous and see if a new range should be
// created, or whether the text should be added to the previous range.
const previous = characters.get(i - 1)
const prevMarks = previous.marks
const added = marks.filterNot(mark => prevMarks.includes(mark))
const removed = prevMarks.filterNot(mark => marks.includes(mark))
const isSame = !added.size && !removed.size
// If the marks are the same, add the text to the previous range.
if (isSame) {
const index = ranges.size - 1
let prevRange = ranges.get(index)
let prevText = prevRange.get('text')
prevRange = prevRange.set('text', prevText += text)
return ranges.set(index, prevRange)
}
// If the marks are the same, add the text to the previous range.
if (isSame) {
const index = memo.size - 1
let prevRange = memo.get(index)
let prevText = prevRange.get('text')
prevRange = prevRange.set('text', prevText += text)
return memo.set(index, prevRange)
}
// Otherwise, create a new range.
return ranges.push(new Range({ text, marks }))
}, new List())
// Otherwise, create a new range.
return memo.push(new Range({ text, marks }))
}, list)
// Return the ranges.
return ranges
}
/**
@@ -246,11 +247,10 @@ class Text extends new Record(DEFAULTS) {
*/
memoize(Text.prototype, [
'getDecoratedCharacters',
'getDecoratedRanges',
'getDecorations',
'getDecorators',
'getRanges',
'getRangesForCharacters',
'validate'
'validate',
])
/**

View File

@@ -160,11 +160,12 @@ function Plugin(options = {}) {
*/
function onBeforeInput(e, data, state, editor) {
const { renderDecorations } = editor
const { startOffset, startText, startBlock } = state
const { document, startKey, startOffset, startText } = state
// Determine what the characters would be if natively inserted.
const prevChars = startText.getDecoratedCharacters(startBlock, renderDecorations)
const schema = editor.getSchema()
const decorators = document.getDescendantDecorators(startKey, schema)
const prevChars = startText.getDecorations(decorators)
const prevChar = prevChars.get(startOffset - 1)
const char = Character.create({
text: e.data,
@@ -183,8 +184,7 @@ function Plugin(options = {}) {
.apply()
const nextText = next.startText
const nextBlock = next.startBlock
const nextChars = nextText.getDecoratedCharacters(nextBlock, renderDecorations)
const nextChars = nextText.getDecorations(decorators)
// We do not have to re-render if the current selection is collapsed, the
// current node is not empty, there are no marks on the cursor, and the