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

broke lots of stuff, but added tests

This commit is contained in:
Ian Storm Taylor
2016-06-22 18:42:49 -07:00
parent b42fd1849c
commit 74cab690e3
241 changed files with 4150 additions and 324 deletions

View File

@@ -31,6 +31,10 @@ dist: $(shell find ./lib)
example-auto-markdown: example-auto-markdown:
@ $(browserify) --debug --transform babelify --outfile ./examples/auto-markdown/build.js ./examples/auto-markdown/index.js @ $(browserify) --debug --transform babelify --outfile ./examples/auto-markdown/build.js ./examples/auto-markdown/index.js
# Build the links example.
example-links:
@ $(browserify) --debug --transform babelify --outfile ./examples/links/build.js ./examples/links/index.js
# Build the plain-text example. # Build the plain-text example.
example-plain-text: example-plain-text:
@ $(browserify) --debug --transform babelify --outfile ./examples/plain-text/build.js ./examples/plain-text/index.js @ $(browserify) --debug --transform babelify --outfile ./examples/plain-text/build.js ./examples/plain-text/index.js
@@ -52,24 +56,28 @@ lint:
@ $(standard) ./lib @ $(standard) ./lib
# Build the test source. # Build the test source.
test/support/build.js: $(shell find ./lib) ./test/browser.js test/browser/support/build.js: $(shell find ./lib) ./test/browser/index.js
@ $(browserify) --debug --transform babelify --outfile ./test/support/build.js ./test/browser.js @ $(browserify) --debug --transform babelify --outfile ./test/browser/support/build.js ./test/browser/index.js
# Run the tests. # Run the tests.
test: test-browser test-server test: test-browser test-server
# Run the browser-side tests. # Run the browser-side tests.
test-browser: ./test/support/build.js test-browser: ./test/browser/support/build.js
@ $(mocha-phantomjs) --reporter spec --timeout 5000 ./test/support/browser.html @ $(mocha-phantomjs) --reporter spec --timeout 5000 ./test/browser/support/browser.html
# Run the server-side tests. # Run the server-side tests.
test-server: test-server:
@ $(mocha) --reporter spec --timeout 5000 ./test/server.js @ $(mocha) --compilers js:babel-core/register --reporter spec --timeout 5000 ./test/server
# Watch the auto-markdown example. # Watch the auto-markdown example.
watch-example-auto-markdown: watch-example-auto-markdown:
@ $(MAKE) example-auto-markdown browserify=$(watchify) @ $(MAKE) example-auto-markdown browserify=$(watchify)
# Watch the links example.
watch-example-links:
@ $(MAKE) example-links browserify=$(watchify)
# Watch the plain-text example. # Watch the plain-text example.
watch-example-plain-text: watch-example-plain-text:
@ $(MAKE) example-plain-text browserify=$(watchify) @ $(MAKE) example-plain-text browserify=$(watchify)

View File

