1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-13 18:53:59 +02:00
* remove some key usage from core, refactor Operations.apply

* undeprecate some methods

* convert more key usage to paths

* update deprecations

* convert selection commands to use all paths

* refactor word boundary selection logic

* convert many at-range commands to use paths

* convert wrapBlock and wrapInline to not use keys

* cleanup

* remove chainability from editor

* simplify commands, queries and middleware

* convert deleteAtRange

* remove key usage from schema, deprecate *ByKey methods

* migrate *ByKey tests, remove index from *ByPath signatures

* rename at-current-range tests

* deprecate mode key usage, migrate more tests away from keys

* deprecate range and point methods which rely on keys to work

* refactor insertBlock, without fixing warnings

* add pathRef/pointRef, fix insertBlock/Inline deprecations, work on insertFragment

* refactor insertFragment

* get rich-text example rendering

* fix lint

* refactor query files, fix more tests

* remove unused queries, refactor others

* deprecate splitDescendantsByPath

* merge master

* add typescript, convert slate, slate-hyperscript, slate-plain-serializer

* add Point, Path, Range, Annotation tests

* add Annotation, Change, Element, Fragment, Mark, Range, Selection, Value interfaces tests

* add Operation and Text tests

* add Node tests

* get operations and normalization tests working for slate

* get *AtPath command tests passing

* rename *AtPath command tests

* rename

* get *AtPoint tests working

* rename

* rename

* add value queries tests

* add element, mark and path queries tests

* convert most on-selection tests

* convert on-selection commands

* rename

* get addMarks and delete commands working

* rename

* rename

* rename

* refactor value.positions(), work on delete tests

* progress on delete tests

* more delete work

* finish delete tests

* start converting to at-based commands

* restructure query tests

* restructure operations tests

* more work converting to multi-purpose commands

* lots of progress on converting to at-based commands

* add unwrapNodes

* remove setValue

* more progress

* refactor node commands to use consistent matching logic

* cleanup, get non-fragment commands passing

* remove annotations and isAtomic

* rename surround/pluck to cover/uncover

* add location concept, change at-path to from-path for iterables

* refactor batches

* add location-based queries

* refactor hanging logic

* more location query work

* renaming

* use getMatch more

* add split to wrap/unwrap

* flip levels/ancestors ordering

* switch splitNodes to use levels

* change split to always:false by default

* fix tests

* add more queries tests

* fixing more delete logic

* add more splitNodes tests

* get rest of delete tests passing

* fix location-based logic in some commands

* cleanup

* get previous packages tests passing again

* add slate-history package

* start slate-schema work

* start of react working

* rendering fixes

* get rich and plain text examples working

* get image example working with hooks and dropping

* refactor onDrop to be internal

* inline more event handlers

* refactor lots of event-related logic

* change rendering to use render props

* delete unused stuff

* cleanup dom utils

* remove unused deps

* remove unnecessary packages, add placeholder

* remove slate-react-placeholder package

* remove unused dep

* remove unnecessary tests, fix readonly example

* convert checklists example

* switch to next from webpack

* get link example working

* convert more examples

* preserve keys, memoized leafs/texts, fix node lookup

* fix to always useLayoutEffect for ordering

* fix annotations to be maps, memoize elements

* remove Change interface

* remove String interface

* rename Node.entries to Node.nodes

* remove unnecessary value queries

* default to selection when iterating, cleanup

* remove unused files

* update scroll into view logic

* fix undoing, remove constructor types

* dont sync selection while composing

* add workflows

* remove unused deps

* convert mentions example

* tweaks

* convert remaining examples

* rename h to jsx, update schema

* fix schema tests

* fix slate-schema logic and tests

* really fix slate-schema and forced-layout example

* get start of insertFragment tests working

* remove Fragment interface

* remove debugger

* get all non-skipped tests passing

* cleanup deps

* run prettier

* configure eslint for typescript

* more eslint fixes...

* more passing

* update some docs

* fix examples

* port windows undo hotkey change

* fix deps, add basic firefox support

* add event overriding, update walkthroughs

* add commands, remove classes, cleanup examples

* cleanup rollup config

* update tests

* rename queries tests

* update other tests

* update walkthroughs

* cleanup interface exports

* cleanup, change mark transforms to require location

* undo mark transform change

* more

* fix tests

* fix example

* update walkthroughs

* update docs

* update docs

