Many patch action working again in UI

This commit is contained in:
jos 2017-12-15 20:34:07 +01:00
parent 156f330e4e
commit 1a6661fbb5
5 changed files with 153 additions and 365 deletions

View File

@ -1,9 +1,11 @@
import last from 'lodash/last'
import initial from 'lodash/initial'
import {
compileJSONPointer, getInEson, esonToJson, findNextProp,
META,
compileJSONPointer, esonToJson, findNextProp,
pathsFromSelection, findRootPath, findSelectionIndices
} from './eson'
import { cloneWithSymbols, getIn, setIn } from './utils/immutabilityHelpers'
import { findUniqueName } from './utils/stringUtils'
import { isObject, stringConvert } from './utils/typeUtils'
import { compareAsc, compareDesc, strictShallowEqual } from './utils/arrayUtils'
@ -11,14 +13,14 @@ import { compareAsc, compareDesc, strictShallowEqual } from './utils/arrayUtils'
/**
* Create a JSONPatch to change the value of a property or item
* @param {ESON} data
* @param {ESON} eson
* @param {Path} path
* @param {*} value
* @return {Array}
*/
export function changeValue (data, path, value) {
export function changeValue (eson, path, value) {
// console.log('changeValue', data, value)
const oldDataValue = getInEson(data, path)
const oldDataValue = getIn(eson, path)
return [{
op: 'replace',
@ -32,18 +34,18 @@ export function changeValue (data, path, value) {
/**
* Create a JSONPatch to change a property name
* @param {ESON} data
* @param {ESON} eson
* @param {Path} parentPath
* @param {string} oldProp
* @param {string} newProp
* @return {Array}
*/
export function changeProperty (data, parentPath, oldProp, newProp) {
export function changeProperty (eson, parentPath, oldProp, newProp) {
// console.log('changeProperty', parentPath, oldProp, newProp)
const parent = getInEson(data, parentPath)
const parent = getIn(eson, parentPath)
// prevent duplicate property names
const uniqueNewProp = findUniqueName(newProp, parent.props.map(p => p.name))
const uniqueNewProp = findUniqueName(newProp, parent[META].keys)
return [{
op: 'move',
@ -57,13 +59,13 @@ export function changeProperty (data, parentPath, oldProp, newProp) {
/**
* Create a JSONPatch to change the type of a property or item
* @param {ESON} data
* @param {ESON} eson
* @param {Path} path
* @param {ESONType} type
* @return {Array}
*/
export function changeType (data, path, type) {
const oldValue = esonToJson(getInEson(data, path))
export function changeType (eson, path, type) {
const oldValue = esonToJson(getIn(eson, path))
const newValue = convertType(oldValue, type)
// console.log('changeType', path, type, oldValue, newValue)
@ -85,20 +87,20 @@ export function changeType (data, path, type) {
* a unique property name for the duplicated node in case of duplicating
* and object property
*
* @param {ESON} data
* @param {ESON} eson
* @param {Selection} selection
* @return {Array}
*/
export function duplicate (data, selection) {
export function duplicate (eson, selection) {
// console.log('duplicate', path)
if (!selection.start || !selection.end) {
return []
}
const rootPath = findRootPath(selection)
const root = getInEson(data, rootPath)
const root = getIn(eson, rootPath)
const { maxIndex } = findSelectionIndices(root, rootPath, selection)
const paths = pathsFromSelection(data, selection)
const paths = pathsFromSelection(eson, selection)
if (root.type === 'Array') {
return paths.map((path, offset) => ({
@ -108,12 +110,11 @@ export function duplicate (data, selection) {
}))
}
else { // object.type === 'Object'
const nextProp = root.props && root.props[maxIndex]
const before = nextProp ? nextProp.name : null
const before = root[META].keys[maxIndex] || null
return paths.map(path => {
const prop = last(path)
const newProp = findUniqueName(prop, root.props.map(p => p.name))
const newProp = findUniqueName(prop, root[META].keys)
return {
op: 'copy',
@ -134,14 +135,14 @@ export function duplicate (data, selection) {
* a unique property name for the inserted node in case of duplicating
* and object property
*
* @param {ESON} data
* @param {ESON} eson
* @param {Path} path
* @param {Array.<{name?: string, value: JSONType, type?: ESONType}>} values
* @return {Array}
*/
export function insertBefore (data, path, values) { // TODO: find a better name and define datastructure for values
export function insertBefore (eson, path, values) { // TODO: find a better name and define datastructure for values
const parentPath = initial(path)
const parent = getInEson(data, parentPath)
const parent = getIn(eson, parentPath)
if (parent.type === 'Array') {
const startIndex = parseInt(last(path))
@ -157,7 +158,7 @@ export function insertBefore (data, path, values) { // TODO: find a better name
else { // object.type === 'Object'
const before = last(path)
return values.map(entry => {
const newProp = findUniqueName(entry.name, parent.props.map(p => p.name))
const newProp = findUniqueName(entry.name, parent[META].keys)
return {
op: 'add',
path: compileJSONPointer(parentPath.concat(newProp)),
@ -178,18 +179,18 @@ export function insertBefore (data, path, values) { // TODO: find a better name
* a unique property name for the inserted node in case of duplicating
* and object property
*
* @param {ESON} data
* @param {ESON} eson
* @param {Selection} selection
* @param {Array.<{name?: string, value: JSONType, type?: ESONType}>} values
* @return {Array}
*/
export function replace (data, selection, values) { // TODO: find a better name and define datastructure for values
export function replace (eson, selection, values) { // TODO: find a better name and define datastructure for values
const rootPath = findRootPath(selection)
const root = getInEson(data, rootPath)
const root = getIn(eson, rootPath)
const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection)
if (root.type === 'Array') {
const removeActions = removeAll(pathsFromSelection(data, selection))
const removeActions = removeAll(pathsFromSelection(eson, selection))
const insertActions = values.map((entry, offset) => ({
op: 'add',
path: compileJSONPointer(rootPath.concat(minIndex + offset)),
@ -202,12 +203,11 @@ export function replace (data, selection, values) { // TODO: find a better name
return removeActions.concat(insertActions)
}
else { // object.type === 'Object'
const nextProp = root.props && root.props[maxIndex]
const before = nextProp ? nextProp.name : null
const before = root[META].keys[maxIndex] || null
const removeActions = removeAll(pathsFromSelection(data, selection))
const removeActions = removeAll(pathsFromSelection(eson, selection))
const insertActions = values.map(entry => {
const newProp = findUniqueName(entry.name, root.props.map(p => p.name))
const newProp = findUniqueName(entry.name, root[META].keys)
return {
op: 'add',
path: compileJSONPointer(rootPath.concat(newProp)),
@ -230,15 +230,15 @@ export function replace (data, selection, values) { // TODO: find a better name
* a unique property name for the inserted node in case of duplicating
* and object property
*
* @param {ESON} data
* @param {ESON} eson
* @param {Path} parentPath
* @param {ESONType} type
* @return {Array}
*/
export function append (data, parentPath, type) {
export function append (eson, parentPath, type) {
// console.log('append', parentPath, value)
const parent = getInEson(data, parentPath)
const parent = getIn(eson, parentPath)
const value = createEntry(type)
if (parent.type === 'Array') {
@ -252,7 +252,7 @@ export function append (data, parentPath, type) {
}]
}
else { // object.type === 'Object'
const newProp = findUniqueName('', parent.props.map(p => p.name))
const newProp = findUniqueName('', parent[META].keys)
return [{
op: 'add',
@ -295,50 +295,49 @@ export function removeAll (paths) {
/**
* Create a JSONPatch to order the items of an array or the properties of an object in ascending
* or descending order
* @param {ESON} data
* @param {ESON} eson
* @param {Path} path
* @param {'asc' | 'desc' | null} [order=null] If not provided, will toggle current ordering
* @return {Array}
*/
export function sort (data, path, order = null) {
export function sort (eson, path, order = null) {
// console.log('sort', path, order)
const compare = order === 'desc' ? compareDesc : compareAsc
const object = getInEson(data, path)
const object = getIn(eson, path)
if (object.type === 'Array') {
const orderedItems = object.items.slice(0)
const orderedItems = object.slice()
// order the items by value
orderedItems.sort((a, b) => compare(a.value, b.value))
// when no order is provided, test whether ordering ascending
// changed anything. If not, sort descending
if (!order && strictShallowEqual(object.items, orderedItems)) {
if (!order && strictShallowEqual(object, orderedItems)) {
orderedItems.reverse()
}
return [{
op: 'replace',
path: compileJSONPointer(path),
value: esonToJson({
type: 'Array',
items: orderedItems
})
value: orderedItems
}]
}
else { // object.type === 'Object'
const orderedProps = object.props.slice(0)
// order the properties by key
orderedProps.sort((a, b) => compare(a.name, b.name))
const orderedKeys = object[META].keys.slice().sort((a, b) => compare(a.name, b.name))
// when no order is provided, test whether ordering ascending
// changed anything. If not, sort descending
if (!order && strictShallowEqual(object.props, orderedProps)) {
orderedProps.reverse()
if (!order && strictShallowEqual(object[META].keys, orderedKeys)) {
orderedKeys.reverse()
}
const orderedProps = cloneWithSymbols(object)
orderedProps[META] = setIn(object[META], ['keys'], orderedKeys)
return [{
op: 'replace',
path: compileJSONPointer(path),

View File

@ -5,7 +5,7 @@ import Ajv from 'ajv'
import { parseJSON } from '../utils/jsonUtils'
import { escapeUnicodeChars } from '../utils/stringUtils'
import { enrichSchemaError, limitErrors } from '../utils/schemaUtils'
import { jsonToEsonOld, esonToJson } from '../eson'
import { jsonToEson, esonToJson } from '../eson'
import { patchEson } from '../patchEson'
import { createFindKeyBinding } from '../utils/keyBindings'
import { KEY_BINDINGS } from '../constants'
@ -334,7 +334,7 @@ export default class TextMode extends Component {
patch (actions) {
const json = this.get()
const data = jsonToEsonOld(json)
const data = jsonToEson(json)
const result = patchEson(data, actions)
this.set(esonToJson(result.data))

View File

@ -336,19 +336,19 @@ export default class TreeMode extends Component {
}
handleChangeValue = (path, value) => {
this.handlePatch(changeValue(this.state.data, path, value))
this.handlePatch(changeValue(this.state.eson, path, value))
}
handleChangeProperty = (parentPath, oldProp, newProp) => {
this.handlePatch(changeProperty(this.state.data, parentPath, oldProp, newProp))
this.handlePatch(changeProperty(this.state.eson, parentPath, oldProp, newProp))
}
handleChangeType = (path, type) => {
this.handlePatch(changeType(this.state.data, path, type))
this.handlePatch(changeType(this.state.eson, path, type))
}
handleInsert = (path, type) => {
this.handlePatch(insertBefore(this.state.data, path, [{
this.handlePatch(insertBefore(this.state.eson, path, [{
type,
name: '',
value: createEntry(type)
@ -367,7 +367,7 @@ export default class TreeMode extends Component {
}
handleAppend = (parentPath, type) => {
this.handlePatch(append(this.state.data, parentPath, type))
this.handlePatch(append(this.state.eson, parentPath, type))
// apply focus to new node
this.focusToNext(parentPath)
@ -375,7 +375,7 @@ export default class TreeMode extends Component {
handleDuplicate = () => {
if (this.state.selection) {
this.handlePatch(duplicate(this.state.data, this.state.selection))
this.handlePatch(duplicate(this.state.eson, this.state.selection))
// TODO: focus to duplicated selection
}
}
@ -395,7 +395,7 @@ export default class TreeMode extends Component {
else if (this.state.selection) {
// remove selection
// TODO: select next property? (same as when removing a path?)
const paths = pathsFromSelection(this.state.data, this.state.selection)
const paths = pathsFromSelection(this.state.eson, this.state.selection)
this.setState({ selection: null })
this.handlePatch(removeAll(paths))
}
@ -446,13 +446,13 @@ export default class TreeMode extends Component {
}
handleKeyDownPaste = (event) => {
const { clipboard, data } = this.state
const { clipboard, eson } = this.state
if (clipboard && clipboard.length > 0) {
event.preventDefault()
const path = this.findDataPathFromElement(event.target)
this.handlePatch(insertBefore(data, path, clipboard))
this.handlePatch(insertBefore(eson, path, clipboard))
}
}
@ -460,7 +460,7 @@ export default class TreeMode extends Component {
const path = this.findDataPathFromElement(event.target)
if (path) {
const selection = { start: path, end: path }
this.handlePatch(duplicate(this.state.data, selection))
this.handlePatch(duplicate(this.state.eson, selection))
// apply focus to the duplicated node
this.focusToNext(path)
@ -470,9 +470,9 @@ export default class TreeMode extends Component {
handleCut = () => {
const selection = this.state.selection
if (selection && selection.start && selection.end) {
const data = this.state.data
const paths = pathsFromSelection(data, selection)
const clipboard = contentsFromPaths(data, paths)
const eson = this.state.eson
const paths = pathsFromSelection(eson, selection)
const clipboard = contentsFromPaths(eson, paths)
this.setState({ clipboard, selection: null })
@ -490,9 +490,9 @@ export default class TreeMode extends Component {
handleCopy = () => {
const selection = this.state.selection
if (selection && selection.start && selection.end) {
const data = this.state.data
const paths = pathsFromSelection(data, selection)
const clipboard = contentsFromPaths(data, paths)
const eson = this.state.eson
const paths = pathsFromSelection(eson, selection)
const clipboard = contentsFromPaths(eson, paths)
this.setState({ clipboard })
}
@ -503,11 +503,11 @@ export default class TreeMode extends Component {
}
handlePaste = () => {
const { data, selection, clipboard } = this.state
const { eson, selection, clipboard } = this.state
if (selection && clipboard && clipboard.length > 0) {
this.setState({ selection: null })
this.handlePatch(replace(data, selection, clipboard))
this.handlePatch(replace(eson, selection, clipboard))
// TODO: select the pasted contents
}
}
@ -539,7 +539,7 @@ export default class TreeMode extends Component {
}
handleSort = (path, order = null) => {
this.handlePatch(sort(this.state.data, path, order))
this.handlePatch(sort(this.state.eson, path, order))
}
handleSelect = (selection: Selection) => {
@ -753,13 +753,13 @@ export default class TreeMode extends Component {
* Emit an onChange event when there is a listener for it.
* @private
*/
emitOnChange (patch: ESONPatch, revert: ESONPatch, data: ESON) {
emitOnChange (patch: ESONPatch, revert: ESONPatch, eson: ESON) {
if (this.props.onPatch) {
this.props.onPatch(patch, revert)
}
if (this.props.onChange || this.props.onChangeText) {
const json = esonToJson(data)
const json = esonToJson(eson)
if (this.props.onChange) {
this.props.onChange(json)
@ -798,10 +798,10 @@ export default class TreeMode extends Component {
const historyIndex = this.state.historyIndex
const historyItem = history[historyIndex]
const result = patchEson(this.state.data, historyItem.undo)
const result = patchEson(this.state.eson, historyItem.undo)
this.setState({
data: result.data,
eson: result.data,
history,
historyIndex: historyIndex + 1
})
@ -816,10 +816,10 @@ export default class TreeMode extends Component {
const historyIndex = this.state.historyIndex - 1
const historyItem = history[historyIndex]
const result = patchEson(this.state.data, historyItem.redo)
const result = patchEson(this.state.eson, historyItem.redo)
this.setState({
data: result.data,
eson: result.data,
history,
historyIndex
})
@ -845,10 +845,10 @@ export default class TreeMode extends Component {
}
const expand = options.expand || (path => this.expandKeepOrExpandAll(path))
const result = patchEson(this.state.data, actions, expand)
const data = result.data
const result = patchEson(this.state.eson, actions, expand)
const eson = result.data
if (this.props.history != false) {
if (this.props.history !== false) {
// update data and store history
const historyItem = {
redo: actions,
@ -860,21 +860,21 @@ export default class TreeMode extends Component {
.slice(0, MAX_HISTORY_ITEMS)
this.setState({
data,
eson,
history,
historyIndex: 0
})
}
else {
// update data and don't store history
this.setState({ data })
this.setState({ eson })
}
return {
patch: actions,
revert: result.revert,
error: result.error,
data // FIXME: shouldn't pass data here
data: eson // FIXME: shouldn't pass data here
}
}
@ -973,7 +973,7 @@ export default class TreeMode extends Component {
* @param {Path} path
*/
exists (path) {
return pathExists(this.state.data, path)
return pathExists(this.state.eson, path)
}
/**

View File

@ -15,7 +15,7 @@ import initial from 'lodash/initial'
import last from 'lodash/last'
import type {
ESON, ESONObject, ESONArrayItem, ESONPointer, Selection, ESONType, ESONPath,
ESON, ESONObject, ESONArrayItem, ESONPointer, Selection, ESONPath,
Path,
JSONPath, JSONType
} from './types'
@ -29,6 +29,15 @@ export const SELECTED_AFTER = 4
export const META = Symbol('meta')
/**
* Expand function which will expand all nodes
* @param {Path} path
* @return {boolean}
*/
export function expandAll (path) {
return true
}
/**
*
* @param {JSONType} json
@ -58,57 +67,6 @@ export function jsonToEson (json, path = []) {
}
}
/**
* Expand function which will expand all nodes
* @param {Path} path
* @return {boolean}
*/
export function expandAll (path) {
return true
}
/**
* Convert a JSON object into ESON
* @param {Object | Array | string | number | boolean | null} json
* @param {function(path: JSONPath)} [expand]
* @param {JSONPath} [path=[]]
* @param {ESONType} [type='value'] Optional eson type for the created value
* @return {ESON}
*/
export function jsonToEsonOld (json, expand = expandAll, path: JSONPath = [], type: ESONType = 'value') : ESON {
if (Array.isArray(json)) {
return {
type: 'Array',
expanded: expand(path),
items: json.map((child, index) => {
return {
id: createId(), // TODO: use id based on index (only has to be unique within this array)
value: jsonToEsonOld(child, expand, path.concat(index))
}
})
}
}
else if (isObject(json)) {
return {
type: 'Object',
expanded: expand(path),
props: Object.keys(json).map((name, index) => {
return {
id: createId(), // TODO: use id based on index (only has to be unique within this array)
name,
value: jsonToEsonOld(json[name], expand, path.concat(name))
}
})
}
}
else { // value
return {
type: (type === 'string') ? 'string' : 'value',
value: json
}
}
}
/**
* Convert an ESON object to a JSON object
* @param {ESON} eson
@ -133,103 +91,6 @@ export function esonToJson (eson: ESON) {
}
}
/**
* Convert a path of a JSON object into a path in the corresponding ESON object
* @param {ESON} eson
* @param {JSONPath} path
* @return {ESONPath} esonPath
* @private
*/
export function toEsonPath (eson: ESON, path: JSONPath) : ESONPath {
if (path.length === 0) {
return []
}
if (eson.type === 'Array') {
// index of an array
const index = path[0]
const item = eson.items[parseInt(index)]
if (!item) {
throw new Error('Array item "' + index + '" not found')
}
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
const index = findPropertyIndex(eson, path[0])
const prop = eson.props[index]
if (!prop) {
throw new Error('Object property "' + path[0] + '" not found')
}
return ['props', String(index), 'value']
.concat(toEsonPath(prop.value, path.slice(1)))
}
else {
return []
}
}
/**
* 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 []
}
}
/**
* Get a nested property from an ESON object using a JSON path
*/
export function getInEson (eson: ESON, jsonPath: JSONPath) {
return getIn(eson, toEsonPath(eson, jsonPath))
}
/**
* Set the value of a nested property in an ESON object using a JSON path
*/
export function setInEson (eson: ESON, jsonPath: JSONPath, value: JSONType) {
return setIn(eson, toEsonPath(eson, jsonPath), value)
}
/**
* Set the value of a nested property in an ESON object using a JSON path
*/
export function deleteInEson (eson: ESON, jsonPath: JSONPath) : JSONType {
// with initial we remove the 'value' property,
// we want to remove the whole item from the items array
return deleteIn(eson, initial(toEsonPath(eson, jsonPath)))
}
/**
* Transform an eson object, traverse over the whole object (excluding the _meta)
* objects, and allow replacing Objects/Arrays/values
@ -569,14 +430,19 @@ export function applySelection (eson, selection) {
* Find the min and max index of a start and end child.
* Start and end can be a property name in case of an Object,
* or a matrix index (string with a number) in case of an Array.
*
* @param {ESON} root
* @param {Path} rootPath
* @param {Selection} selection
* @return {{minIndex: number, maxIndex: number}}
*/
export function findSelectionIndices (root: ESON, rootPath: JSONPath, selection: Selection) : { minIndex: number, maxIndex: number } {
export function findSelectionIndices (root, rootPath, selection) {
const start = (selection.after || selection.before || selection.start)[rootPath.length]
const end = (selection.after || selection.before || selection.end)[rootPath.length]
// if no object we assume it's an Array
const startIndex = root.type === 'Object' ? findPropertyIndex(root, start) : parseInt(start)
const endIndex = root.type === 'Object' ? findPropertyIndex(root, end) : parseInt(end)
const startIndex = root[META].type === 'Object' ? root[META].keys.indexOf(start) : parseInt(start)
const endIndex = root[META].type === 'Object' ? root[META].keys.indexOf(end) : parseInt(end)
const minIndex = Math.min(startIndex, endIndex)
const maxIndex = Math.max(startIndex, endIndex) +
@ -588,15 +454,15 @@ export function findSelectionIndices (root: ESON, rootPath: JSONPath, selection:
/**
* Get the JSON paths from a selection, sorted from first to last
*/
export function pathsFromSelection (eson: ESON, selection: Selection): JSONPath[] {
export function pathsFromSelection (eson, selection: Selection): JSONPath[] {
// find the parent node shared by both start and end of the selection
const rootPath = findRootPath(selection)
const root = getInEson(eson, rootPath)
const root = getIn(eson, rootPath)
const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection)
if (root.type === 'Object') {
return times(maxIndex - minIndex, i => rootPath.concat(root.props[i + minIndex].name))
if (root[META].type === 'Object') {
return times(maxIndex - minIndex, i => rootPath.concat(root[META].keys[i + minIndex]))
}
else { // root.type === 'Array'
return times(maxIndex - minIndex, i => rootPath.concat(String(i + minIndex)))
@ -611,10 +477,9 @@ export function pathsFromSelection (eson: ESON, selection: Selection): JSONPath[
*/
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))
name: getIn(data, last(path)),
value: esonToJson(getIn(data, path))
// FIXME: also store the type and expanded state
}
})
@ -659,93 +524,6 @@ function findSharedPath (path1: JSONPath, path2: JSONPath): JSONPath {
return path1.slice(0, i)
}
//
// /**
// * Recursively transform ESON: a recursive "map" function
// * @param {ESON} eson
// * @param {function(value: ESON, path: Path, root: ESON)} callback
// * @return {ESON} Returns the transformed eson object
// */
// export function transform (eson: ESON, callback: RecurseCallback) : ESON {
// return recurseTransform (eson, [], eson, callback)
// }
//
// /**
// * Recursively transform ESON
// * @param {ESON} value
// * @param {JSONPath} path
// * @param {ESON} root The root object, object at path=[]
// * @param {function(value: ESON, path: Path, root: ESON)} callback
// * @return {ESON} Returns the transformed eson object
// */
// function recurseTransform (value: ESON, path: JSONPath, root: ESON, callback: RecurseCallback) : ESON {
// let updatedValue: ESON = callback(value, path, root)
//
// if (value.type === 'Array') {
// let updatedItems = updatedValue.items
//
// updatedValue.items.forEach((item, index) => {
// const updatedItem = recurseTransform(item.value, path.concat(String(index)), root, callback)
// updatedItems = setIn(updatedItems, [index, 'value'], updatedItem)
// })
//
// updatedValue = setIn(updatedValue, ['items'], updatedItems)
// }
//
// if (value.type === 'Object') {
// let updatedProps = updatedValue.props
//
// updatedValue.props.forEach((prop, index) => {
// const updatedItem = recurseTransform(prop.value, path.concat(prop.name), root, callback)
// updatedProps = setIn(updatedProps, [index, 'value'], updatedItem)
// })
//
// updatedValue = setIn(updatedValue, ['props'], updatedProps)
// }
//
// // (for type 'string' or 'value' there are no childs to traverse)
//
// return updatedValue
// }
/**
* Recursively loop over a ESON object: a recursive "forEach" function.
* @param {ESON} eson
* @param {function(value: ESON, path: JSONPath, root: ESON)} callback
*/
export function traverse (eson: ESON, callback: RecurseCallback) {
return recurseTraverse (eson, [], eson, callback)
}
/**
* Recursively traverse a ESON object
* @param {ESON} value
* @param {JSONPath} path
* @param {ESON | null} root The root object, object at path=[]
* @param {function(value: ESON, path: Path, root: ESON)} callback
*/
function recurseTraverse (value: ESON, path: JSONPath, root: ESON, callback: RecurseCallback) {
callback(value, path, root)
switch (value.type) {
case 'Array': {
value.items.forEach((item: ESONArrayItem, index) => {
recurseTraverse(item.value, path.concat(String(index)), root, callback)
})
break
}
case 'Object': {
value.props.forEach((prop) => {
recurseTraverse(prop.value, path.concat(prop.name), root, callback)
})
break
}
default: // type 'string' or 'value'
// no childs to traverse
}
}
/**
* Test whether a path exists in the eson object
@ -808,12 +586,12 @@ export function findNextProp (parent, prop) {
/**
* Find the index of a property
* @param {ESONObject} object
* @param {ESON} object
* @param {string} prop
* @return {number} Returns the index when found, -1 when not found
*/
export function findPropertyIndex (object: ESONObject, prop: string) {
return object.props.findIndex(p => p.name === prop)
export function findPropertyIndex (object, prop) {
return object[META].keys.indexOf(prop)
}
// TODO: move parseJSONPointer and compileJSONPointer to a separate file

View File

@ -3,7 +3,7 @@ import test from 'ava'
import { setIn, getIn, deleteIn } from '../src/utils/immutabilityHelpers'
import {
META,
esonToJson, toEsonPath, toJsonPath, pathExists, transform, traverse,
esonToJson, pathExists, transform,
parseJSONPointer, compileJSONPointer,
jsonToEson,
expand, expandOne, expandPath, applyErrors, search, nextSearchResult,
@ -15,8 +15,6 @@ import 'console.table'
import repeat from 'lodash/repeat'
import { assertDeepEqualEson } from './utils/assertDeepEqualEson'
const JSON1 = loadJSON('./resources/json1.json')
const ESON1 = loadJSON('./resources/eson1.json')
const ESON2 = loadJSON('./resources/eson2.json')
test('jsonToEson', t => {
@ -236,33 +234,6 @@ test('add and remove errors', t => {
t.is(actual3.str, eson.str) // shouldn't have touched values not affected by the errors
})
test('traverse', t => {
// {obj: {a: 2}, arr: [3]}
let log = []
const returnValue = traverse(ESON2, function (value, path, root) {
t.is(root, ESON2)
log.push([value, path, root])
})
t.is(returnValue, undefined)
const EXPECTED_LOG = [
[ESON2, [], ESON2],
[ESON2.props[0].value, ['obj'], ESON2],
[ESON2.props[0].value.props[0].value, ['obj', 'a'], ESON2],
[ESON2.props[1].value, ['arr'], ESON2],
[ESON2.props[1].value.items[0].value, ['arr', '0'], ESON2],
]
log.forEach((row, index) => {
t.deepEqual(log[index], EXPECTED_LOG[index], 'should have equal log at index ' + index )
})
t.deepEqual(log, EXPECTED_LOG)
})
test('search', t => {
const eson = jsonToEson({
"obj": {
@ -476,12 +447,20 @@ test('selection (node)', t => {
})
test('pathsFromSelection (object)', t => {
const eson = jsonToEson({
"obj": {
"arr": [1,2, {"first":3,"last":4}]
},
"str": "hello world",
"nill": null,
"bool": false
})
const selection = {
start: ['obj', 'arr', '2', 'last'],
end: ['nill']
}
t.deepEqual(pathsFromSelection(ESON1, selection), [
t.deepEqual(pathsFromSelection(eson, selection), [
['obj'],
['str'],
['nill']
@ -489,42 +468,74 @@ test('pathsFromSelection (object)', t => {
})
test('pathsFromSelection (array)', t => {
const eson = jsonToEson({
"obj": {
"arr": [1,2, {"first":3,"last":4}]
},
"str": "hello world",
"nill": null,
"bool": false
})
const selection = {
start: ['obj', 'arr', '1'],
end: ['obj', 'arr', '0'] // note the "wrong" order of start and end
}
t.deepEqual(pathsFromSelection(ESON1, selection), [
t.deepEqual(pathsFromSelection(eson, selection), [
['obj', 'arr', '0'],
['obj', 'arr', '1']
])
})
test('pathsFromSelection (value)', t => {
const eson = jsonToEson({
"obj": {
"arr": [1,2, {"first":3,"last":4}]
},
"str": "hello world",
"nill": null,
"bool": false
})
const selection = {
start: ['obj', 'arr', '2', 'first'],
end: ['obj', 'arr', '2', 'first']
}
t.deepEqual(pathsFromSelection(ESON1, selection), [
t.deepEqual(pathsFromSelection(eson, selection), [
['obj', 'arr', '2', 'first'],
])
})
test('pathsFromSelection (before)', t => {
const eson = jsonToEson({
"obj": {
"arr": [1,2, {"first":3,"last":4}]
},
"str": "hello world",
"nill": null,
"bool": false
})
const selection = {
before: ['obj', 'arr', '2', 'first']
}
t.deepEqual(pathsFromSelection(ESON1, selection), [])
t.deepEqual(pathsFromSelection(eson, selection), [])
})
test('pathsFromSelection (after)', t => {
const eson = jsonToEson({
"obj": {
"arr": [1,2, {"first":3,"last":4}]
},
"str": "hello world",
"nill": null,
"bool": false
})
const selection = {
after: ['obj', 'arr', '2', 'first']
}
t.deepEqual(pathsFromSelection(ESON1, selection), [])
t.deepEqual(pathsFromSelection(eson, selection), [])
})
// helper function to print JSON in the console