@@ -24,6 +24,7 @@ class App extends React.Component {
}; };
/** /**
*
* Render the example. * Render the example.
* *
* @return {Component} component * @return {Component} component
@@ -153,12 +154,10 @@ class App extends React.Component {
case '*': case '*':
case '-': case '-':
case '+': case '+':
if (node.type == 'list-item') break if (node.type == 'list-item') return
transform = node.type == 'list-item' transform = transform
? transform .setType('list-item')
: transform .wrapBlock('bulleted-list')
.setType('list-item')
.wrap('bulleted-list')
break break
default: default:
return return
@@ -196,7 +195,7 @@ class App extends React.Component {
.transform() .transform()
.setType('paragraph') .setType('paragraph')
if (node.type == 'list-item') transform = transform.unwrap('bulleted-list') if (node.type == 'list-item') transform = transform.unwrapBlock('bulleted-list')
state = transform.apply() state = transform.apply()
return state return state

48
examples/links/index.css Normal file
View File

@@ -0,0 +1,48 @@
html {
background: #eee;
padding: 20px;
}
main {
background: #fff;
padding: 10px;
max-width: 40em;
margin: 0 auto;
}
p {
margin: 0;
}
.editor > * > * + * {
margin-top: 1em;
}
.menu {
margin: 0 -10px;
padding: 1px 0 9px 8px;
border-bottom: 2px solid #eee;
margin-bottom: 10px;
}
.menu > * {
display: inline-block;
}
.menu > * + * {
margin-left: 10px;
}
.button {
color: #ccc;
cursor: pointer;
}
.button[data-active="true"] {
color: black;
}
.material-icons {
font-size: 18px;
}

12
examples/links/index.html Normal file
View File

@@ -0,0 +1,12 @@
<html>
<head>
<meta charset="utf-8" />
<title>Editor | Links Example</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="index.css">
</head>
<body>
<main></main>
<script src="build.js"></script>
</body>
</html>

159
examples/links/index.js Normal file
View File

@@ -0,0 +1,159 @@
import Editor, { Mark, Raw } from '../..'
import React from 'react'
import ReactDOM from 'react-dom'
import state from './state.json'
import { Map } from 'immutable'
/**
* App.
*/
class App extends React.Component {
state = {
state: Raw.deserialize(state)
};
/**
* Check whether the current selection has a link in it.
*
* @return {Boolean} hasLinks
*/
hasLinks() {
let { state } = this.state
const { currentInlineNodes } = state
const hasLinks = currentInlineNodes.some(inline => inline.type == 'link')
return hasLinks
}
/**
* When clicking a link, if the selection has a link in it, remove the link.
* Otherwise, add a new link with an href and text.
*
* @param {Event} e
*/
onClickLink(e) {
e.preventDefault()
let { state } = this.state
const hasLinks = this.hasLinks()
if (hasLinks) {
state = state
.transform()
.unwrapInline('link')
.apply()
}
else if (state.isCurrentlyExpanded) {
// const href = window.prompt('Enter the URL of the link:')
state = state
.transform()
.wrapInline('link', new Map({ href: 'https://google.com' }))
.apply()
}
else {
const href = window.prompt('Enter the URL of the link:')
const text = window.prompt('Enter the text for the link:')
state = state
.transform()
.insertText(text)
.extendBackward(text.length)
.wrapInline('link', new Map({ href }))
.apply()
}
this.setState({ state })
}
/**
* Render the app.
*
* @return {Component} component
*/
render() {
return (
<div>
{this.renderToolbar()}
{this.renderEditor()}
</div>
)
}
/**
* Render the toolbar.
*
* @return {Component} component
*/
renderToolbar() {
const hasLinks = this.hasLinks()
return (
<div className="menu">
<span className="button" onMouseDown={e => this.onClickLink(e)} data-active={hasLinks}>
<span className="material-icons">link</span>
</span>
</div>
)
}
/**
* Render the editor.
*
* @return {Component} component
*/
renderEditor() {
return (
<div className="editor">
<Editor
state={this.state.state}
renderNode={node => this.renderNode(node)}
onChange={(state) => {
console.groupCollapsed('Change!')
console.log('Document:', state.document.toJS())
console.log('Selection:', state.selection.toJS())
console.log('Content:', Raw.serialize(state))
console.groupEnd()
this.setState({ state })
}}
/>
</div>
)
}
/**
* Render our custom `node`.
*
* @param {Node} node
* @return {Component} component
*/
renderNode(node) {
switch (node.type) {
case 'link': {
return (props) => {
const { data } = props.node
const href = data.get('href')
return <a href={href}>{props.children}</a>
}
}
case 'paragraph': {
return (props) => <p>{props.children}</p>
}
}
}
}
/**
* Attach.
*/
const app = <App />
const root = document.body.querySelector('main')
ReactDOM.render(app, root)

57
examples/links/state.json Normal file
View File