* remove annotations

* remove value, move selection and children to editor

* add migrating doc

* fix lint

* fix tests

* fix DOM types aliasing

* add next export

* update deps, fix prod build

* fix prod build

* update scripts

* update docs and changelogs

* update workflow and pull request template
This commit is contained in:
Ian Storm Taylor
2019-11-27 20:54:42 -05:00
committed by GitHub
parent 02b87d5968
commit 4ff6972096
2367 changed files with 45706 additions and 80698 deletions

View File

@@ -1,582 +0,0 @@
import {
Annotation,
Document,
Mark,
Node,
Point,
Selection,
Text,
Value,
} from 'slate'
/**
* Auto-incrementing ID to keep track of paired annotations.
*
* @type {Number}
*/
let uid = 0
/**
* Create an anchor point.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {AnchorPoint}
*/
export function createAnchor(tagName, attributes, children) {
return new AnchorPoint(attributes)
}
/**
* Create a block.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {Block}
*/
export function createBlock(tagName, attributes, children) {
const attrs = { ...attributes, object: 'block' }
const block = createNode(null, attrs, children)
return block
}
/**
* Create a cursor point.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {CursorPoint}
*/
export function createCursor(tagName, attributes, children) {
return new CursorPoint(attributes)
}
/**
* Create a annotation point, or wrap a list of leaves and set the annotation
* point tracker on them.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {AnnotationPoint|List<Leaf>}
*/
export function createAnnotation(tagName, attributes, children) {
const { key, data } = attributes
const type = tagName
if (key) {
return new AnnotationPoint({ id: key, type, data })
}
const texts = createChildren(children)
const first = texts.first()
const last = texts.last()
const id = `${uid++}`
const start = new AnnotationPoint({ id, type, data })
const end = new AnnotationPoint({ id, type, data })
setPoint(first, start, 0)
setPoint(last, end, last.text.length)
return texts
}
/**
* Create a document.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {Document}
*/
export function createDocument(tagName, attributes, children) {
const attrs = { ...attributes, object: 'document' }
const document = createNode(null, attrs, children)
return document
}
/**
* Create a focus point.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {FocusPoint}
*/
export function createFocus(tagName, attributes, children) {
return new FocusPoint(attributes)
}
/**
* Create an inline.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {Inline}
*/
export function createInline(tagName, attributes, children) {
const attrs = { ...attributes, object: 'inline' }
const inline = createNode(null, attrs, children)
return inline
}
/**
* Create a list of leaves from a mark.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {List<Leaf>}
*/
export function createMark(tagName, attributes, children) {
const { key, ...mark } = attributes
const marks = Mark.createSet([mark])
const list = createChildren(children)
let node
if (list.size > 1) {
throw new Error(
`The <mark> hyperscript tag must only contain a single node's worth of children.`
)
} else if (list.size === 0) {
node = Text.create({ key, marks })
} else {
node = list.first()
node = preservePoints(node, n => {
if (key) n = n.set('key', key)
if (marks) n = n.set('marks', n.marks.union(marks))
return n
})
}
return node
}
/**
* Create a node.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {Node}
*/
export function createNode(tagName, attributes, children) {
const { object } = attributes
if (object === 'text') {
const text = createText(null, attributes, children)
return text
}
const nodes = createChildren(children)
const node = Node.create({ ...attributes, nodes })
return node
}
/**
* Create a selection.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {Selection}
*/
export function createSelection(tagName, attributes, children) {
const anchor = children.find(c => c instanceof AnchorPoint)
const focus = children.find(c => c instanceof FocusPoint)
const { marks, focused } = attributes
const selection = Selection.create({
marks,
isFocused: focused,
anchor: anchor && {
key: anchor.key,
offset: anchor.offset,
path: anchor.path,
},
focus: focus && {
key: focus.key,
offset: focus.offset,
path: focus.path,
},
})
return selection
}
/**
* Create a text node.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {Text}
*/
export function createText(tagName, attributes, children) {
const { key, marks } = attributes
const list = createChildren(children)
let node
if (list.size > 1) {
throw new Error(
`The <text> hyperscript tag must only contain a single node's worth of children.`
)
} else if (list.size === 0) {
node = Text.create({ key })
} else {
node = list.first()
node = preservePoints(node, n => {
if (key) n = n.set('key', key)
if (marks) n = n.set('marks', Mark.createSet(marks))
return n
})
}
return node
}
/**
* Create a value.
*
* @param {String} tagName
* @param {Object} attributes
* @param {Array} children
* @return {Value}
*/
export function createValue(tagName, attributes, children) {
const { data } = attributes
const document = children.find(Document.isDocument)
let selection = children.find(Selection.isSelection)
let anchor
let focus
let marks
let isFocused
let annotations = {}
const partials = {}
// Search the document's texts to see if any of them have the anchor or
// focus information saved, or annotations applied.
if (document) {
for (const [node, path] of document.texts()) {
const { __anchor, __annotations, __focus } = node
if (__anchor != null) {
anchor = Point.create({ path, key: node.key, offset: __anchor.offset })
marks = __anchor.marks
isFocused = __anchor.isFocused
}
if (__focus != null) {
focus = Point.create({ path, key: node.key, offset: __focus.offset })
marks = __focus.marks
isFocused = __focus.isFocused
}
if (__annotations != null) {
for (const ann of __annotations) {
const { id } = ann
const partial = partials[id]
delete partials[id]
if (!partial) {
ann.key = node.key
partials[id] = ann
continue
}
const annotation = Annotation.create({
key: id,
type: ann.type,
data: ann.data,
anchor: {
key: partial.key,
path: document.getPath(partial.key),
offset: partial.offset,
},
focus: {
path,
key: node.key,
offset: ann.offset,
},
})
annotations[id] = annotation
}
}
}
}
if (Object.keys(partials).length > 0) {
throw new Error(
`Slate hyperscript must have both a start and an end defined for each annotation using the \`key=\` prop.`
)
}
if (anchor && !focus) {
throw new Error(
`Slate hyperscript ranges must have both \`<anchor />\` and \`<focus />\` defined if one is defined, but you only defined \`<anchor />\`. For collapsed selections, use \`<cursor />\` instead.`
)
}
if (!anchor && focus) {
throw new Error(
`Slate hyperscript ranges must have both \`<anchor />\` and \`<focus />\` defined if one is defined, but you only defined \`<focus />\`. For collapsed selections, use \`<cursor />\` instead.`
)
}
if (anchor || focus) {
if (!selection) {
selection = Selection.create({ anchor, focus, isFocused, marks })
} else {
selection = selection.setPoints([anchor, focus])
}
} else if (!selection) {
selection = Selection.create()
}
selection = selection.normalize(document)
if (annotations.length > 0) {
annotations = annotations.map(a => a.normalize(document))
}
const value = Value.fromJSON({
data,
annotations,
document,
selection,
...attributes,
})
return value
}
/**
* Create a list of text nodes.
*
* @param {Array} children
* @return {List<Leaf>}
*/
export function createChildren(children) {
let nodes = Node.createList()
const push = node => {
const last = nodes.last()
const isString = typeof node === 'string'
if (last && last.__string && (isString || node.__string)) {
const text = isString ? node : node.text
const { length } = last.text
const next = preservePoints(last, l => l.insertText(length, text))
incrementPoints(node, length)
copyPoints(node, next)
next.__string = true
nodes = nodes.pop().push(next)
} else if (isString) {
node = Text.create({ text: node })
node.__string = true
nodes = nodes.push(node)
} else {
nodes = nodes.push(node)
}
}
children.forEach(child => {
if (Node.isNodeList(child)) {
child.forEach(c => push(c))
}
if (Node.isNode(child)) {
push(child)
}
if (typeof child === 'string') {
push(child)
}
if (isPoint(child)) {
if (!nodes.size) {
push('')
}
let last = nodes.last()
if (last.object !== 'text') {
push('')
last = nodes.last()
}
if (!last || !last.__string) {
push('')
last = nodes.last()
}
setPoint(last, child, last.text.length)
}
})
return nodes
}
/**
* Point classes that can be created at different points in the document and
* then searched for afterwards, for creating ranges.
*
* @type {Class}
*/
class CursorPoint {
constructor(attrs = {}) {
const { isFocused = true, marks = null } = attrs
this.isFocused = isFocused
this.marks = marks
this.offset = null
}
}
class AnchorPoint {
constructor(attrs = {}) {
const {
isFocused = true,
key = null,
marks = null,
offset = null,
path = null,
} = attrs
this.isFocused = isFocused
this.key = key
this.marks = marks
this.offset = offset
this.path = path
}
}
class FocusPoint {
constructor(attrs = {}) {
const {
isFocused = true,
key = null,
marks = null,
offset = null,
path = null,
} = attrs
this.isFocused = isFocused
this.key = key
this.marks = marks
this.offset = offset
this.path = path
}
}
class AnnotationPoint {
constructor(attrs) {
const { id = null, data = {}, type } = attrs
this.id = id
this.offset = null
this.type = type
this.data = data
}
}
/**
* Increment any existing `point` on object by `n`.
*
* @param {Any} object
* @param {Number} n
*/
function incrementPoints(object, n) {
const { __anchor, __focus, __annotations } = object
if (__anchor != null) {
__anchor.offset += n
}
if (__focus != null && __focus !== __anchor) {
__focus.offset += n
}
if (__annotations != null) {
__annotations.forEach(a => (a.offset += n))
}
}
/**
* Check whether an `object` is a point.
*
* @param {Any} object
* @return {Boolean}
*/
function isPoint(object) {
return (
object instanceof AnchorPoint ||
object instanceof CursorPoint ||
object instanceof AnnotationPoint ||
object instanceof FocusPoint
)
}
/**
* Preserve any point information on an object.
*
* @param {Any} object
* @param {Function} updator
* @return {Any}
*/
function preservePoints(object, updator) {
const next = updator(object)
copyPoints(object, next)
return next
}
function copyPoints(object, other) {
const { __anchor, __focus, __annotations } = object
if (__anchor != null) other.__anchor = __anchor
if (__focus != null) other.__focus = __focus
if (__annotations != null) other.__annotations = __annotations
}
/**
* Set a `point` on an `object`.
*
* @param {Any} object
* @param {*Point} point
* @param {Number} offset
*/
function setPoint(object, point, offset) {
if (point instanceof AnchorPoint || point instanceof CursorPoint) {
point.offset = offset
object.__anchor = point
}
if (point instanceof FocusPoint || point instanceof CursorPoint) {
point.offset = offset
object.__focus = point
}
if (point instanceof AnnotationPoint) {
point.offset = offset
object.__annotations = object.__annotations || []
object.__annotations = object.__annotations.concat(point)
}
}

