Implemented cut/copy/paste (via quickkeys)

This commit is contained in:
jos 2017-09-29 21:52:17 +02:00
parent 375ea56316
commit 26fa339c35
9 changed files with 376 additions and 98 deletions

View File

@ -1,6 +1,7 @@
// @flow weak
import { createElement as h, Component } from 'react'
import initial from 'lodash/initial'
import ActionMenu from './menu/ActionMenu'
import { escapeHTML, unescapeHTML } from '../utils/stringUtils'
@ -477,7 +478,7 @@ export default class JSONNode extends Component {
/** @private */
handleChangeProperty = (event) => {
const parentPath = allButLast(this.props.path)
const parentPath = initial(this.props.path)
const oldProp = this.props.prop.name
const newProp = unescapeHTML(getInnerText(event.target))
@ -595,10 +596,3 @@ export default class JSONNode extends Component {
: stringConvert(stringValue)
}
}
/**
* Returns a copy of the array having the last item removed
*/
function allButLast (array: []): any {
return array.slice(0, array.length - 1)
}

View File

@ -2,19 +2,22 @@
import { createElement as h, Component } from 'react'
import isEqual from 'lodash/isEqual'
import reverse from 'lodash/reverse'
import initial from 'lodash/initial'
import last from 'lodash/last'
import Hammer from 'react-hammerjs'
import jump from '../assets/jump.js/src/jump'
import Ajv from 'ajv'
import { updateIn, getIn, setIn } from '../utils/immutabilityHelpers'
import { parseJSON } from '../utils/jsonUtils'
import { allButLast } from '../utils/arrayUtils'
import { findUniqueName } from '../utils/stringUtils'
import { enrichSchemaError } from '../utils/schemaUtils'
import {
jsonToEson, esonToJson, toEsonPath, pathExists,
expand, expandPath, addErrors,
search, applySearchResults, nextSearchResult, previousSearchResult,
applySelection,
applySelection, pathsFromSelection, contentsFromPaths,
compileJSONPointer, parseJSONPointer
} from '../eson'
import { patchEson } from '../patchEson'
@ -29,12 +32,12 @@ import ModeButton from './menu/ModeButton'
import Search from './menu/Search'
import {
moveUp, moveDown, moveLeft, moveRight, moveDownSibling, moveHome, moveEnd,
findNode, selectFind, searchHasFocus, setSelection
findNode, findBaseNode, selectFind, searchHasFocus, setSelection
} from './utils/domSelector'
import { createFindKeyBinding } from '../utils/keyBindings'
import { KEY_BINDINGS } from '../constants'
import type { ESON, ESONPatch, JSONPath } from '../types'
import type { ESON, ESONPatch, JSONPath, ESONSelection } from '../types'
const AJV_OPTIONS = {
allErrors: true,
@ -50,19 +53,7 @@ export default class TreeMode extends Component {
id: number
state: Object
keyDownActions = {
'up': (event) => moveUp(event.target),
'down': (event) => moveDown(event.target),
'left': (event) => moveLeft(event.target),
'right': (event) => moveRight(event.target),
'home': (event) => moveHome(event.target),
'end': (event) => moveEnd(event.target),
'undo': (event) => this.undo(),
'redo': (event) => this.redo(),
'find': (event) => selectFind(event.target),
'findNext': (event) => this.handleNext(),
'findPrevious': (event) => this.handlePrevious()
}
keyDownActions = null
constructor (props) {
super(props)
@ -71,6 +62,23 @@ export default class TreeMode extends Component {
this.id = Math.round(Math.random() * 1e5) // TODO: create a uuid here?
this.keyDownActions = {
'up': this.moveUp,
'down': this.moveDown,
'left': this.moveLeft,
'right': this.moveRight,
'home': this.moveHome,
'end': this.moveEnd,
'cut': this.handleCut,
'copy': this.handleCopy,
'paste': this.handlePaste,
'undo': this.handleUndo,
'redo': this.handleRedo,
'find': this.handleFocusFind,
'findNext': this.handleNext,
'findPrevious': this.handlePrevious
}
this.state = {
data,
@ -101,7 +109,9 @@ export default class TreeMode extends Component {
selection: {
start: null, // ESONPointer
end: null, // ESONPointer
}
},
clipboard: null // array entries {prop: string, value: JSON}
}
}
@ -193,7 +203,8 @@ export default class TreeMode extends Component {
direction: 'DIRECTION_VERTICAL',
onTap: this.handleTap,
onPanStart: this.handlePanStart,
onPan: this.handlePan
onPan: this.handlePan,
onPanEnd: this.handlePanEnd
},
h('ul', {className: 'jsoneditor-list jsoneditor-root' + (data.selected ? ' jsoneditor-selected' : '')},
h(Node, {
@ -304,27 +315,22 @@ export default class TreeMode extends Component {
const action = this.keyDownActions[keyBinding]
if (action) {
event.preventDefault()
action(event)
}
}
/** @private */
handleChangeValue = (path, value) => {
this.handlePatch(changeValue(this.state.data, path, value))
}
/** @private */
handleChangeProperty = (parentPath, oldProp, newProp) => {
this.handlePatch(changeProperty(this.state.data, parentPath, oldProp, newProp))
}
/** @private */
handleChangeType = (path, type) => {
this.handlePatch(changeType(this.state.data, path, type))
}
/** @private */
handleInsert = (path, type) => {
this.handlePatch(insert(this.state.data, path, type))
@ -332,7 +338,6 @@ export default class TreeMode extends Component {
this.focusToNext(path)
}
/** @private */
handleAppend = (parentPath, type) => {
this.handlePatch(append(this.state.data, parentPath, type))
@ -340,7 +345,6 @@ export default class TreeMode extends Component {
this.focusToNext(parentPath)
}
/** @private */
handleDuplicate = (path) => {
this.handlePatch(duplicate(this.state.data, path))
@ -348,7 +352,6 @@ export default class TreeMode extends Component {
this.focusToNext(path)
}
/** @private */
handleRemove = (path) => {
// apply focus to next sibling element if existing, else to the previous element
const fromElement = findNode(this.refs.contents, path)
@ -360,6 +363,114 @@ export default class TreeMode extends Component {
this.handlePatch(remove(path))
}
moveUp = (event) => {
event.preventDefault()
moveUp(event.target)
}
moveDown = (event) => {
event.preventDefault()
moveDown(event.target)
}
moveLeft = (event) => {
event.preventDefault()
moveLeft(event.target)
}
moveRight = (event) => {
event.preventDefault()
moveRight(event.target)
}
moveHome = (event) => {
event.preventDefault()
moveHome(event.target)
}
moveEnd = (event) => {
event.preventDefault()
moveEnd(event.target)
}
handleCut = (event) => {
const { data, selection } = this.state
if (selection) {
event.preventDefault()
const paths = pathsFromSelection(data, selection)
const clipboard = contentsFromPaths(data, paths)
this.setState({ clipboard, selection: null })
// note that we reverse the order, else we will mess up indices to be deleted in case of an array
const patch = reverse(paths).map(path => ({op: 'remove', path: compileJSONPointer(path)}))
this.handlePatch(patch)
}
else {
// clear clipboard
this.setState({ clipboard: null, selection: null })
}
}
handleCopy = (event) => {
const { data, selection } = this.state
if (selection) {
event.preventDefault()
const paths = pathsFromSelection(data, selection)
const clipboard = contentsFromPaths(data, paths)
this.setState({ clipboard })
}
else {
// clear clipboard
this.setState({ clipboard: null, selection: null })
}
}
handlePaste = (event) => {
const { data, clipboard } = this.state
if (clipboard && clipboard.length > 0) {
event.preventDefault()
// FIXME: handle pasting in an empty object or array
const path = this.findDataPathFromElement(event.target)
if (path && path.length > 0) {
const parentPath = initial(path)
const parent = getIn(data, toEsonPath(data, parentPath))
const isObject = parent.type === 'Object'
if (parent.type === 'Object') {
const existingProps = parent.props.map(p => p.name)
const prop = last(path)
const patch = clipboard.map(entry => ({
op: 'add',
path: compileJSONPointer(parentPath.concat(findUniqueName(entry.name, existingProps))),
value: entry.value,
jsoneditor: { before: prop }
}))
this.handlePatch(patch)
}
else { // parent.type === 'Array'
const patch = clipboard.map(entry => ({
op: 'add',
path: compileJSONPointer(path),
value: entry.value
}))
this.handlePatch(patch)
}
}
}
}
/**
* Move focus to the next search result
* @param {Path} path
@ -374,12 +485,10 @@ export default class TreeMode extends Component {
})
}
/** @private */
handleSort = (path, order = null) => {
this.handlePatch(sort(this.state.data, path, order))
}
/** @private */
handleExpand = (path, expanded, recurse) => {
if (recurse) {
const esonPath = toEsonPath(this.state.data, path)
@ -397,13 +506,11 @@ export default class TreeMode extends Component {
}
}
/** @private */
handleFindKeyBinding = (event) => {
// findKeyBinding can change on the fly, so we can't bind it statically
return this.findKeyBinding (event)
}
/** @private */
handleExpandAll = () => {
const expanded = true
@ -412,7 +519,6 @@ export default class TreeMode extends Component {
})
}
/** @private */
handleCollapseAll = () => {
const expanded = false
@ -421,7 +527,6 @@ export default class TreeMode extends Component {
})
}
/** @private */
handleSearch = (text) => {
const searchResults = search(this.state.data, text)
@ -430,7 +535,7 @@ export default class TreeMode extends Component {
this.setState({
search: { text, active },
data: expandPath(this.state.data, allButLast(active.path))
data: expandPath(this.state.data, initial(active.path))
})
// scroll to active search result (on next tick, after this path has been expanded)
@ -443,15 +548,21 @@ export default class TreeMode extends Component {
}
}
/** @private */
handleNext = () => {
handleFocusFind = (event) => {
event.preventDefault()
selectFind(event.target)
}
handleNext = (event) => {
event.preventDefault()
const searchResults = search(this.state.data, this.state.search.text)
if (searchResults) {
const next = nextSearchResult(searchResults, this.state.search.active)
this.setState({
search: setIn(this.state.search, ['active'], next),
data: next ? expandPath(this.state.data, allButLast(next.path)) : this.state.data
data: next ? expandPath(this.state.data, initial(next.path)) : this.state.data
})
// scroll to the active result (on next tick, after this path has been expanded)
@ -467,15 +578,16 @@ export default class TreeMode extends Component {
}
}
/** @private */
handlePrevious = () => {
handlePrevious = (event) => {
event.preventDefault()
const searchResults = search(this.state.data, this.state.search.text)
if (searchResults) {
const previous = previousSearchResult(searchResults, this.state.search.active)
this.setState({
search: setIn(this.state.search, ['active'], previous),
data: previous ? expandPath(this.state.data, allButLast(previous.path)) : this.state.data
data: previous ? expandPath(this.state.data, initial(previous.path)) : this.state.data
})
// scroll to the active result (on next tick, after this path has been expanded)
@ -533,10 +645,24 @@ export default class TreeMode extends Component {
}
}
handlePanEnd = (event) => {
const path = this.findDataPathFromElement(event.target.firstChild)
if (path) {
// TODO: implement a better solution to keep focus in the editor than selecting the action menu. Most also be solved for undo/redo for example
const element = findNode(this.refs.contents, path)
const actionMenuButton = element && element.querySelector('button.jsoneditor-actionmenu')
if (actionMenuButton) {
actionMenuButton.focus()
}
}
}
findDataPathFromElement (element: Element) : JSONPath | null {
const base = findBaseNode(element)
const attr = base && base.getAttribute && base.getAttribute('data-path')
// The .replace is to change paths like `/myarray/-` into `/myarray`
const attr = element && element.getAttribute && element.getAttribute('data-path').replace(/\/-$/, '')
return attr ? parseJSONPointer(attr) : null
return attr ? parseJSONPointer(attr.replace(/\/-$/, '')) : null
}
/**
@ -579,6 +705,16 @@ export default class TreeMode extends Component {
}
}
handleUndo = (event) => {
event.preventDefault()
this.undo()
}
handleRedo = (event) => {
event.preventDefault()
this.redo()
}
canUndo = () => {
return this.state.historyIndex < this.state.history.length
}
@ -588,6 +724,7 @@ export default class TreeMode extends Component {
}
undo = () => {
console.log('undo')
if (this.canUndo()) {
const history = this.state.history
const historyIndex = this.state.historyIndex

View File

@ -193,7 +193,8 @@ export function findEditorContainer (element) {
return findParentWithAttribute (element, EDITOR_CONTAINER_ATTRIBUTE, 'true')
}
function findBaseNode (element) {
// TODO: find a better name for this function
export function findBaseNode (element) {
return findParentWithClassName (element, NODE_CONTAINER_CLASS_NAME)
}

View File

@ -6,6 +6,9 @@ export const KEY_BINDINGS = {
'remove': ['Ctrl+Delete', 'Command+Delete'],
'expand': ['Ctrl+E', 'Command+E'],
'actionMenu': ['Ctrl+M', 'Command+M'],
'cut': ['Ctrl+X', 'Command+X'],
'copy': ['Ctrl+C', 'Command+C'],
'paste': ['Ctrl+V', 'Command+V'],
'undo': ['Ctrl+Z', 'Command+Z'],
'redo': ['Ctrl+Shift+Z', 'Command+Shift+Z'],
'find': ['Ctrl+F', 'Command+F'],

View File

@ -7,8 +7,10 @@
import { setIn, getIn, updateIn } from './utils/immutabilityHelpers'
import { isObject } from './utils/typeUtils'
import { last, allButLast } from './utils/arrayUtils'
import isEqual from 'lodash/isEqual'
import times from 'lodash/times'
import initial from 'lodash/initial'
import last from 'lodash/last'
import type {
ESON, ESONObject, ESONArrayItem, ESONPointer, ESONSelection, ESONType, ESONPath,
@ -113,7 +115,7 @@ export function toEsonPath (eson: ESON, path: JSONPath) : ESONPath {
throw new Error('Array item "' + index + '" not found')
}
return ['items', index, 'value'].concat(toEsonPath(item.value, path.slice(1)))
return ['items', String(index), 'value'].concat(toEsonPath(item.value, path.slice(1)))
}
else if (eson.type === 'Object') {
// object property. find the index of this property
@ -123,7 +125,7 @@ export function toEsonPath (eson: ESON, path: JSONPath) : ESONPath {
throw new Error('Object property "' + path[0] + '" not found')
}
return ['props', index, 'value']
return ['props', String(index), 'value']
.concat(toEsonPath(prop.value, path.slice(1)))
}
else {
@ -131,6 +133,42 @@ export function toEsonPath (eson: ESON, path: JSONPath) : ESONPath {
}
}
/**
* Convert an ESON object to a JSON object
* @param {ESON} eson
* @param {ESONPath} esonPath
* @return {JSONPath} path
*/
export function toJsonPath (eson: ESON, esonPath: ESONPath) : JSONPath {
if (esonPath.length === 0) {
return []
}
if (eson.type === 'Array') {
// index of an array
const index = esonPath[1]
const item = eson.items[parseInt(index)]
if (!item) {
throw new Error('Array item "' + index + '" not found')
}
return [index].concat(toJsonPath(item.value, esonPath.slice(3)))
}
else if (eson.type === 'Object') {
// object property. find the index of this property
const index = esonPath[1]
const prop = eson.props[parseInt(index)]
if (!prop) {
throw new Error('Object property "' + esonPath[1] + '" not found')
}
return [prop.name].concat(toJsonPath(prop.value, esonPath.slice(3)))
}
else {
return []
}
}
type ExpandCallback = (Path) => boolean
/**
@ -219,7 +257,7 @@ export function search (eson: ESON, text: string): ESONPointer[] {
if (containsCaseInsensitive(prop, text)) {
// only add search result when this is an object property name,
// don't add search result for array indices
const parentPath = allButLast(path)
const parentPath = initial(path)
const parent = getIn(eson, toEsonPath(eson, parentPath))
if (parent.type === 'Object') {
results.push({path, field: 'property'})
@ -302,7 +340,7 @@ export function applySearchResults (eson: ESON, searchResults: ESONPointer[], ac
if (searchResult.field === 'property') {
const esonPath = toEsonPath(updatedEson, searchResult.path)
const propertyPath = allButLast(esonPath).concat('searchResult')
const propertyPath = initial(esonPath).concat('searchResult')
const value = isEqual(searchResult, activeSearchResult) ? 'active' : 'normal'
updatedEson = setIn(updatedEson, propertyPath, value)
}
@ -337,7 +375,7 @@ export function applySelection (eson: ESON, selection: ESONSelection) {
const childsKey = (root.type === 'Object') ? 'props' : 'items' // property name of the array with props/items
const childsBefore = root[childsKey].slice(0, minIndex)
const childsUpdated = root[childsKey].slice(minIndex, maxIndex)
.map((child, index) => setIn(child, ['value', 'selected'], true))
.map(child => setIn(child, ['value', 'selected'], true))
const childsAfter = root[childsKey].slice(maxIndex)
return setIn(root, [childsKey], childsBefore.concat(childsUpdated, childsAfter))
@ -361,6 +399,51 @@ export function findSelectionIndices (root: ESON, start: string, end: string) :
return { minIndex, maxIndex }
}
/**
* Get the JSON paths from a selection, sorted from first to last
*/
export function pathsFromSelection (eson: ESON, selection: ESONSelection): JSONPath[] {
// find the parent node shared by both start and end of the selection
const rootPath = findSharedPath(selection.start.path, selection.end.path)
const rootEsonPath = toEsonPath(eson, rootPath)
if (rootPath.length === selection.start.path.length || rootPath.length === selection.end.path.length) {
// select a single node
return [ rootPath ]
}
else {
// select multiple childs of an object or array
const root = getIn(eson, rootEsonPath)
const start = selection.start.path[rootPath.length]
const end = selection.end.path[rootPath.length]
const { minIndex, maxIndex } = findSelectionIndices(root, start, end)
if (root.type === 'Object') {
return times(maxIndex - minIndex, i => rootPath.concat(root.props[i + minIndex].name))
}
else { // root.type === 'Array'
return times(maxIndex - minIndex, i => rootPath.concat(String(i + minIndex)))
}
}
}
/**
* Get the contents of a list with paths
* @param {ESON} data
* @param {JSONPath[]} paths
* @return {Array.<{name: string, value: JSONType}>}
*/
export function contentsFromPaths (data: ESON, paths: JSONPath[]) {
return paths.map(path => {
const esonPath = toEsonPath(data, path)
return {
name: getIn(data, initial(esonPath).concat('name')) || String(esonPath[esonPath.length - 2]),
value: esonToJson(getIn(data, esonPath))
}
})
}
/**
* Find the common path of two paths.
* For example findCommonRoot(['arr', '1', 'name'], ['arr', '1', 'address', 'contact']) returns ['arr', '1']
@ -468,7 +551,7 @@ function recurseTraverse (value: ESON, path: JSONPath, root: ESON, callback: Rec
* @return {boolean} Returns true if the path exists, else returns false
* @private
*/
export function pathExists (eson, path) {
export function pathExists (eson: ESON, path: JSONPath) {
if (eson === undefined) {
return false
}
@ -480,11 +563,11 @@ export function pathExists (eson, path) {
if (eson.type === 'Array') {
// index of an array
const index = path[0]
const item = eson.items[index]
const item = eson.items[parseInt(index)]
return pathExists(item && item.value, path.slice(1))
}
else {
else { // eson.type === 'Object'
// object property. find the index of this property
const index = findPropertyIndex(eson, path[0])
const prop = eson.props[index]

View File

@ -1,12 +1,13 @@
import isEqual from 'lodash/isEqual'
import initial from 'lodash/initial'
import type { ESON, Path, JSONPatch, ESONPatchAction, ESONPatchOptions, ESONPatchResult } from './types'
import type { ESON, Path, ESONPatch, ESONPatchOptions, ESONPatchResult, ESONSelection } from './types'
import { setIn, updateIn, getIn, deleteIn, insertAt } from './utils/immutabilityHelpers'
import { allButLast } from './utils/arrayUtils'
import {
jsonToEson, esonToJson, toEsonPath,
parseJSONPointer, compileJSONPointer,
expandAll, pathExists, resolvePathIndex, findPropertyIndex, findNextProp, getId
expandAll, pathExists, resolvePathIndex, findPropertyIndex, findNextProp, getId,
pathsFromSelection
} from './eson'
/**
@ -17,7 +18,7 @@ import {
* what nodes must be expanded
* @return {{data: ESON, revert: Object[], error: Error | null}}
*/
export function patchEson (eson: ESON, patch: ESONPatchAction[], expand = expandAll) {
export function patchEson (eson: ESON, patch: ESONPatch, expand = expandAll) {
let updatedEson = eson
let revert = []
@ -99,11 +100,11 @@ export function patchEson (eson: ESON, patch: ESONPatchAction[], expand = expand
}
default: {
// unknown jsonpatch operation. Cancel the whole patch and return an error
// unknown ESONPatch operation. Cancel the whole patch and return an error
return {
data: eson,
revert: [],
error: new Error('Unknown jsonpatch op ' + JSON.stringify(action.op))
error: new Error('Unknown ESONPatch op ' + JSON.stringify(action.op))
}
}
}
@ -123,7 +124,7 @@ export function patchEson (eson: ESON, patch: ESONPatchAction[], expand = expand
* @param {ESON} data
* @param {Path} path
* @param {ESON} value
* @return {{data: ESON, revert: JSONPatch}}
* @return {{data: ESON, revert: ESONPatch}}
*/
export function replace (data: ESON, path: Path, value: ESON) {
const esonPath = toEsonPath(data, path)
@ -146,7 +147,7 @@ export function replace (data: ESON, path: Path, value: ESON) {
* Remove an item or property
* @param {ESON} data
* @param {string} path
* @return {{data: ESON, revert: JSONPatch}}
* @return {{data: ESON, revert: ESONPatch}}
*/
export function remove (data: ESON, path: string) {
// console.log('remove', path)
@ -198,13 +199,13 @@ export function remove (data: ESON, path: string) {
}
/**
* Remove redundant actions from a JSONPatch array.
* Remove redundant actions from a ESONPatch array.
* Actions are redundant when they are followed by an action
* acting on the same path.
* @param {JSONPatch} patch
* @param {ESONPatch} patch
* @return {Array}
*/
export function simplifyPatch(patch: JSONPatch) {
export function simplifyPatch(patch: ESONPatch) {
const simplifiedPatch = []
const paths = {}
@ -235,7 +236,7 @@ export function simplifyPatch(patch: JSONPatch) {
* @param {ESON} value
* @param {{before?: string}} [options]
* @param {number} [id] Optional id for the new item
* @return {{data: ESON, revert: JSONPatch}}
* @return {{data: ESON, revert: ESONPatch}}
* @private
*/
export function add (data: ESON, path: string, value: ESON, options, id = getId()) {
@ -303,7 +304,7 @@ export function add (data: ESON, path: string, value: ESON, options, id = getId(
* @param {string} path
* @param {string} from
* @param {{before?: string}} [options]
* @return {{data: ESON, revert: JSONPatch}}
* @return {{data: ESON, revert: ESONPatch}}
* @private
*/
export function copy (data: ESON, path: string, from: string, options) {
@ -318,17 +319,17 @@ export function copy (data: ESON, path: string, from: string, options) {
* @param {string} path
* @param {string} from
* @param {{before?: string}} [options]
* @return {{data: ESON, revert: JSONPatch}}
* @return {{data: ESON, revert: ESONPatch}}
* @private
*/
export function move (data: ESON, path: string, from: string, options) {
const fromArray = parseJSONPointer(from)
const prop = getIn(data, allButLast(toEsonPath(data, fromArray)))
const prop = getIn(data, initial(toEsonPath(data, fromArray)))
const dataValue = prop.value
const id = prop.id // we want to use the existing id in case the move is a renaming a property
// FIXME: only reuse the existing id when move is renaming a property in the same object
const parentPathFrom = allButLast(fromArray)
const parentPathFrom = initial(fromArray)
const parent = getIn(data, toEsonPath(data, parentPathFrom))
const result1 = remove(data, from)

View File

@ -1,19 +1,3 @@
/**
* Returns the last item of an array
* @param {Array} array
* @return {*}
*/
export function last (array) {
return array[array.length - 1]
}
/**
* Returns a copy of the array having the last item removed
*/
export function allButLast (array: []): [] {
return array.slice(0, -1)
}
/**
* Comparator to sort an array in ascending order
*

View File

@ -1,17 +1,39 @@
import { readFileSync } from 'fs'
import test from 'ava';
import test from 'ava'
import { setIn, getIn } from '../src/utils/immutabilityHelpers'
import {
jsonToEson, esonToJson, toEsonPath, pathExists, transform, traverse,
jsonToEson, esonToJson, toEsonPath, toJsonPath, pathExists, transform, traverse,
parseJSONPointer, compileJSONPointer,
expand, addErrors, search, applySearchResults, nextSearchResult, previousSearchResult,
applySelection, getSelection
applySelection, pathsFromSelection
} from '../src/eson'
const JSON1 = loadJSON('./resources/json1.json')
const ESON1 = loadJSON('./resources/eson1.json')
const ESON2 = loadJSON('./resources/eson2.json')
test('toEsonPath', t => {
const jsonPath = ['obj', 'arr', '2', 'last']
const esonPath = [
'props', '0', 'value',
'props', '0', 'value',
'items', '2', 'value',
'props', '1', 'value'
]
t.deepEqual(toEsonPath(ESON1, jsonPath), esonPath)
})
test('toJsonPath', t => {
const jsonPath = ['obj', 'arr', '2', 'last']
const esonPath = [
'props', '0', 'value',
'props', '0', 'value',
'items', '2', 'value',
'props', '1', 'value'
]
t.deepEqual(toJsonPath(ESON1, esonPath), jsonPath)
})
test('jsonToEson', t => {
function expand (path) {
return true
@ -303,6 +325,42 @@ test('selection (node)', t => {
t.deepEqual(actual, expected)
})
test('pathsFromSelection (object)', t => {
const selection = {
start: {path: ['obj', 'arr', '2', 'last']},
end: {path: ['nill']}
}
t.deepEqual(pathsFromSelection(ESON1, selection), [
['obj'],
['str'],
['nill']
])
})
test('pathsFromSelection (array)', t => {
const selection = {
start: {path: ['obj', 'arr', '1']},
end: {path: ['obj', 'arr', '0']} // note the "wrong" order of start and end
}
t.deepEqual(pathsFromSelection(ESON1, selection), [
['obj', 'arr', '0'],
['obj', 'arr', '1']
])
})
test('pathsFromSelection (value)', t => {
const selection = {
start: {path: ['obj', 'arr', '2', 'first']},
end: {path: ['obj', 'arr', '2', 'first']}
}
t.deepEqual(pathsFromSelection(ESON1, selection), [
['obj', 'arr', '2', 'first'],
])
})
// helper function to replace all id properties with a constant value
function replaceIds (data, value = '[ID]') {
if (data.type === 'Object') {
@ -328,6 +386,7 @@ function printJSON (json, message = null) {
console.log(JSON.stringify(json, null, 2))
}
// helper function to load a JSON file
function loadJSON (filename) {
return JSON.parse(readFileSync(__dirname + '/' + filename, 'utf-8'))
}

View File

@ -1,6 +1,9 @@
import test from 'ava';
import { jsonToEson, esonToJson } from '../src/eson'
import { patchEson } from '../src/patchEson'
import { readFileSync } from 'fs'
import test from 'ava'
import { jsonToEson, esonToJson, toEsonPath } from '../src/eson'
import { patchEson, cut } from '../src/patchEson'
const ESON1 = loadJSON('./resources/eson1.json')
test('jsonpatch add', t => {
const json = {
@ -501,3 +504,16 @@ function replaceIds (data, value = '[ID]') {
})
}
}
// helper function to print JSON in the console
function printJSON (json, message = null) {
if (message) {
console.log(message)
}
console.log(JSON.stringify(json, null, 2))
}
// helper function to load a JSON file
function loadJSON (filename) {
return JSON.parse(readFileSync(__dirname + '/' + filename, 'utf-8'))
}