@@ -0,0 +1,57 @@
{
"nodes": [
{
"kind": "block",
"type": "paragraph",
"nodes": [
{
"kind": "text",
"ranges": [
{
"text": "In addition to block nodes, you can create inline nodes, like "
},
]
},
{
"kind": "inline",
"type": "link",
"data": {
"href": "https://en.wikipedia.org/wiki/Hypertext"
},
"nodes": [
{
"kind": "text",
"ranges": [
{
"text": "hyperlinks"
},
]
},
]
},
{
"kind": "text",
"ranges": [
{
"text": "!"
},
]
}
]
},
{
"kind": "block",
"type": "paragraph",
"nodes": [
{
"kind": "text",
"ranges": [
{
"text": "This example shows hyperlinks in action. It features two ways to add links. You can either add a link via the toolbar icon above, or if you want in on a little secret, copy a URL to your keyboard and paste it while a range of text is selected."
}
]
}
]
}
]
}

View File

@@ -1,7 +1,7 @@
<html> <html>
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Editor | Auto-markdown Example</title> <title>Editor | Table Example</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"> <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="index.css"> <link rel="stylesheet" href="index.css">
</head> </head>

View File

@@ -83,7 +83,7 @@ class Content extends React.Component {
const { anchorNode, anchorOffset, focusNode, focusOffset } = native const { anchorNode, anchorOffset, focusNode, focusOffset } = native
const anchor = OffsetKey.findPoint(anchorNode, anchorOffset) const anchor = OffsetKey.findPoint(anchorNode, anchorOffset)
const focus = OffsetKey.findPoint(focusNode, focusOffset) const focus = OffsetKey.findPoint(focusNode, focusOffset)
const edges = document.filterNodes((node) => { const edges = document.filterDeep((node) => {
return node.key == anchor.key || node.key == focus.key return node.key == anchor.key || node.key == focus.key
}) })

View File

@@ -12,6 +12,7 @@ export default Editor
export { default as Block } from './models/block' export { default as Block } from './models/block'
export { default as Character } from './models/character' export { default as Character } from './models/character'
export { default as Data } from './models/data'
export { default as Document } from './models/document' export { default as Document } from './models/document'
export { default as Inline } from './models/inline' export { default as Inline } from './models/inline'
export { default as Mark } from './models/mark' export { default as Mark } from './models/mark'

File diff suppressed because it is too large Load Diff

View File

@@ -60,6 +60,16 @@ class Selection extends SelectionRecord {
return this.isExpanded return this.isExpanded
} }
/**
* Get whether the range's anchor of focus keys are not set yet.
*
* @return {Boolean} isUnset
*/
get isUnset() {
return this.anchorKey == null || this.focusKey == null
}
/** /**
* Get whether the selection is forward. * Get whether the selection is forward.
* *
@@ -109,7 +119,7 @@ class Selection extends SelectionRecord {
isAtStartOf(node) { isAtStartOf(node) {
const { startKey, startOffset } = this const { startKey, startOffset } = this
const first = node.kind == 'text' ? node : node.getFirstTextNode() const first = node.kind == 'text' ? node : node.getFirstText()
return startKey == first.key && startOffset == 0 return startKey == first.key && startOffset == 0
} }
@@ -122,7 +132,7 @@ class Selection extends SelectionRecord {
isAtEndOf(node) { isAtEndOf(node) {
const { endKey, endOffset } = this const { endKey, endOffset } = this
const last = node.kind == 'text' ? node : node.getLastTextNode() const last = node.kind == 'text' ? node : node.getLastText()
return endKey == last.key && endOffset == last.length return endKey == last.key && endOffset == last.length
} }
@@ -142,25 +152,25 @@ class Selection extends SelectionRecord {
if (anchorKey == null || focusKey == null) return selection if (anchorKey == null || focusKey == null) return selection
// Asset that the anchor and focus nodes exist in the node tree. // Asset that the anchor and focus nodes exist in the node tree.
node.assertHasNode(anchorKey) node.assertHasDeep(anchorKey)
node.assertHasNode(focusKey) node.assertHasDeep(focusKey)
let anchorNode = node.getNode(anchorKey) let anchorNode = node.getDeep(anchorKey)
let focusNode = node.getNode(focusKey) let focusNode = node.getDeep(focusKey)
// If the anchor node isn't a text node, match it to one. // If the anchor node isn't a text node, match it to one.
if (anchorNode.kind != 'text') { if (anchorNode.kind != 'text') {
anchorNode = node.getTextNodeAtOffset(anchorOffset) anchorNode = node.getTextAtOffset(anchorOffset)
let parent = node.getParentNode(anchorNode) let parent = node.getParent(anchorNode)
let offset = parent.getNodeOffset(anchorNode) let offset = parent.getOffset(anchorNode)
anchorOffset = anchorOffset - offset anchorOffset = anchorOffset - offset
anchorKey = anchorNode.key anchorKey = anchorNode.key
} }
// If the focus node isn't a text node, match it to one. // If the focus node isn't a text node, match it to one.
if (focusNode.kind != 'text') { if (focusNode.kind != 'text') {
focusNode = node.getTextNodeAtOffset(focusOffset) focusNode = node.getTextAtOffset(focusOffset)
let parent = node.getParentNode(focusNode) let parent = node.getParent(focusNode)
let offset = parent.getNodeOffset(focusNode) let offset = parent.getOffset(focusNode)
focusOffset = focusOffset - offset focusOffset = focusOffset - offset
focusKey = focusNode.key focusKey = focusNode.key
} }

View File

@@ -32,8 +32,16 @@ const NODE_LIKE_METHODS = [
'deleteAtRange', 'deleteAtRange',
'deleteBackwardAtRange', 'deleteBackwardAtRange',
'deleteForwardAtRange', 'deleteForwardAtRange',
'insertAtRange', 'insertTextAtRange',
'splitAtRange' 'markAtRange',
'setBlockAtRange',
'setInlineAtRange',
'splitBlockAtRange',
'splitInlineAtRange',
'unmarkAtRange',
'unwrapBlockAtRange',
'wrapBlockAtRange',
'wrapInlineAtRange'
] ]
/** /**
@@ -140,7 +148,17 @@ class State extends Record(DEFAULTS) {
*/ */
get currentBlockNodes() { get currentBlockNodes() {
return this.document.getBlockNodesAtRange(this.selection) return this.document.getBlocksAtRange(this.selection)
}
/**
* Get the inline nodes in the current selection.
*
* @return {OrderedMap} nodes
*/
get currentInlineNodes() {
return this.document.getInlinesAtRange(this.selection)
} }
/** /**
@@ -198,7 +216,7 @@ class State extends Record(DEFAULTS) {
// Determine what the selection should be after deleting. // Determine what the selection should be after deleting.
const { startKey } = selection const { startKey } = selection
const startNode = document.getNode(startKey) const startNode = document.getDeep(startKey)
if (selection.isExpanded) { if (selection.isExpanded) {
after = selection.moveToStart() after = selection.moveToStart()
@@ -209,8 +227,8 @@ class State extends Record(DEFAULTS) {
} }
else if (selection.isAtStartOf(startNode)) { else if (selection.isAtStartOf(startNode)) {
const parent = document.getParentNode(startNode) const parent = document.getParent(startNode)
const previous = document.getPreviousNode(parent).nodes.first() const previous = document.getPrevious(parent).nodes.first()
after = selection.moveToEndOf(previous) after = selection.moveToEndOf(previous)
} }
@@ -283,16 +301,31 @@ class State extends Record(DEFAULTS) {
} }
/** /**
* Set the nodes in the current selection to `type`. * Set the block nodes in the current selection to `type`.
* *
* @param {String} type * @param {String} type
* @return {State} state * @return {State} state
*/ */
setType(type) { setBlock(type, data) {
let state = this let state = this
let { document, selection } = state let { document, selection } = state
document = document.setTypeAtRange(selection, type) document = document.setBlockAtRange(selection, type, data)
state = state.merge({ document })
return state
}
/**
* Set the inline nodes in the current selection to `type`.
*
* @param {String} type
* @return {State} state
*/
setInline(type, data) {
let state = this
let { document, selection } = state
document = document.setInlineAtRange(selection, type, data)
state = state.merge({ document }) state = state.merge({ document })
return state return state
} }
@@ -313,9 +346,9 @@ class State extends Record(DEFAULTS) {
// Determine what the selection should be after splitting. // Determine what the selection should be after splitting.
const { startKey } = selection const { startKey } = selection
const startNode = document.getNode(startKey) const startNode = document.getDeep(startKey)
const parent = document.getParentNode(startNode) const parent = document.getParent(startNode)
const next = document.getNextNode(parent) const next = document.getNext(parent)
const text = next.nodes.first() const text = next.nodes.first()
selection = selection.moveToStartOf(text) selection = selection.moveToStartOf(text)
@@ -345,10 +378,10 @@ class State extends Record(DEFAULTS) {
* @return {State} state * @return {State} state
*/ */
wrap(type) { wrapBlock(type) {
let state = this let state = this
let { document, selection } = state let { document, selection } = state
document = document.wrapAtRange(selection, type) document = document.wrapBlockAtRange(selection, type)
state = state.merge({ document }) state = state.merge({ document })
return state return state
} }
@@ -360,11 +393,43 @@ class State extends Record(DEFAULTS) {
* @return {State} state * @return {State} state
*/ */
unwrap(type) { unwrapBlock(type) {
let state = this let state = this
let { document, selection } = state let { document, selection } = state
selection = selection.normalize(document) selection = selection.normalize(document)
document = document.unwrapAtRange(selection, type) document = document.unwrapBlockAtRange(selection, type)
state = state.merge({ document, selection })
return state
}
/**
* Wrap the current selection in new inline nodes of `type`.
*
* @param {String} type
* @param {Map} data
* @return {State} state
*/
wrapInline(type, data) {
let state = this
let { document, selection } = state
document = document.wrapInlineAtRange(selection, type, data)
state = state.merge({ document })
return state
}
/**
* Unwrap the current selection from a parent of `type`.
*
* @param {String} type
* @return {State} state
*/
unwrapInline(type) {
let state = this
let { document, selection } = state
selection = selection.normalize(document)
document = document.unwrapInlineAtRange(selection, type)
state = state.merge({ document, selection }) state = state.merge({ document, selection })
return state return state
} }

View File

@@ -46,16 +46,24 @@ const TRANSFORM_TYPES = [
'insertTextAtRange', 'insertTextAtRange',
'mark', 'mark',
'markAtRange', 'markAtRange',
'setType', 'setBlock',
'setTypeAtRange', 'setBlockAtRange',
'split', 'setInline',
'splitAtRange', 'setInlineAtRange',
'splitBlock',
'splitBlockAtRange',
'splitInline',
'splitInlineAtRange',
'unmark', 'unmark',
'unmarkAtRange', 'unmarkAtRange',
'unwrap', 'unwrapBlock',
'unwrapAtRange', 'unwrapBlockAtRange',
'wrap', 'unwrapInline',
'wrapAtRange' 'unwrapInlineAtRange',
'wrapBlock',
'wrapBlockAtRange',
'wrapInline',
'wrapInlineAtRange'
] ]
/** /**

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import keycode from 'keycode' import keycode from 'keycode'
import { IS_WINDOWS, IS_MAC } from '../utils/environment' import environment from '../utils/environment'
/** /**
* Export. * Export.
@@ -20,6 +20,7 @@ export default {
onKeyDown(e, state, editor) { onKeyDown(e, state, editor) {
const key = keycode(e.which) const key = keycode(e.which)
const { IS_WINDOWS, IS_MAC } = environment()
switch (key) { switch (key) {
case 'enter': { case 'enter': {

View File

@@ -17,9 +17,7 @@ import { Map } from 'immutable'
*/ */
function serialize(state) { function serialize(state) {
return { return serializeNode(state.document)
nodes: serializeNode(state.document)
}
} }
/** /**
@@ -44,12 +42,12 @@ function serializeNode(node) {
} }
case 'block': case 'block':
case 'inline': { case 'inline': {
return { const obj = {}
data: node.data.toJSON(), obj.kind = node.kind
kind: node.kind, obj.type = node.type
nodes: node.nodes.toArray().map(node => serializeNode(node)), obj.nodes = node.nodes.toArray().map(node => serializeNode(node))
type: node.type if (node.data.size) obj.data = node.data.toJSON()
} return obj
} }
} }
} }
@@ -65,10 +63,10 @@ function serializeCharacters(characters) {
return groupByMarks(characters) return groupByMarks(characters)
.toArray() .toArray()
.map((range) => { .map((range) => {
return { const obj = {}
text: range.text, obj.text = range.text
marks: range.marks.map(serializeMark) if (range.marks.size) obj.marks = range.marks.toArray().map(serializeMark)
} return obj
}) })
} }
@@ -80,10 +78,10 @@ function serializeCharacters(characters) {
*/ */
function serializeMark(mark) { function serializeMark(mark) {
return { const obj = {}
type: mark.type, obj.type = mark.type
data: mark.data.toJSON() if (mark.data.size) obj.data = mark.data.toJSON()
} return obj
} }
/** /**

View File

@@ -3,16 +3,28 @@ import browser from 'detect-browser'
import Parser from 'ua-parser-js' import Parser from 'ua-parser-js'
/** /**
* Detections. * Read the environment.
*
* @return {Object} environment
*/ */
export const IS_ANDROID = browser.name === 'android' function environment() {
export const IS_CHROME = browser.name === 'chrome' return {
export const IS_EDGE = browser.name === 'edge' IS_ANDROID: browser.name === 'android',
export const IS_FIREFOX = browser.name === 'firefox' IS_CHROME: browser.name === 'chrome',
export const IS_IE = browser.name === 'ie' IS_EDGE: browser.name === 'edge',
export const IS_IOS = browser.name === 'ios' IS_FIREFOX: browser.name === 'firefox',
export const IS_MAC = new Parser().getOS().name === 'Mac OS' IS_IE: browser.name === 'ie',
export const IS_UBUNTU = new Parser().getOS().name === 'Ubuntu' IS_IOS: browser.name === 'ios',
export const IS_SAFARI = browser.name === 'safari' IS_MAC: new Parser().getOS().name === 'Mac OS',
export const IS_WINDOWS = new Parser().getOS().name.includes('Windows') IS_UBUNTU: new Parser().getOS().name === 'Ubuntu',
IS_SAFARI: browser.name === 'safari',
IS_WINDOWS: new Parser().getOS().name.includes('Windows')
}
}
/**
* Export.
*/
export default environment

View File

@@ -8,7 +8,6 @@
"keycode": "^2.1.2", "keycode": "^2.1.2",
"lodash": "^4.13.1", "lodash": "^4.13.1",
"react": "^15.1.0", "react": "^15.1.0",
"to-camel-case": "^1.0.0",
"ua-parser-js": "^0.7.10", "ua-parser-js": "^0.7.10",
"uid": "0.0.2" "uid": "0.0.2"
}, },
@@ -21,10 +20,13 @@
"babel-preset-stage-0": "^6.5.0", "babel-preset-stage-0": "^6.5.0",
"babelify": "^7.3.0", "babelify": "^7.3.0",
"browserify": "^13.0.1", "browserify": "^13.0.1",
"component-type": "^1.2.1",
"mocha": "^2.5.3", "mocha": "^2.5.3",
"mocha-phantomjs": "^4.0.2", "mocha-phantomjs": "^4.0.2",
"react-dom": "^15.1.0", "react-dom": "^15.1.0",
"read-metadata": "^1.0.0",
"standard": "^7.1.2", "standard": "^7.1.2",
"to-camel-case": "^1.0.0",
"watchify": "^3.7.0" "watchify": "^3.7.0"
} }
} }