View File

@@ -0,0 +1,318 @@
import {
Element,
Descendant,
Mark,
Node,
Path,
Range,
Text,
Editor,
createEditor as makeEditor,
} from 'slate'
import {
AnchorToken,
FocusToken,
Token,
addAnchorToken,
addFocusToken,
getAnchorOffset,
getFocusOffset,
} from './tokens'
/**
* Resolve the descedants of a node by normalizing the children that can be
* passed into a hyperscript creator function.
*/
const STRINGS: WeakSet<Text> = new WeakSet()
const resolveDescendants = (children: any[]): Descendant[] => {
const nodes: Node[] = []
const addChild = (child: Node | Token): void => {
if (child == null) {
return
}
const prev = nodes[nodes.length - 1]
if (typeof child === 'string') {
const text = { text: child, marks: [] }
STRINGS.add(text)
child = text
}
if (Text.isText(child)) {
const c = child // HACK: fix typescript complaining
if (
Text.isText(prev) &&
STRINGS.has(prev) &&
STRINGS.has(c) &&
c.marks.every(m => Mark.exists(m, prev.marks)) &&
prev.marks.every(m => Mark.exists(m, c.marks))
) {
prev.text += c.text
} else {
nodes.push(c)
}
} else if (Element.isElement(child)) {
nodes.push(child)
} else if (child instanceof Token) {
let n = nodes[nodes.length - 1]
if (!Text.isText(n)) {
addChild('')
n = nodes[nodes.length - 1] as Text
}
if (child instanceof AnchorToken) {
addAnchorToken(n, child)
} else if (child instanceof FocusToken) {
addFocusToken(n, child)
}
} else {
throw new Error(`Unexpected hyperscript child object: ${child}`)
}
}
for (const child of children.flat(Infinity)) {
addChild(child)
}
return nodes
}
/**
* Create an anchor token.
*/
export function createAnchor(
tagName: string,
attributes: { [key: string]: any },
children: any[]
): AnchorToken {
return new AnchorToken(attributes)
}
/**
* Create an anchor and a focus token.
*/
export function createCursor(
tagName: string,
attributes: { [key: string]: any },
children: any[]
): Token[] {
return [new AnchorToken(attributes), new FocusToken(attributes)]
}
/**
* Create an `Element` object.
*/
export function createElement(
tagName: string,
attributes: { [key: string]: any },
children: any[]
): Element {
return { ...attributes, children: resolveDescendants(children) }
}
/**
* Create a focus token.
*/
export function createFocus(
tagName: string,
attributes: { [key: string]: any },
children: any[]
): FocusToken {
return new FocusToken(attributes)
}
/**
* Create a fragment.
*/
export function createFragment(
tagName: string,
attributes: { [key: string]: any },
children: any[]
): Descendant[] {
return resolveDescendants(children)
}
/**
* Create a `Text` object with a mark applied.
*/
export function createMark(
tagName: string,
attributes: { [key: string]: any },
children: any[]
): Text {
const mark = { ...attributes }
const nodes = resolveDescendants(children)
if (nodes.length > 1) {
throw new Error(
`The <mark> hyperscript tag must only contain a single node's worth of children.`
)
}
if (nodes.length === 0) {
return { text: '', marks: [mark] }
}
const [node] = nodes
if (!Text.isText(node)) {
throw new Error(
`The <mark> hyperscript tag must only contain text content as children.`
)
}
if (!Mark.exists(mark, node.marks)) {
node.marks.push(mark)
}
return node
}
/**
* Create a `Selection` object.
*/
export function createSelection(
tagName: string,
attributes: { [key: string]: any },
children: any[]
): Range {
const anchor: AnchorToken = children.find(c => c instanceof AnchorToken)
const focus: FocusToken = children.find(c => c instanceof FocusToken)
if (!anchor || !anchor.offset || !anchor.path) {
throw new Error(
`The <selection> hyperscript tag must have an <anchor> tag as a child with \`path\` and \`offset\` attributes defined.`
)
}
if (!focus || !focus.offset || !focus.path) {
throw new Error(
`The <selection> hyperscript tag must have a <focus> tag as a child with \`path\` and \`offset\` attributes defined.`
)
}
return {
anchor: {
offset: anchor.offset,
path: anchor.path,
},
focus: {
offset: focus.offset,
path: focus.path,
},
...attributes,
}
}
/**
* Create a `Text` object.
*/
export function createText(
tagName: string,
attributes: { [key: string]: any },
children: any[]
): Text {
const nodes = resolveDescendants(children)
if (nodes.length > 1) {
throw new Error(
`The <text> hyperscript tag must only contain a single node's worth of children.`
)
}
let [node] = nodes
if (node == null) {
node = { text: '', marks: [] }
}
if (!Text.isText(node)) {
throw new Error(`
The <text> hyperscript tag can only contain text content as children.`)
}
// COMPAT: Re-create the node, because if they used the <text> tag we want to
// guarantee that it won't be merge with other string children.
STRINGS.delete(node)
Object.assign(node, attributes)
return node
}
/**
* Create a top-level `Editor` object.
*/
export function createEditor(
tagName: string,
attributes: { [key: string]: any },
children: any[]
): Editor {
const otherChildren: any[] = []
let selectionChild: Range | undefined
for (const child of children) {
if (Range.isRange(child)) {
selectionChild = child
} else {
otherChildren.push(child)
}
}
const descendants = resolveDescendants(otherChildren)
const selection: Partial<Range> = {}
const editor = makeEditor()
Object.assign(editor, attributes)
editor.children = descendants
// Search the document's texts to see if any of them have tokens associated
// that need incorporated into the selection.
for (const [node, path] of Node.texts(editor)) {
const anchor = getAnchorOffset(node)
const focus = getFocusOffset(node)
if (anchor != null) {
const [offset] = anchor
selection.anchor = { path, offset }
}
if (focus != null) {
const [offset] = focus
selection.focus = { path, offset }
}
}
if (selection.anchor && !selection.focus) {
throw new Error(
`Slate hyperscript ranges must have both \`<anchor />\` and \`<focus />\` defined if one is defined, but you only defined \`<anchor />\`. For collapsed selections, use \`<cursor />\` instead.`
)
}
if (!selection.anchor && selection.focus) {
throw new Error(
`Slate hyperscript ranges must have both \`<anchor />\` and \`<focus />\` defined if one is defined, but you only defined \`<focus />\`. For collapsed selections, use \`<cursor />\` instead.`
)
}
if (selectionChild != null) {
editor.selection = selectionChild
} else if (Range.isRange(selection)) {
editor.selection = selection
}
return editor
}

