diff --git a/examples/code-highlighting/index.js b/examples/code-highlighting/index.js
index 91278f48a..db5cddfcd 100644
--- a/examples/code-highlighting/index.js
+++ b/examples/code-highlighting/index.js
@@ -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 {
@@ -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()
- }
-
}
/**
diff --git a/lib/components/content.js b/lib/components/content.js
index 4ea5a47ea..f24caf6da 100644
--- a/lib/components/content.js
+++ b/lib/components/content.js
@@ -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 (
)
}
diff --git a/lib/components/editor.js b/lib/components/editor.js
index 8dad464a3..4ec4b777f 100644
--- a/lib/components/editor.js
+++ b/lib/components/editor.js
@@ -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.
*
diff --git a/lib/components/node.js b/lib/components/node.js
index 9261dbd13..89ac39326 100644
--- a/lib/components/node.js
+++ b/lib/components/node.js
@@ -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 (
)
}
@@ -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) => {
diff --git a/lib/models/node.js b/lib/models/node.js
index 7fe49664c..3ae72a31e 100644
--- a/lib/models/node.js
+++ b/lib/models/node.js
@@ -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',
diff --git a/lib/models/schema.js b/lib/models/schema.js
index 6b7cf1923..9058a8e4c 100644
--- a/lib/models/schema.js
+++ b/lib/models/schema.js
@@ -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.
diff --git a/lib/models/text.js b/lib/models/text.js
index 0d0f7ab24..56aa2195c 100644
--- a/lib/models/text.js
+++ b/lib/models/text.js
@@ -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',
])
/**
diff --git a/lib/plugins/core.js b/lib/plugins/core.js
index 3f61cea8e..065d3a944 100644
--- a/lib/plugins/core.js
+++ b/lib/plugins/core.js
@@ -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