185
test/helpers/assert-json.js Normal file
View File

@@ -0,0 +1,185 @@
import assert from 'assert'
import type from 'component-type'
/**
* Assertion error.
*/
const AssertionError = assert.AssertionError
/**
* Assert that an `actual` JSON object equals an `expected` value.
*
* @param {Object} actual
* @param {Object} expected
* @throws {AssertionError}
*/
export function equal(actual, expected, message) {
if (!test(actual, expected)) {
throw new AssertionError({
actual: actual,
expected: wrap(actual, expected),
operator: '==',
stackStartFunction: equal
})
}
}
/**
* Assert that an `actual` JSON object does not equal an `expected` value.
*
* @param {Object} actual
* @param {Object} expected
* @throws {AssertionError}
*/
export function notEqual(actual, expected, message) {
if (test(actual, expected)) {
throw new AssertionError({
actual: actual,
expected: wrap(actual, expected),
operator: '!=',
stackStartFunction: notEqual
})
}
}
/**
* Assert that an `actual` JSON object strict equals an `expected` value.
*
* @param {Object} actual
* @param {Object} expected
* @throws {AssertionError}
*/
export function strictEqual(actual, expected, message) {
if (!test(actual, expected, true)) {
throw new AssertionError({
actual: actual,
expected: wrap(actual, expected),
operator: '===',
stackStartFunction: equal
})
}
}
/**
* Assert that an `actual` JSON object does not strict equal an `expected` value.
*
* @param {Object} actual
* @param {Object} expected
* @throws {AssertionError}
*/
export function notStrictEqual(actual, expected, message) {
if (test(actual, expected, true)) {
throw new AssertionError({
actual: actual,
expected: wrap(actual, expected),
operator: '!==',
stackStartFunction: notEqual
})
}
}
/**
* Test that an `actual` JSON value equals an `expected` JSON value.
*
* If a function is passed as any value, it is called with the actual value and
* must return a boolean.
*
* Strict mode uses strict equality, forces arrays to be of the same length, and
* objects to have the same keys.
*
* @param {Mixed} actual
* @param {Mixed} expected
* @param {Boolean} strict
* @return {Boolean}
*/
function test(actual, expected, strict) {
if (type(expected) == 'function') return !! expected(actual)
if (type(actual) != type(expected)) return false
switch (type(expected)) {
case 'object':
return object(actual, expected, strict)
case 'array':
return array(actual, expected, strict)
default:
return strict ? actual === expected : actual == expected
}
}
/**
* Test that an `actual` object equals an `expected` object.
*
* @param {Object} object
* @param {Object} expected
* @param {Boolean} strict
* @return {Boolean}
*/
function object(actual, expected, strict) {
if (strict) {
var ka = Object.keys(actual).sort()
var ke = Object.keys(expected).sort()
if (!test(ka, ke, strict)) return false
}
for (var key in expected) {
if (!test(actual[key], expected[key], strict)) return false
}
return true
}
/**
* Test that an `actual` array equals an `expected` array.
*
* @param {Array} actual
* @param {Array} expected
* @param {Boolean} strict
* @return {Boolean}
*/
function array(actual, expected, strict) {
if (strict) {
if (!test(actual.length, expected.length, strict)) return false
}
for (var i = 0; i < expected.length; i++) {
if (!test(actual[i], expected[i], strict)) return false
}
return true
}
/**
* Wrap an expected value to remove annoying false negatives.
*
* @param {Mixed} actual
* @param {Mixed} expected
* @return {Mixed}
*/
function wrap(actual, expected) {
if (type(expected) == 'function') return expected(actual) ? actual : expected
if (type(actual) != type(expected)) return expected
if (type(expected) == 'object') {
for (var key in expected) {
expected[key] = wrap(actual[key], expected[key])
}
}
if (type(expected) == 'array') {
for (var i = 0; i < expected.length; i++) {
expected[i] = wrap(actual[i], expected[i])
}
}
return expected
}