View File

@@ -0,0 +1,163 @@
import isPlainObject from 'is-plain-object'
import { Element, Mark } from 'slate'
import {
createAnchor,
createCursor,
createEditor,
createElement,
createFocus,
createFragment,
createMark,
createSelection,
createText,
} from './creators'
/**
* The default creators for Slate objects.
*/
const DEFAULT_CREATORS = {
anchor: createAnchor,
cursor: createCursor,
editor: createEditor,
element: createElement,
focus: createFocus,
fragment: createFragment,
mark: createMark,
selection: createSelection,
text: createText,
}
/**
* `HyperscriptCreators` are dictionaries of `HyperscriptCreator` functions
* keyed by tag name.
*/
type HyperscriptCreators<T = any> = Record<
string,
(tagName: string, attributes: { [key: string]: any }, children: any[]) => T
>
/**
* `HyperscriptShorthands` are dictionaries of properties applied to specific
* kind of object, keyed by tag name. They allow you to easily define custom
* hyperscript tags for your schema.
*/
type HyperscriptShorthands = Record<string, Record<string, any>>
/**
* Create a Slate hyperscript function with `options`.
*/
const createHyperscript = (
options: {
creators?: HyperscriptCreators
elements?: HyperscriptShorthands
marks?: HyperscriptShorthands
} = {}
) => {
const { elements = {}, marks = {} } = options
const elementCreators = normalizeElements(elements)
const markCreators = normalizeMarks(marks)
const creators = {
...DEFAULT_CREATORS,
...elementCreators,
...markCreators,
...options.creators,
}
const jsx = createFactory(creators)
return jsx
}
/**
* Create a Slate hyperscript function with `options`.
*/
const createFactory = <T extends HyperscriptCreators>(creators: T) => {
const jsx = <S extends keyof T & string>(
tagName: S,
attributes?: Object,
...children: any[]
): ReturnType<T[S]> => {
const creator = creators[tagName]
if (!creator) {
throw new Error(`No hyperscript creator found for tag: <${tagName}>`)
}
if (attributes == null) {
attributes = {}
}
if (!isPlainObject(attributes)) {
children = [attributes].concat(children)
attributes = {}
}
children = children.filter(child => Boolean(child)).flat()
const ret = creator(tagName, attributes, children)
return ret
}
return jsx
}
/**
* Normalize a dictionary of element shorthands into creator functions.
*/
const normalizeElements = (elements: HyperscriptShorthands) => {
const creators: HyperscriptCreators<Element> = {}
for (const tagName in elements) {
const props = elements[tagName]
if (typeof props !== 'object') {
throw new Error(
`Properties specified for a hyperscript shorthand should be an object, but for the custom element <${tagName}> tag you passed: ${props}`
)
}
creators[tagName] = (
tagName: string,
attributes: { [key: string]: any },
children: any[]
) => {
return createElement('element', { ...props, ...attributes }, children)
}
}
return creators
}
/**
* Normalize a dictionary of mark shorthands into creator functions.
*/
const normalizeMarks = (marks: HyperscriptShorthands) => {
const creators: HyperscriptCreators<Mark> = {}
for (const tagName in marks) {
const props = marks[tagName]
if (typeof props !== 'object') {
throw new Error(
`Properties specified for a hyperscript shorthand should be an object, but for the custom mark <${tagName}> tag you passed: ${props}`
)
}
creators[tagName] = (
tagName: string,
attributes: { [key: string]: any },
children: any[]
) => {
return createMark('mark', { ...props, ...attributes }, children)
}
}
return creators
}
export { createHyperscript, HyperscriptCreators, HyperscriptShorthands }

