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

View File

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

View File

@ -336,19 +336,19 @@ export default class TreeMode extends Component {
} }
handleChangeValue = (path, value) => { handleChangeValue = (path, value) => {
this.handlePatch(changeValue(this.state.data, path, value)) this.handlePatch(changeValue(this.state.eson, path, value))
} }
handleChangeProperty = (parentPath, oldProp, newProp) => { 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) => { handleChangeType = (path, type) => {
this.handlePatch(changeType(this.state.data, path, type)) this.handlePatch(changeType(this.state.eson, path, type))
} }
handleInsert = (path, type) => { handleInsert = (path, type) => {
this.handlePatch(insertBefore(this.state.data, path, [{ this.handlePatch(insertBefore(this.state.eson, path, [{
type, type,
name: '', name: '',
value: createEntry(type) value: createEntry(type)
@ -367,7 +367,7 @@ export default class TreeMode extends Component {
} }
handleAppend = (parentPath, type) => { handleAppend = (parentPath, type) => {
this.handlePatch(append(this.state.data, parentPath, type)) this.handlePatch(append(this.state.eson, parentPath, type))
// apply focus to new node // apply focus to new node
this.focusToNext(parentPath) this.focusToNext(parentPath)
@ -375,7 +375,7 @@ export default class TreeMode extends Component {
handleDuplicate = () => { handleDuplicate = () => {
if (this.state.selection) { 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 // TODO: focus to duplicated selection
} }
} }
@ -395,7 +395,7 @@ export default class TreeMode extends Component {
else if (this.state.selection) { else if (this.state.selection) {
// remove selection // remove selection
// TODO: select next property? (same as when removing a path?) // 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.setState({ selection: null })
this.handlePatch(removeAll(paths)) this.handlePatch(removeAll(paths))
} }
@ -446,13 +446,13 @@ export default class TreeMode extends Component {
} }
handleKeyDownPaste = (event) => { handleKeyDownPaste = (event) => {
const { clipboard, data } = this.state const { clipboard, eson } = this.state
if (clipboard && clipboard.length > 0) { if (clipboard && clipboard.length > 0) {
event.preventDefault() event.preventDefault()
const path = this.findDataPathFromElement(event.target) 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) const path = this.findDataPathFromElement(event.target)
if (path) { if (path) {
const selection = { start: path, end: 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 // apply focus to the duplicated node
this.focusToNext(path) this.focusToNext(path)
@ -470,9 +470,9 @@ export default class TreeMode extends Component {
handleCut = () => { handleCut = () => {
const selection = this.state.selection const selection = this.state.selection
if (selection && selection.start && selection.end) { if (selection && selection.start && selection.end) {
const data = this.state.data const eson = this.state.eson
const paths = pathsFromSelection(data, selection) const paths = pathsFromSelection(eson, selection)
const clipboard = contentsFromPaths(data, paths) const clipboard = contentsFromPaths(eson, paths)
this.setState({ clipboard, selection: null }) this.setState({ clipboard, selection: null })
@ -490,9 +490,9 @@ export default class TreeMode extends Component {
handleCopy = () => { handleCopy = () => {
const selection = this.state.selection const selection = this.state.selection
if (selection && selection.start && selection.end) { if (selection && selection.start && selection.end) {
const data = this.state.data const eson = this.state.eson
const paths = pathsFromSelection(data, selection) const paths = pathsFromSelection(eson, selection)
const clipboard = contentsFromPaths(data, paths) const clipboard = contentsFromPaths(eson, paths)
this.setState({ clipboard }) this.setState({ clipboard })
} }
@ -503,11 +503,11 @@ export default class TreeMode extends Component {
} }
handlePaste = () => { handlePaste = () => {
const { data, selection, clipboard } = this.state const { eson, selection, clipboard } = this.state
if (selection && clipboard && clipboard.length > 0) { if (selection && clipboard && clipboard.length > 0) {
this.setState({ selection: null }) this.setState({ selection: null })
this.handlePatch(replace(data, selection, clipboard)) this.handlePatch(replace(eson, selection, clipboard))
// TODO: select the pasted contents // TODO: select the pasted contents
} }
} }
@ -539,7 +539,7 @@ export default class TreeMode extends Component {
} }
handleSort = (path, order = null) => { handleSort = (path, order = null) => {
this.handlePatch(sort(this.state.data, path, order)) this.handlePatch(sort(this.state.eson, path, order))
} }
handleSelect = (selection: Selection) => { handleSelect = (selection: Selection) => {
@ -753,13 +753,13 @@ export default class TreeMode extends Component {
* Emit an onChange event when there is a listener for it. * Emit an onChange event when there is a listener for it.
* @private * @private
*/ */
emitOnChange (patch: ESONPatch, revert: ESONPatch, data: ESON) { emitOnChange (patch: ESONPatch, revert: ESONPatch, eson: ESON) {
if (this.props.onPatch) { if (this.props.onPatch) {
this.props.onPatch(patch, revert) this.props.onPatch(patch, revert)
} }
if (this.props.onChange || this.props.onChangeText) { if (this.props.onChange || this.props.onChangeText) {
const json = esonToJson(data) const json = esonToJson(eson)
if (this.props.onChange) { if (this.props.onChange) {
this.props.onChange(json) this.props.onChange(json)
@ -798,10 +798,10 @@ export default class TreeMode extends Component {
const historyIndex = this.state.historyIndex const historyIndex = this.state.historyIndex
const historyItem = history[historyIndex] const historyItem = history[historyIndex]
const result = patchEson(this.state.data, historyItem.undo) const result = patchEson(this.state.eson, historyItem.undo)
this.setState({ this.setState({
data: result.data, eson: result.data,
history, history,
historyIndex: historyIndex + 1 historyIndex: historyIndex + 1
}) })
@ -816,10 +816,10 @@ export default class TreeMode extends Component {
const historyIndex = this.state.historyIndex - 1 const historyIndex = this.state.historyIndex - 1
const historyItem = history[historyIndex] const historyItem = history[historyIndex]
const result = patchEson(this.state.data, historyItem.redo) const result = patchEson(this.state.eson, historyItem.redo)
this.setState({ this.setState({
data: result.data, eson: result.data,
history, history,
historyIndex historyIndex
}) })
@ -845,10 +845,10 @@ export default class TreeMode extends Component {
} }
const expand = options.expand || (path => this.expandKeepOrExpandAll(path)) const expand = options.expand || (path => this.expandKeepOrExpandAll(path))
const result = patchEson(this.state.data, actions, expand) const result = patchEson(this.state.eson, actions, expand)
const data = result.data const eson = result.data
if (this.props.history != false) { if (this.props.history !== false) {
// update data and store history // update data and store history
const historyItem = { const historyItem = {
redo: actions, redo: actions,
@ -860,21 +860,21 @@ export default class TreeMode extends Component {
.slice(0, MAX_HISTORY_ITEMS) .slice(0, MAX_HISTORY_ITEMS)
this.setState({ this.setState({
data, eson,
history, history,
historyIndex: 0 historyIndex: 0
}) })
} }
else { else {
// update data and don't store history // update data and don't store history
this.setState({ data }) this.setState({ eson })
} }
return { return {
patch: actions, patch: actions,
revert: result.revert, revert: result.revert,
error: result.error, 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 * @param {Path} path
*/ */
exists (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 last from 'lodash/last'
import type { import type {
ESON, ESONObject, ESONArrayItem, ESONPointer, Selection, ESONType, ESONPath, ESON, ESONObject, ESONArrayItem, ESONPointer, Selection, ESONPath,
Path, Path,
JSONPath, JSONType JSONPath, JSONType
} from './types' } from './types'
@ -29,6 +29,15 @@ export const SELECTED_AFTER = 4
export const META = Symbol('meta') 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 * @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 * Convert an ESON object to a JSON object
* @param {ESON} eson * @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) * Transform an eson object, traverse over the whole object (excluding the _meta)
* objects, and allow replacing Objects/Arrays/values * 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. * 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, * 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. * 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 start = (selection.after || selection.before || selection.start)[rootPath.length]
const end = (selection.after || selection.before || selection.end)[rootPath.length] const end = (selection.after || selection.before || selection.end)[rootPath.length]
// if no object we assume it's an Array // if no object we assume it's an Array
const startIndex = root.type === 'Object' ? findPropertyIndex(root, start) : parseInt(start) const startIndex = root[META].type === 'Object' ? root[META].keys.indexOf(start) : parseInt(start)
const endIndex = root.type === 'Object' ? findPropertyIndex(root, end) : parseInt(end) const endIndex = root[META].type === 'Object' ? root[META].keys.indexOf(end) : parseInt(end)
const minIndex = Math.min(startIndex, endIndex) const minIndex = Math.min(startIndex, endIndex)
const maxIndex = Math.max(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 * 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 // find the parent node shared by both start and end of the selection
const rootPath = findRootPath(selection) const rootPath = findRootPath(selection)
const root = getInEson(eson, rootPath) const root = getIn(eson, rootPath)
const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection) const { minIndex, maxIndex } = findSelectionIndices(root, rootPath, selection)
if (root.type === 'Object') { if (root[META].type === 'Object') {
return times(maxIndex - minIndex, i => rootPath.concat(root.props[i + minIndex].name)) return times(maxIndex - minIndex, i => rootPath.concat(root[META].keys[i + minIndex]))
} }
else { // root.type === 'Array' else { // root.type === 'Array'
return times(maxIndex - minIndex, i => rootPath.concat(String(i + minIndex))) 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[]) { export function contentsFromPaths (data: ESON, paths: JSONPath[]) {
return paths.map(path => { return paths.map(path => {
const esonPath = toEsonPath(data, path)
return { return {
name: getIn(data, initial(esonPath).concat('name')) || String(esonPath[esonPath.length - 2]), name: getIn(data, last(path)),
value: esonToJson(getIn(data, esonPath)) value: esonToJson(getIn(data, path))
// FIXME: also store the type and expanded state // FIXME: also store the type and expanded state
} }
}) })
@ -659,93 +524,6 @@ function findSharedPath (path1: JSONPath, path2: JSONPath): JSONPath {
return path1.slice(0, i) 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 * Test whether a path exists in the eson object
@ -808,12 +586,12 @@ export function findNextProp (parent, prop) {
/** /**
* Find the index of a property * Find the index of a property
* @param {ESONObject} object * @param {ESON} object
* @param {string} prop * @param {string} prop
* @return {number} Returns the index when found, -1 when not found * @return {number} Returns the index when found, -1 when not found
*/ */
export function findPropertyIndex (object: ESONObject, prop: string) { export function findPropertyIndex (object, prop) {
return object.props.findIndex(p => p.name === prop) return object[META].keys.indexOf(prop)
} }
// TODO: move parseJSONPointer and compileJSONPointer to a separate file // 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 { setIn, getIn, deleteIn } from '../src/utils/immutabilityHelpers'
import { import {
META, META,
esonToJson, toEsonPath, toJsonPath, pathExists, transform, traverse, esonToJson, pathExists, transform,
parseJSONPointer, compileJSONPointer, parseJSONPointer, compileJSONPointer,
jsonToEson, jsonToEson,
expand, expandOne, expandPath, applyErrors, search, nextSearchResult, expand, expandOne, expandPath, applyErrors, search, nextSearchResult,
@ -15,8 +15,6 @@ import 'console.table'
import repeat from 'lodash/repeat' import repeat from 'lodash/repeat'
import { assertDeepEqualEson } from './utils/assertDeepEqualEson' import { assertDeepEqualEson } from './utils/assertDeepEqualEson'
const JSON1 = loadJSON('./resources/json1.json')
const ESON1 = loadJSON('./resources/eson1.json')
const ESON2 = loadJSON('./resources/eson2.json') const ESON2 = loadJSON('./resources/eson2.json')
test('jsonToEson', t => { 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 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 => { test('search', t => {
const eson = jsonToEson({ const eson = jsonToEson({
"obj": { "obj": {
@ -476,12 +447,20 @@ test('selection (node)', t => {
}) })
test('pathsFromSelection (object)', 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 = { const selection = {
start: ['obj', 'arr', '2', 'last'], start: ['obj', 'arr', '2', 'last'],
end: ['nill'] end: ['nill']
} }
t.deepEqual(pathsFromSelection(ESON1, selection), [ t.deepEqual(pathsFromSelection(eson, selection), [
['obj'], ['obj'],
['str'], ['str'],
['nill'] ['nill']
@ -489,42 +468,74 @@ test('pathsFromSelection (object)', t => {
}) })
test('pathsFromSelection (array)', 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 = { const selection = {
start: ['obj', 'arr', '1'], start: ['obj', 'arr', '1'],
end: ['obj', 'arr', '0'] // note the "wrong" order of start and end 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', '0'],
['obj', 'arr', '1'] ['obj', 'arr', '1']
]) ])
}) })
test('pathsFromSelection (value)', t => { test('pathsFromSelection (value)', t => {
const eson = jsonToEson({
"obj": {
"arr": [1,2, {"first":3,"last":4}]
},
"str": "hello world",
"nill": null,
"bool": false
})
const selection = { const selection = {
start: ['obj', 'arr', '2', 'first'], start: ['obj', 'arr', '2', 'first'],
end: ['obj', 'arr', '2', 'first'] end: ['obj', 'arr', '2', 'first']
} }
t.deepEqual(pathsFromSelection(ESON1, selection), [ t.deepEqual(pathsFromSelection(eson, selection), [
['obj', 'arr', '2', 'first'], ['obj', 'arr', '2', 'first'],
]) ])
}) })
test('pathsFromSelection (before)', t => { test('pathsFromSelection (before)', t => {
const eson = jsonToEson({
"obj": {
"arr": [1,2, {"first":3,"last":4}]
},
"str": "hello world",
"nill": null,
"bool": false
})
const selection = { const selection = {
before: ['obj', 'arr', '2', 'first'] before: ['obj', 'arr', '2', 'first']
} }
t.deepEqual(pathsFromSelection(ESON1, selection), []) t.deepEqual(pathsFromSelection(eson, selection), [])
}) })
test('pathsFromSelection (after)', t => { test('pathsFromSelection (after)', t => {
const eson = jsonToEson({
"obj": {
"arr": [1,2, {"first":3,"last":4}]
},
"str": "hello world",
"nill": null,
"bool": false
})
const selection = { const selection = {
after: ['obj', 'arr', '2', 'first'] after: ['obj', 'arr', '2', 'first']
} }
t.deepEqual(pathsFromSelection(ESON1, selection), []) t.deepEqual(pathsFromSelection(eson, selection), [])
}) })
// helper function to print JSON in the console // helper function to print JSON in the console