mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-21 22:45:18 +02:00
Merge branch 'master' into schema-normalize
This commit is contained in:
@@ -51,9 +51,9 @@ An array of rules to initialize the `Html` serializer with, defining your schema
|
||||
Deserialize an HTML `string` into a [`State`](../models/state.md). How the string is deserialized will be determined by the rules that the `Html` serializer was constructed with.
|
||||
|
||||
### `Html.serialize`
|
||||
`Html.serialize(state: State) => String`
|
||||
`Html.serialize(state: State, [options: Object]) => String || Array`
|
||||
|
||||
Serialize a `state` into an HTML string. How the string is serialized will be determined by the rules that the `Html` serializer was constructed with.
|
||||
Serialize a `state` into an HTML string. How the string is serialized will be determined by the rules that the `Html` serializer was constructed with. If you pass `render: false` as an option, the return value will instead be an iterable list of the top-level React elements, to be rendered as children in your own React component.
|
||||
|
||||
|
||||
## Rules
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "slate",
|
||||
"description": "A completely customizable framework for building rich text editors.",
|
||||
"version": "0.14.15",
|
||||
"version": "0.14.16",
|
||||
"license": "MIT",
|
||||
"repository": "git://github.com/ianstormtaylor/slate.git",
|
||||
"main": "./lib/index.js",
|
||||
|
@@ -128,7 +128,7 @@ class Void extends React.Component {
|
||||
|
||||
renderLeaf = () => {
|
||||
const { node, schema, state } = this.props
|
||||
const child = node.getTexts().first()
|
||||
const child = node.getFirstText()
|
||||
const ranges = child.getRanges()
|
||||
const text = ''
|
||||
const marks = Mark.createSet()
|
||||
|
@@ -101,23 +101,53 @@ const Node = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Recursively find all ancestor nodes by `iterator`.
|
||||
* Recursively find all descendant nodes by `iterator`. Breadth first.
|
||||
*
|
||||
* @param {Function} iterator
|
||||
* @return {Node} node
|
||||
* @return {Node or Null} node
|
||||
*/
|
||||
|
||||
findDescendant(iterator) {
|
||||
return (
|
||||
this.nodes.find(iterator) ||
|
||||
this.nodes
|
||||
.map(node => node.kind == 'text' ? null : node.findDescendant(iterator))
|
||||
.find(exists => exists)
|
||||
)
|
||||
const found = this.nodes.find(iterator)
|
||||
if (found) return found
|
||||
|
||||
let descendantFound = null
|
||||
this.nodes.find(node => {
|
||||
if (node.kind != 'text') {
|
||||
descendantFound = node.findDescendant(iterator)
|
||||
return descendantFound
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
return descendantFound
|
||||
},
|
||||
|
||||
/**
|
||||
* Recursively filter all ancestor nodes with `iterator`.
|
||||
* Recursively find all descendant nodes by `iterator`. Depth first.
|
||||
*
|
||||
* @param {Function} iterator
|
||||
* @return {Node or Null} node
|
||||
*/
|
||||
|
||||
findDescendantDeep(iterator) {
|
||||
let descendantFound = null
|
||||
|
||||
const found = this.nodes.find(node => {
|
||||
if (node.kind != 'text') {
|
||||
descendantFound = node.findDescendantDeep(iterator)
|
||||
return descendantFound || iterator(node)
|
||||
}
|
||||
|
||||
return iterator(node) ? node : null
|
||||
})
|
||||
|
||||
return descendantFound || found
|
||||
},
|
||||
|
||||
/**
|
||||
* Recursively filter all descendant nodes with `iterator`.
|
||||
*
|
||||
* @param {Function} iterator
|
||||
* @return {List} nodes
|
||||
@@ -132,7 +162,7 @@ const Node = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Recursively filter all ancestor nodes with `iterator`, depth-first.
|
||||
* Recursively filter all descendant nodes with `iterator`, depth-first.
|
||||
*
|
||||
* @param {Function} iterator
|
||||
* @return {List} nodes
|
||||
@@ -282,14 +312,13 @@ const Node = {
|
||||
*/
|
||||
|
||||
getClosest(key, iterator) {
|
||||
let node = this.assertDescendant(key)
|
||||
|
||||
while (node = this.getParent(node)) {
|
||||
if (node == this) return null
|
||||
if (iterator(node)) return node
|
||||
let ancestors = this.getAncestors(key)
|
||||
if (!ancestors) {
|
||||
throw new Error(`Could not find a descendant node with key "${key}".`)
|
||||
}
|
||||
|
||||
return null
|
||||
// Exclude this node itself
|
||||
return ancestors.rest().findLast(iterator)
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -406,16 +435,7 @@ const Node = {
|
||||
getDescendant(key) {
|
||||
key = Normalize.key(key)
|
||||
|
||||
let child = this.getChild(key)
|
||||
if (child) return child
|
||||
|
||||
this.nodes.find((node) => {
|
||||
if (node.kind == 'text') return false
|
||||
child = node.getDescendant(key)
|
||||
return child
|
||||
})
|
||||
|
||||
return child
|
||||
return this.findDescendantDeep(node => node.key == key)
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -649,10 +669,10 @@ const Node = {
|
||||
let last
|
||||
|
||||
if (child.kind == 'block') {
|
||||
last = child.getTexts().last()
|
||||
last = child.getLastText()
|
||||
} else {
|
||||
const block = this.getClosestBlock(key)
|
||||
last = block.getTexts().last()
|
||||
last = block.getLastText()
|
||||
}
|
||||
|
||||
const next = this.getNextText(last)
|
||||
@@ -743,10 +763,13 @@ const Node = {
|
||||
|
||||
let node = null
|
||||
|
||||
this.nodes.forEach((child) => {
|
||||
if (child.kind == 'text') return
|
||||
const match = child.getParent(key)
|
||||
if (match) node = match
|
||||
this.nodes.find((child) => {
|
||||
if (child.kind == 'text') {
|
||||
return false
|
||||
} else {
|
||||
node = child.getParent(key)
|
||||
return node
|
||||
}
|
||||
})
|
||||
|
||||
return node
|
||||
@@ -755,7 +778,7 @@ const Node = {
|
||||
/**
|
||||
* Get the path of a descendant node by `key`.
|
||||
*
|
||||
* @param {String || Node} node
|
||||
* @param {String || Node} key
|
||||
* @return {Array}
|
||||
*/
|
||||
|
||||
@@ -764,17 +787,50 @@ const Node = {
|
||||
|
||||
if (key == this.key) return []
|
||||
|
||||
let child = this.assertDescendant(key)
|
||||
let path = []
|
||||
let childKey = key
|
||||
let parent
|
||||
|
||||
while (parent = this.getParent(child)) {
|
||||
const index = parent.nodes.indexOf(child)
|
||||
// Efficient with getParent memoization
|
||||
while (parent = this.getParent(childKey)) {
|
||||
const index = parent.nodes.findIndex(n => n.key === childKey)
|
||||
path.unshift(index)
|
||||
child = parent
|
||||
childKey = parent.key
|
||||
}
|
||||
|
||||
return path
|
||||
if (childKey === key) {
|
||||
// Did not loop once, meaning we could not find the child
|
||||
throw new Error(`Could not find a descendant node with key "${key}".`)
|
||||
} else {
|
||||
return path
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the path of ancestors of a descendant node by `key`.
|
||||
*
|
||||
* @param {String || Node} node
|
||||
* @return {List<Node> or Null}
|
||||
*/
|
||||
|
||||
getAncestors(key) {
|
||||
key = Normalize.key(key)
|
||||
|
||||
if (key == this.key) return List()
|
||||
if (this.hasChild(key)) return List([this])
|
||||
|
||||
let ancestors
|
||||
this.nodes.find((node) => {
|
||||
if (node.kind == 'text') return false
|
||||
ancestors = node.getAncestors(key)
|
||||
return ancestors
|
||||
})
|
||||
|
||||
if (ancestors) {
|
||||
return ancestors.unshift(this)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -819,10 +875,10 @@ const Node = {
|
||||
let first
|
||||
|
||||
if (child.kind == 'block') {
|
||||
first = child.getTexts().first()
|
||||
first = child.getFirstText()
|
||||
} else {
|
||||
const block = this.getClosestBlock(key)
|
||||
first = block.getTexts().first()
|
||||
first = block.getFirstText()
|
||||
}
|
||||
|
||||
const previous = this.getPreviousText(first)
|
||||
@@ -876,6 +932,34 @@ const Node = {
|
||||
}, Block.createList())
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the first child text node.
|
||||
*
|
||||
* @return {Node || Null} node
|
||||
*/
|
||||
|
||||
getFirstText() {
|
||||
return this.findDescendantDeep(node => node.kind == 'text')
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the last child text node.
|
||||
*
|
||||
* @return {Node} node
|
||||
*/
|
||||
|
||||
getLastText() {
|
||||
let descendantFound = null
|
||||
|
||||
const found = this.nodes.findLast((node) => {
|
||||
if (node.kind == 'text') return true
|
||||
descendantFound = node.getLastText()
|
||||
return descendantFound
|
||||
})
|
||||
|
||||
return descendantFound || found
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all of the text nodes in a `range`.
|
||||
*
|
||||
@@ -1070,10 +1154,13 @@ const Node = {
|
||||
*/
|
||||
|
||||
removeDescendant(key) {
|
||||
key = Normalize.key(key)
|
||||
|
||||
let node = this
|
||||
const desc = node.assertDescendant(key)
|
||||
let parent = node.getParent(desc)
|
||||
const index = parent.nodes.indexOf(desc)
|
||||
let parent = node.getParent(key)
|
||||
if (!parent) throw new Error(`Could not find a descendant node with key "${key}".`)
|
||||
|
||||
const index = parent.nodes.findIndex(n => n.key === key)
|
||||
const isParent = node == parent
|
||||
const nodes = parent.nodes.splice(index, 1)
|
||||
|
||||
@@ -1183,8 +1270,22 @@ const Node = {
|
||||
*/
|
||||
|
||||
updateDescendant(node) {
|
||||
this.assertDescendant(node)
|
||||
return this.mapDescendants(d => d.key == node.key ? node : d)
|
||||
let found = false
|
||||
|
||||
const result = this.mapDescendants(d => {
|
||||
if (d.key == node.key) {
|
||||
found = true
|
||||
return node
|
||||
} else {
|
||||
return d
|
||||
}
|
||||
})
|
||||
|
||||
if (!found) {
|
||||
throw new Error(`Could not update descendant node with key "${node.key}".`)
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1210,6 +1311,8 @@ memoize(Node, [
|
||||
'filterDescendants',
|
||||
'filterDescendantsDeep',
|
||||
'findDescendant',
|
||||
'findDescendantDeep',
|
||||
'getAncestors',
|
||||
'getBlocks',
|
||||
'getBlocksAtRange',
|
||||
'getCharactersAtRange',
|
||||
@@ -1228,6 +1331,7 @@ memoize(Node, [
|
||||
'getDepth',
|
||||
'getDescendant',
|
||||
'getDescendantDecorators',
|
||||
'getFirstText',
|
||||
'getFragmentAtRange',
|
||||
'getFurthest',
|
||||
'getFurthestBlock',
|
||||
@@ -1235,6 +1339,7 @@ memoize(Node, [
|
||||
'getHighestChild',
|
||||
'getHighestOnlyChildParent',
|
||||
'getInlinesAtRange',
|
||||
'getLastText',
|
||||
'getMarksAtRange',
|
||||
'getNextBlock',
|
||||
'getNextSibling',
|
||||
|
@@ -149,7 +149,7 @@ class Selection extends new Record(DEFAULTS) {
|
||||
|
||||
hasAnchorAtStartOf(node) {
|
||||
if (this.anchorOffset != 0) return false
|
||||
const first = node.kind == 'text' ? node : node.getTexts().first()
|
||||
const first = node.kind == 'text' ? node : node.getFirstText()
|
||||
return this.anchorKey == first.key
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ class Selection extends new Record(DEFAULTS) {
|
||||
*/
|
||||
|
||||
hasAnchorAtEndOf(node) {
|
||||
const last = node.kind == 'text' ? node : node.getTexts().last()
|
||||
const last = node.kind == 'text' ? node : node.getLastText()
|
||||
return this.anchorKey == last.key && this.anchorOffset == last.length
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ class Selection extends new Record(DEFAULTS) {
|
||||
*/
|
||||
|
||||
hasFocusAtEndOf(node) {
|
||||
const last = node.kind == 'text' ? node : node.getTexts().last()
|
||||
const last = node.kind == 'text' ? node : node.getLastText()
|
||||
return this.focusKey == last.key && this.focusOffset == last.length
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ class Selection extends new Record(DEFAULTS) {
|
||||
|
||||
hasFocusAtStartOf(node) {
|
||||
if (this.focusOffset != 0) return false
|
||||
const first = node.kind == 'text' ? node : node.getTexts().first()
|
||||
const first = node.kind == 'text' ? node : node.getFirstText()
|
||||
return this.focusKey == first.key
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ class Selection extends new Record(DEFAULTS) {
|
||||
const { isExpanded, startKey, startOffset } = this
|
||||
if (isExpanded) return false
|
||||
if (startOffset != 0) return false
|
||||
const first = node.kind == 'text' ? node : node.getTexts().first()
|
||||
const first = node.kind == 'text' ? node : node.getFirstText()
|
||||
return startKey == first.key
|
||||
}
|
||||
|
||||
@@ -275,7 +275,7 @@ class Selection extends new Record(DEFAULTS) {
|
||||
isAtEndOf(node) {
|
||||
const { endKey, endOffset, isExpanded } = this
|
||||
if (isExpanded) return false
|
||||
const last = node.kind == 'text' ? node : node.getTexts().last()
|
||||
const last = node.kind == 'text' ? node : node.getLastText()
|
||||
return endKey == last.key && endOffset == last.length
|
||||
}
|
||||
|
||||
|
@@ -45,7 +45,7 @@ class State extends new Record(DEFAULTS) {
|
||||
let selection = Selection.create(properties.selection)
|
||||
|
||||
if (selection.isUnset) {
|
||||
const text = document.getTexts().first()
|
||||
const text = document.getFirstText()
|
||||
selection = selection.collapseToStartOf(text)
|
||||
}
|
||||
|
||||
|
@@ -12,6 +12,8 @@ It doesn't hardcode any information about the schema itself (like which tag mean
|
||||
|
||||
It handles all of the heavy lifting of actually parsing the HTML, and iterating over the elements, and all you have to supply it is a `serialize()` and `deserialize()` function for each type of [`Node`](../models#node) or [`Mark`](../models/#mark) you want it to handle.
|
||||
|
||||
If called with `{render: false}` as the optional second argument, the serializer will return an iterable list of the top-level React elements generated, instead of automatically rendering these to a markup string.
|
||||
|
||||
|
||||
#### Raw
|
||||
|
||||
@@ -20,4 +22,3 @@ The `Raw` serializer is the simplest serializer, which translates a [`State`](..
|
||||
It doesn't just use Immutable.js's [`.toJSON()`](https://facebook.github.io/immutable-js/docs/#/List/toJS) method. Instead, it performs a little bit of "minifying" logic to reduce unnecessary information from being in the raw output.
|
||||
|
||||
It also transforms [`Text`](../models#text) nodes's content from being organized by [`Characters`](../models#character) into the concept of "ranges", which have a unique set of [`Marks`](../models#mark).
|
||||
|
||||
|
@@ -225,12 +225,16 @@ class Html {
|
||||
* Serialize a `state` object into an HTML string.
|
||||
*
|
||||
* @param {State} state
|
||||
* @return {String} html
|
||||
* @param {Object} options
|
||||
* @property {Boolean} render
|
||||
* @return {String|Array} html
|
||||
*/
|
||||
|
||||
serialize = (state) => {
|
||||
serialize = (state, options = {}) => {
|
||||
const { document } = state
|
||||
const elements = document.nodes.map(this.serializeNode)
|
||||
if (options.render === false) return elements
|
||||
|
||||
const html = ReactDOMServer.renderToStaticMarkup(<body>{elements}</body>)
|
||||
const inner = html.slice(6, -7)
|
||||
return inner
|
||||
|
@@ -70,7 +70,7 @@ export function _delete(transform) {
|
||||
after = selection.collapseToEndOf(previous)
|
||||
}
|
||||
} else {
|
||||
const last = previous.getTexts().last()
|
||||
const last = previous.getLastText()
|
||||
after = selection.collapseToEndOf(last)
|
||||
}
|
||||
}
|
||||
@@ -113,6 +113,7 @@ export function deleteBackward(transform, n = 1) {
|
||||
export function deleteForward(transform, n = 1) {
|
||||
const { state } = transform
|
||||
const { selection } = state
|
||||
|
||||
return transform
|
||||
.deleteForwardAtRange(selection, n)
|
||||
.collapseToEnd()
|
||||
@@ -156,9 +157,10 @@ export function insertFragment(transform, fragment) {
|
||||
|
||||
if (!fragment.length) return transform
|
||||
|
||||
const lastText = fragment.getTexts().last()
|
||||
const lastText = fragment.getLastText()
|
||||
const lastInline = fragment.getClosestInline(lastText)
|
||||
const beforeTexts = document.getTexts()
|
||||
const appending = selection.hasEdgeAtEndOf(document.getDescendant(selection.endKey))
|
||||
|
||||
transform.unsetSelection()
|
||||
transform.insertFragmentAtRange(selection, fragment)
|
||||
@@ -167,17 +169,13 @@ export function insertFragment(transform, fragment) {
|
||||
|
||||
const keys = beforeTexts.map(text => text.key)
|
||||
const news = document.getTexts().filter(n => !keys.includes(n.key))
|
||||
const text = news.size ? news.takeLast(2).first() : null
|
||||
const text = appending ? news.last() : news.takeLast(2).first()
|
||||
let after
|
||||
|
||||
if (text && lastInline) {
|
||||
after = selection.collapseToEndOf(text)
|
||||
}
|
||||
|
||||
else if (text && lastInline) {
|
||||
after = selection.collapseToStart()
|
||||
}
|
||||
|
||||
else if (text) {
|
||||
after = selection
|
||||
.collapseToStartOf(text)
|
||||
@@ -495,7 +493,7 @@ export function wrapInline(transform, properties) {
|
||||
}
|
||||
|
||||
else if (selection.startOffset == 0) {
|
||||
const text = previous ? document.getNextText(previous) : document.getTexts().first()
|
||||
const text = previous ? document.getNextText(previous) : document.getFirstText()
|
||||
after = selection.moveToRangeOf(text)
|
||||
}
|
||||
|
||||
|
@@ -65,6 +65,8 @@ export function deleteAtRange(transform, range, options = {}) {
|
||||
|
||||
let { state } = transform
|
||||
let { document } = state
|
||||
|
||||
// split the nodes at range, within the common ancestor
|
||||
let ancestor = document.getCommonAncestor(startKey, endKey)
|
||||
let startChild = ancestor.getHighestChild(startKey)
|
||||
let endChild = ancestor.getHighestChild(endKey)
|
||||
@@ -76,18 +78,16 @@ export function deleteAtRange(transform, range, options = {}) {
|
||||
|
||||
state = transform.state
|
||||
document = state.document
|
||||
ancestor = document.getCommonAncestor(startKey, endKey)
|
||||
const startBlock = document.getClosestBlock(startKey)
|
||||
const endBlock = document.getClosestBlock(document.getNextText(endKey))
|
||||
|
||||
// remove all of the nodes between range
|
||||
ancestor = document.getCommonAncestor(startKey, endKey)
|
||||
startChild = ancestor.getHighestChild(startKey)
|
||||
endChild = ancestor.getHighestChild(endKey)
|
||||
|
||||
const startIndex = ancestor.nodes.indexOf(startChild)
|
||||
const endIndex = ancestor.nodes.indexOf(endChild)
|
||||
const middles = ancestor.nodes.slice(
|
||||
startIndex + 1,
|
||||
endIndex + 1
|
||||
)
|
||||
const middles = ancestor.nodes.slice(startIndex + 1, endIndex + 1)
|
||||
|
||||
if (middles.size) {
|
||||
// remove first nodes directly so the document is not normalized
|
||||
@@ -110,6 +110,9 @@ export function deleteAtRange(transform, range, options = {}) {
|
||||
if (normalize) {
|
||||
transform.normalizeNodeByKey(ancestor.key)
|
||||
}
|
||||
|
||||
transform.normalizeDocument()
|
||||
|
||||
return transform
|
||||
}
|
||||
|
||||
|
@@ -1,11 +1,14 @@
|
||||
|
||||
import assert from 'assert'
|
||||
import type from 'type-of'
|
||||
import fs from 'fs'
|
||||
import readMetadata from 'read-metadata'
|
||||
import strip from '../helpers/strip-dynamic'
|
||||
import { Html, Json, Plain, Raw } from '../..'
|
||||
import { equal, strictEqual } from '../helpers/assert-json'
|
||||
import { resolve } from 'path'
|
||||
import React from 'react'
|
||||
import { Iterable } from 'immutable'
|
||||
|
||||
/**
|
||||
* Tests.
|
||||
@@ -46,6 +49,14 @@ describe('serializers', () => {
|
||||
strictEqual(serialized, expected.trim())
|
||||
})
|
||||
}
|
||||
|
||||
it('optionally returns an iterable list of React elements', () => {
|
||||
const html = new Html(require('./fixtures/html/serialize/block-nested').default)
|
||||
const input = require('./fixtures/html/serialize/block-nested/input.js').default
|
||||
const serialized = html.serialize(input, { render: false })
|
||||
assert(Iterable.isIterable(serialized), 'did not return an interable list')
|
||||
assert(React.isValidElement(serialized.first()), 'did not return valid React elements')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
@@ -0,0 +1,17 @@
|
||||
|
||||
nodes:
|
||||
- kind: block
|
||||
type: list-item
|
||||
nodes:
|
||||
- kind: text
|
||||
text: fragment
|
||||
- kind: block
|
||||
type: list-item
|
||||
nodes:
|
||||
- kind: text
|
||||
text: second fragment
|
||||
- kind: block
|
||||
type: list-item
|
||||
nodes:
|
||||
- kind: text
|
||||
text: third fragment
|
@@ -0,0 +1,41 @@
|
||||
|
||||
import assert from 'assert'
|
||||
import path from 'path'
|
||||
import readMetadata from 'read-metadata'
|
||||
import { Raw } from '../../../../../..'
|
||||
|
||||
export default function (state) {
|
||||
const file = path.resolve(__dirname, 'fragment.yaml')
|
||||
const raw = readMetadata.sync(file)
|
||||
const fragment = Raw.deserialize(raw, { terse: true }).document
|
||||
|
||||
const { document, selection } = state
|
||||
const texts = document.getTexts()
|
||||
const first = texts.first()
|
||||
const range = selection.merge({
|
||||
anchorKey: first.key,
|
||||
anchorOffset: first.length,
|
||||
focusKey: first.key,
|
||||
focusOffset: first.length
|
||||
})
|
||||
|
||||
const next = state
|
||||
.transform()
|
||||
.moveTo(range)
|
||||
.insertFragment(fragment)
|
||||
.apply()
|
||||
|
||||
const last = next.document.getTexts().last()
|
||||
|
||||
assert.deepEqual(
|
||||
next.selection.toJS(),
|
||||
range.merge({
|
||||
anchorKey: last.key,
|
||||
anchorOffset: last.length,
|
||||
focusKey: last.key,
|
||||
focusOffset: last.length
|
||||
}).toJS()
|
||||
)
|
||||
|
||||
return next
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
|
||||
nodes:
|
||||
- kind: block
|
||||
type: paragraph
|
||||
nodes:
|
||||
- kind: text
|
||||
text: word
|
@@ -0,0 +1,17 @@
|
||||
|
||||
nodes:
|
||||
- kind: block
|
||||
type: paragraph
|
||||
nodes:
|
||||
- kind: text
|
||||
text: wordfragment
|
||||
- kind: block
|
||||
type: list-item
|
||||
nodes:
|
||||
- kind: text
|
||||
text: second fragment
|
||||
- kind: block
|
||||
type: list-item
|
||||
nodes:
|
||||
- kind: text
|
||||
text: third fragment
|
Reference in New Issue
Block a user