View File

@@ -1,132 +0,0 @@
import isPlainObject from 'is-plain-object'
import {
createAnchor,
createBlock,
createCursor,
createAnnotation,
createDocument,
createFocus,
createInline,
createMark,
createNode,
createSelection,
createText,
createValue,
} from './creators'
/**
* Create a Slate hyperscript function with `options`.
*
* @param {Object} options
* @return {Function}
*/
function createHyperscript(options = {}) {
const { blocks = {}, inlines = {}, marks = {}, annotations = {} } = options
const creators = {
anchor: createAnchor,
annotation: createAnnotation,
block: createBlock,
cursor: createCursor,
document: createDocument,
focus: createFocus,
inline: createInline,
mark: createMark,
node: createNode,
selection: createSelection,
text: createText,
value: createValue,
...(options.creators || {}),
}
for (const key in blocks) {
creators[key] = normalizeCreator(blocks[key], createBlock)
}
for (const key in inlines) {
creators[key] = normalizeCreator(inlines[key], createInline)
}
for (const key in marks) {
creators[key] = normalizeCreator(marks[key], createMark)
}
for (const key in annotations) {
creators[key] = normalizeCreator(annotations[key], createAnnotation)
}
function create(tagName, attributes, ...children) {
const creator = creators[tagName]
if (!creator) {
throw new Error(`No hyperscript creator found for tag: "${tagName}"`)
}
if (attributes == null) {
attributes = {}
}
if (!isPlainObject(attributes)) {
children = [attributes].concat(children)
attributes = {}
}
children = children
.filter(child => Boolean(child))
.reduce((memo, child) => memo.concat(child), [])
const ret = creator(tagName, attributes, children)
return ret
}
return create
}
/**
* Normalize a `creator` of `value`.
*
* @param {Function|Object|String} value
* @param {Function} creator
* @return {Function}
*/
function normalizeCreator(value, creator) {
if (typeof value === 'function') {
return value
}
if (typeof value === 'string') {
value = { type: value }
}
if (isPlainObject(value)) {
return (tagName, attributes, children) => {
const { key, ...rest } = attributes
const attrs = {
...value,
key,
data: {
...(value.data || {}),
...rest,
},
}
return creator(tagName, attrs, children)
}
}
throw new Error(
`Slate hyperscript creators can be either functions, objects or strings, but you passed: ${value}`
)
}
/**
* Export.
*
* @type {Function}
*/
export default createHyperscript()
export { createHyperscript }