View File

@@ -1,11 +0,0 @@
const assert = require('assert')
const Editor = require('..')
/**
* Tests.
*/
describe('server', () => {
})

2
test/server/index.js Normal file
View File

@@ -0,0 +1,2 @@
import './transforms'

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 1
})
return state
.transform()
.deleteAtRange(range)
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: ord

View File

@@ -0,0 +1,18 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const second = texts.last()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 2,
focusKey: second.key,
focusOffset: 2
})
return state
.transform()
.deleteAtRange(range)
.apply()
}

View File

@@ -0,0 +1,14 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: another

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: woother

View File

@@ -0,0 +1,18 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const second = texts.last()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: first.length,
focusKey: second.key,
focusOffset: 0
})
return state
.transform()
.deleteAtRange(range)
.apply()
}

View File

@@ -0,0 +1,14 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: another

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: wordanother

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: first.length - 1,
focusKey: first.key,
focusOffset: first.length
})
return state
.transform()
.deleteAtRange(range)
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: wor

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 1,
focusKey: first.key,
focusOffset: 2
})
return state
.transform()
.deleteAtRange(range)
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: wrd

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: first.length
})
return state
.transform()
.deleteAtRange(range)
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: ""

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 1,
focusKey: first.key,
focusOffset: 1
})
return state
.transform()
.deleteBackwardAtRange(range)
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: ord

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const second = texts.last()
const range = selection.merge({
anchorKey: second.key,
anchorOffset: 0,
focusKey: second.key,
focusOffset: 0
})
return state
.transform()
.deleteBackwardAtRange(range)
.apply()
}