View File

@@ -0,0 +1,13 @@
import {
createHyperscript,
HyperscriptCreators,
HyperscriptShorthands,
} from './hyperscript'
/**
* The default hyperscript factory that ships with Slate, without custom tags.
*/
const jsx = createHyperscript()
export { jsx, createHyperscript, HyperscriptCreators, HyperscriptShorthands }

View File

@@ -0,0 +1,111 @@
import { Mark, Node, Path, Text } from 'slate'
/**
* A weak map to hold anchor tokens.
*/
const ANCHOR: WeakMap<Node, [number, AnchorToken]> = new WeakMap()
/**
* A weak map to hold focus tokens.
*/
const FOCUS: WeakMap<Node, [number, FocusToken]> = new WeakMap()
/**
* All tokens inherit from a single constructor for `instanceof` checking.
*/
export class Token {}
/**
* Anchor tokens represent the selection's anchor point.
*/
export class AnchorToken extends Token {
focused: boolean
marks: Mark[] | null
offset?: number
path?: Path
constructor(
props: {
focused?: boolean
marks?: Mark[] | null
offset?: number
path?: Path
} = {}
) {
super()
const { focused = true, marks = null, offset, path } = props
this.focused = focused
this.marks = marks
this.offset = offset
this.path = path
}
}
/**
* Focus tokens represent the selection's focus point.
*/
export class FocusToken extends Token {
focused: boolean
marks: Mark[] | null
offset?: number
path?: Path
constructor(
props: {
focused?: boolean
marks?: Mark[] | null
offset?: number
path?: Path
} = {}
) {
super()
const { focused = true, marks = null, offset, path } = props
this.focused = focused
this.marks = marks
this.offset = offset
this.path = path
}
}
/**
* Add an anchor token to the end of a text node.
*/
export const addAnchorToken = (text: Text, token: AnchorToken) => {
const offset = text.text.length
ANCHOR.set(text, [offset, token])
}
/**
* Get the offset if a text node has an associated anchor token.
*/
export const getAnchorOffset = (
text: Text
): [number, AnchorToken] | undefined => {
return ANCHOR.get(text)
}
/**
* Add a focus token to the end of a text node.
*/
export const addFocusToken = (text: Text, token: FocusToken) => {
const offset = text.text.length
FOCUS.set(text, [offset, token])
}
/**
* Get the offset if a text node has an associated focus token.
*/
export const getFocusOffset = (
text: Text
): [number, FocusToken] | undefined => {
return FOCUS.get(text)
}