View File

@@ -0,0 +1,14 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: another

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: wordanother

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: first.length,
focusKey: first.key,
focusOffset: first.length
})
return state
.transform()
.deleteBackwardAtRange(range)
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: wor

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 2,
focusKey: first.key,
focusOffset: 2
})
return state
.transform()
.deleteBackwardAtRange(range)
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: wrd

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
return state
.transform()
.deleteBackwardAtRange(range)
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: first.length,
focusKey: first.key,
focusOffset: first.length
})
return state
.transform()
.deleteForwardAtRange(range)
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
return state
.transform()
.deleteForwardAtRange(range)
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: ord

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: first.length,
focusKey: first.key,
focusOffset: first.length
})
return state
.transform()
.deleteForwardAtRange(range)
.apply()
}

View File

@@ -0,0 +1,14 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: another

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: wordanother

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: first.length - 1,
focusKey: first.key,
focusOffset: first.length - 1
})
return state
.transform()
.deleteForwardAtRange(range)
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: wor

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 1,
focusKey: first.key,
focusOffset: 1
})
return state
.transform()
.deleteForwardAtRange(range)
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: wrd

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 3,
focusKey: first.key,
focusOffset: 3
})
return state
.transform()
.insertTextAtRange(range, 'a')
.apply()
}

View File

@@ -0,0 +1,12 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: w
- text: or
marks:
- type: bold
- text: d

View File

@@ -0,0 +1,12 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: w
- text: ora
marks:
- type: bold
- text: d

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 1,
focusKey: first.key,
focusOffset: 1
})
return state
.transform()
.insertTextAtRange(range, 'a')
.apply()
}

View File

@@ -0,0 +1,12 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: w
- text: or
marks:
- type: bold
- text: d

View File

@@ -0,0 +1,12 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: wa
- text: or
marks:
- type: bold
- text: d

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 2,
focusKey: first.key,
focusOffset: 2
})
return state
.transform()
.insertTextAtRange(range, 'a')
.apply()
}

View File

@@ -0,0 +1,12 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: w
- text: or
marks:
- type: bold
- text: d

View File

@@ -0,0 +1,12 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: w
- text: oar
marks:
- type: bold
- text: d

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
return state
.transform()
.insertTextAtRange(range, 'a')
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: aword

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
return state
.transform()
.insertTextAtRange(range, ' ')
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: " word"

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 0,
focusKey: first.key,
focusOffset: 0
})
return state
.transform()
.insertTextAtRange(range, 'a few words ')
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: a few words word

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: first.length,
focusKey: first.key,
focusOffset: first.length
})
return state
.transform()
.insertTextAtRange(range, 'a')
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: worda

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: first.length,
focusKey: first.key,
focusOffset: first.length
})
return state
.transform()
.insertTextAtRange(range, ' ')
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: "word "

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: first.length,
focusKey: first.key,
focusOffset: first.length
})
return state
.transform()
.insertTextAtRange(range, ' a few words')
.apply()
}

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word

View File

@@ -0,0 +1,8 @@
nodes:
- kind: block
type: paragraph
nodes:
- kind: text
ranges:
- text: word a few words

View File

@@ -0,0 +1,17 @@
export default function (state) {
const { document, selection } = state
const texts = document.getTextNodes()
const first = texts.first()
const range = selection.merge({
anchorKey: first.key,
anchorOffset: 1,
focusKey: first.key,
focusOffset: 1
})
return state
.transform()
.insertTextAtRange(range, 'a')
.apply()
}

Some files were not shown because too many files have changed in this diff Show More