New ESON model (WIP)

This commit is contained in:
jos 2017-11-29 21:52:18 +01:00
parent a9174edf16
commit c19334894c
11 changed files with 414 additions and 227 deletions

69
package-lock.json generated
View File

@ -2130,6 +2130,15 @@
"array-find-index": "1.0.2" "array-find-index": "1.0.2"
} }
}, },
"d": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz",
"integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=",
"dev": true,
"requires": {
"es5-ext": "0.10.37"
}
},
"dashdash": { "dashdash": {
"version": "1.14.1", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@ -2200,6 +2209,17 @@
"integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=",
"dev": true "dev": true
}, },
"deep-map": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/deep-map/-/deep-map-1.5.0.tgz",
"integrity": "sha1-6qWVy4F4PKKADyakLgnxbn1PuJA=",
"dev": true,
"requires": {
"es6-weak-map": "2.0.2",
"lodash": "4.17.4",
"tslib": "1.8.0"
}
},
"defaults": { "defaults": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
@ -2547,6 +2567,49 @@
"is-arrayish": "0.2.1" "is-arrayish": "0.2.1"
} }
}, },
"es5-ext": {
"version": "0.10.37",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.37.tgz",
"integrity": "sha1-DudB0Ui4AGm6J9AgOTdWryV978M=",
"dev": true,
"requires": {
"es6-iterator": "2.0.3",
"es6-symbol": "3.1.1"
}
},
"es6-iterator": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
"integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
"dev": true,
"requires": {
"d": "1.0.0",
"es5-ext": "0.10.37",
"es6-symbol": "3.1.1"
}
},
"es6-symbol": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz",
"integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=",
"dev": true,
"requires": {
"d": "1.0.0",
"es5-ext": "0.10.37"
}
},
"es6-weak-map": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz",
"integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=",
"dev": true,
"requires": {
"d": "1.0.0",
"es5-ext": "0.10.37",
"es6-iterator": "2.0.3",
"es6-symbol": "3.1.1"
}
},
"escape-html": { "escape-html": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@ -7372,6 +7435,12 @@
"integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
"dev": true "dev": true
}, },
"tslib": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.8.0.tgz",
"integrity": "sha512-ymKWWZJST0/CkgduC2qkzjMOWr4bouhuURNXCn/inEX0L57BnRG6FhX76o7FOnsjHazCjfU2LKeSrlS2sIKQJg==",
"dev": true
},
"tty-browserify": { "tty-browserify": {
"version": "0.0.0", "version": "0.0.0",
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",

View File

@ -42,6 +42,7 @@
"babel-preset-stage-3": "6.17.0", "babel-preset-stage-3": "6.17.0",
"browser-sync": "2.18.6", "browser-sync": "2.18.6",
"css-loader": "0.26.1", "css-loader": "0.26.1",
"deep-map": "1.5.0",
"flow-bin": "0.37.4", "flow-bin": "0.37.4",
"graceful-fs": "4.1.11", "graceful-fs": "4.1.11",
"gulp": "3.9.1", "gulp": "3.9.1",

View File

@ -8,7 +8,7 @@ import FloatingMenu from './menu/FloatingMenu'
import { escapeHTML, unescapeHTML } from '../utils/stringUtils' import { escapeHTML, unescapeHTML } from '../utils/stringUtils'
import { getInnerText, insideRect, findParentWithAttribute } from '../utils/domUtils' import { getInnerText, insideRect, findParentWithAttribute } from '../utils/domUtils'
import { stringConvert, valueType, isUrl } from '../utils/typeUtils' import { stringConvert, valueType, isUrl } from '../utils/typeUtils'
import { compileJSONPointer, SELECTED, SELECTED_END, SELECTED_AFTER, SELECTED_BEFORE } from '../eson' import { compileJSONPointer, mapEsonArray, SELECTED, SELECTED_END, SELECTED_AFTER, SELECTED_BEFORE } from '../eson'
import type { ESONObjectProperty, ESON, SearchResultStatus, Path } from '../types' import type { ESONObjectProperty, ESON, SearchResultStatus, Path } from '../types'
@ -36,12 +36,6 @@ export default class JSONNode extends PureComponent {
hover: false hover: false
} }
constructor (props) {
super(props)
this.path = this.getPath(props)
}
componentWillMount (props) { componentWillMount (props) {
} }
@ -51,26 +45,20 @@ export default class JSONNode extends PureComponent {
} }
} }
componentWillReceiveProps (nextProps) {
this.path = this.getPath(nextProps)
}
render () { render () {
const { props } = this if (this.props.eson._meta.type === 'Object') {
return this.renderJSONObject(this.props)
if (props.data.type === 'Array') {
return this.renderJSONArray(props)
} }
else if (props.data.type === 'Object') { else if (this.props.eson._meta.type === 'Array') {
return this.renderJSONObject(props) return this.renderJSONArray(this.props)
} }
else { else { // no Object or Array
return this.renderJSONValue(props) return this.renderJSONValue(this.props)
} }
} }
renderJSONObject ({prop, index, data, options, events}) { renderJSONObject ({prop, index, eson, options, events}) {
const childCount = data.props.length const keys = eson._meta.keys
const node = h('div', { const node = h('div', {
key: 'node', key: 'node',
onKeyDown: this.handleKeyDown, onKeyDown: this.handleKeyDown,
@ -79,20 +67,20 @@ export default class JSONNode extends PureComponent {
this.renderExpandButton(), this.renderExpandButton(),
// this.renderActionMenu('update', this.state.menu, this.handleCloseActionMenu), // this.renderActionMenu('update', this.state.menu, this.handleCloseActionMenu),
// this.renderActionMenuButton(), // this.renderActionMenuButton(),
this.renderProperty(prop, index, data, options), this.renderProperty(prop, index, eson, options),
this.renderReadonly(`{${childCount}}`, `Array containing ${childCount} items`), this.renderReadonly(`{${keys.length}}`, `Array containing ${keys.length} items`),
// this.renderFloatingMenuButton(), // this.renderFloatingMenuButton(),
this.renderError(data.error) this.renderError(eson._meta.error) // FIXME: render error
]) ])
let childs let childs
if (data.expanded) { if (eson._meta.expanded) {
if (data.props.length > 0) { if (keys.length > 0) {
const props = data.props.map(prop => h(this.constructor, { const props = keys.map(key => h(this.constructor, {
key: prop.id, key: eson[key]._meta.id,
parent: this, // parent: this,
prop, prop: key,
data: prop.value, eson: eson[key],
options, options,
events events
})) }))
@ -106,7 +94,7 @@ export default class JSONNode extends PureComponent {
} }
} }
const floatingMenu = (data.selected === SELECTED_END) const floatingMenu = (eson._meta.selected === SELECTED_END)
? this.renderFloatingMenu([ ? this.renderFloatingMenu([
{type: 'sort'}, {type: 'sort'},
{type: 'duplicate'}, {type: 'duplicate'},
@ -120,16 +108,14 @@ export default class JSONNode extends PureComponent {
const insertArea = this.renderInsertBeforeArea() const insertArea = this.renderInsertBeforeArea()
return h('div', { return h('div', {
'data-path': compileJSONPointer(this.path), 'data-path': compileJSONPointer(this.props.eson._meta.path),
className: this.getContainerClassName(data.selected, this.state.hover), className: this.getContainerClassName(eson._meta.selected, this.state.hover),
onMouseOver: this.handleMouseOver, onMouseOver: this.handleMouseOver,
onMouseLeave: this.handleMouseLeave onMouseLeave: this.handleMouseLeave
}, [node, floatingMenu, insertArea, childs]) }, [node, floatingMenu, insertArea, childs])
} }
// TODO: extract a function renderChilds shared by both renderJSONObject and renderJSONArray (rename .props and .items to .childs?) renderJSONArray ({prop, index, eson, options, events}) {
renderJSONArray ({prop, index, data, options, events}) {
const childCount = data.items.length
const node = h('div', { const node = h('div', {
key: 'node', key: 'node',
onKeyDown: this.handleKeyDown, onKeyDown: this.handleKeyDown,
@ -138,20 +124,20 @@ export default class JSONNode extends PureComponent {
this.renderExpandButton(), this.renderExpandButton(),
// this.renderActionMenu('update', this.state.menu, this.handleCloseActionMenu), // this.renderActionMenu('update', this.state.menu, this.handleCloseActionMenu),
// this.renderActionMenuButton(), // this.renderActionMenuButton(),
this.renderProperty(prop, index, data, options), this.renderProperty(prop, index, eson, options),
this.renderReadonly(`[${childCount}]`, `Array containing ${childCount} items`), this.renderReadonly(`[${eson._meta.length}]`, `Array containing ${eson._meta.length} items`),
// this.renderFloatingMenuButton(), // this.renderFloatingMenuButton(),
this.renderError(data.error) this.renderError(eson._meta.error)
]) ])
let childs let childs
if (data.expanded) { if (eson._meta.expanded) {
if (data.items.length > 0) { if (eson._meta.length > 0) {
const items = data.items.map((item, index) => h(this.constructor, { const items = mapEsonArray(eson, (item, index) => h(this.constructor, {
key : item.id, key : item._meta.id,
parent: this, // parent: this,
index, index,
data: item.value, eson: item,
options, options,
events events
})) }))
@ -165,7 +151,7 @@ export default class JSONNode extends PureComponent {
} }
} }
const floatingMenu = (data.selected === SELECTED_END) const floatingMenu = (eson._meta.selected === SELECTED_END)
? this.renderFloatingMenu([ ? this.renderFloatingMenu([
{type: 'sort'}, {type: 'sort'},
{type: 'duplicate'}, {type: 'duplicate'},
@ -179,14 +165,14 @@ export default class JSONNode extends PureComponent {
const insertArea = this.renderInsertBeforeArea() const insertArea = this.renderInsertBeforeArea()
return h('div', { return h('div', {
'data-path': compileJSONPointer(this.path), 'data-path': compileJSONPointer(this.props.eson._meta.path),
className: this.getContainerClassName(data.selected, this.state.hover), className: this.getContainerClassName(eson._meta.selected, this.state.hover),
onMouseOver: this.handleMouseOver, onMouseOver: this.handleMouseOver,
onMouseLeave: this.handleMouseLeave onMouseLeave: this.handleMouseLeave
}, [node, floatingMenu, insertArea, childs]) }, [node, floatingMenu, insertArea, childs])
} }
renderJSONValue ({prop, index, data, options}) { renderJSONValue ({prop, index, eson, options}) {
const node = h('div', { const node = h('div', {
key: 'node', key: 'node',
onKeyDown: this.handleKeyDown, onKeyDown: this.handleKeyDown,
@ -195,14 +181,14 @@ export default class JSONNode extends PureComponent {
this.renderPlaceholder(), this.renderPlaceholder(),
// this.renderActionMenu('update', this.state.menu, this.handleCloseActionMenu), // this.renderActionMenu('update', this.state.menu, this.handleCloseActionMenu),
// this.renderActionMenuButton(), // this.renderActionMenuButton(),
this.renderProperty(prop, index, data, options), this.renderProperty(prop, index, eson, options),
this.renderSeparator(), this.renderSeparator(),
this.renderValue(data.value, data.searchResult, options), this.renderValue(eson._meta.value, eson._meta.searchResult, options),
// this.renderFloatingMenuButton(), // this.renderFloatingMenuButton(),
this.renderError(data.error) this.renderError(eson._meta.error)
]) ])
const floatingMenu = (data.selected === SELECTED_END) const floatingMenu = (eson._meta.selected === SELECTED_END)
? this.renderFloatingMenu([ ? this.renderFloatingMenu([
// {text: 'String', onClick: this.props.events.onChangeType, type: 'checkbox', checked: false}, // {text: 'String', onClick: this.props.events.onChangeType, type: 'checkbox', checked: false},
{type: 'duplicate'}, {type: 'duplicate'},
@ -216,15 +202,15 @@ export default class JSONNode extends PureComponent {
const insertArea = this.renderInsertBeforeArea() const insertArea = this.renderInsertBeforeArea()
return h('div', { return h('div', {
'data-path': compileJSONPointer(this.path), 'data-path': compileJSONPointer(this.props.eson._meta.path),
className: this.getContainerClassName(data.selected, this.state.hover), className: this.getContainerClassName(eson._meta.selected, this.state.hover),
onMouseOver: this.handleMouseOver, onMouseOver: this.handleMouseOver,
onMouseLeave: this.handleMouseLeave onMouseLeave: this.handleMouseLeave
}, [node, floatingMenu, insertArea]) }, [node, floatingMenu, insertArea])
} }
renderInsertBeforeArea () { renderInsertBeforeArea () {
const floatingMenu = (this.props.data.selected === SELECTED_BEFORE) const floatingMenu = (this.props.eson._meta.selected === SELECTED_BEFORE)
? this.renderFloatingMenu([ ? this.renderFloatingMenu([
{type: 'insertStructure'}, {type: 'insertStructure'},
{type: 'insertValue'}, {type: 'insertValue'},
@ -248,7 +234,7 @@ export default class JSONNode extends PureComponent {
*/ */
renderAppend (text) { renderAppend (text) {
return h('div', { return h('div', {
'data-path': compileJSONPointer(this.path) + '/-', 'data-path': compileJSONPointer(this.props.eson._meta.path) + '/-',
className: 'jsoneditor-node', className: 'jsoneditor-node',
onKeyDown: this.handleKeyDownAppend onKeyDown: this.handleKeyDownAppend
}, [ }, [
@ -268,12 +254,12 @@ export default class JSONNode extends PureComponent {
} }
// TODO: simplify the method renderProperty // TODO: simplify the method renderProperty
renderProperty (prop?: ESONObjectProperty, index?: number, data: ESON, options: {escapeUnicode: boolean, isPropertyEditable: (Path) => boolean}) { renderProperty (prop?: ESONObjectProperty, index?: number, eson: ESON, options: {escapeUnicode: boolean, isPropertyEditable: (Path) => boolean}) {
const isIndex = typeof index === 'number' const isIndex = typeof index === 'number'
if (!prop && !isIndex) { if (!prop && !isIndex) {
// root node // root node
const rootName = JSONNode.getRootName(data, options) const rootName = JSONNode.getRootName(eson, options)
return h('div', { return h('div', {
key: 'property', key: 'property',
@ -283,11 +269,11 @@ export default class JSONNode extends PureComponent {
}, rootName) }, rootName)
} }
const editable = !isIndex && (!options.isPropertyEditable || options.isPropertyEditable(this.path)) const editable = !isIndex && (!options.isPropertyEditable || options.isPropertyEditable(this.props.eson._meta.path))
const emptyClassName = (prop && prop.name.length === 0) ? ' jsoneditor-empty' : '' const emptyClassName = (prop != null && prop.length === 0) ? ' jsoneditor-empty' : ''
const searchClassName = prop ? JSONNode.getSearchResultClass(prop.searchResult) : '' const searchClassName = prop != null ? JSONNode.getSearchResultClass(prop.searchResult) : ''
const escapedPropName = prop ? escapeHTML(prop.name, options.escapeUnicode) : null const escapedPropName = prop != null ? escapeHTML(prop, options.escapeUnicode) : null
if (editable) { if (editable) {
return h('div', { return h('div', {
@ -317,7 +303,7 @@ export default class JSONNode extends PureComponent {
const itsAnUrl = isUrl(value) const itsAnUrl = isUrl(value)
const isEmpty = escapedValue.length === 0 const isEmpty = escapedValue.length === 0
const editable = !options.isValueEditable || options.isValueEditable(this.path) const editable = !options.isValueEditable || options.isValueEditable(this.props.eson._meta.path)
if (editable) { if (editable) {
return h('div', { return h('div', {
key: 'value', key: 'value',
@ -409,7 +395,7 @@ export default class JSONNode extends PureComponent {
} }
target.className = JSONNode.getValueClass(type, itsAnUrl, isEmpty) + target.className = JSONNode.getValueClass(type, itsAnUrl, isEmpty) +
JSONNode.getSearchResultClass(this.props.data.searchResult) JSONNode.getSearchResultClass(this.props.eson._meta.searchResult)
target.title = itsAnUrl ? JSONNode.URL_TITLE : '' target.title = itsAnUrl ? JSONNode.URL_TITLE : ''
// remove all classNames from childs (needed for IE and Edge) // remove all classNames from childs (needed for IE and Edge)
@ -462,7 +448,7 @@ export default class JSONNode extends PureComponent {
} }
renderExpandButton () { renderExpandButton () {
const className = `jsoneditor-button jsoneditor-${this.props.data.expanded ? 'expanded' : 'collapsed'}` const className = `jsoneditor-button jsoneditor-${this.props.eson._meta.expanded ? 'expanded' : 'collapsed'}`
return h('div', {key: 'expand', className: 'jsoneditor-button-container'}, return h('div', {key: 'expand', className: 'jsoneditor-button-container'},
h('button', { h('button', {
@ -483,9 +469,9 @@ export default class JSONNode extends PureComponent {
return h(ActionMenu, { return h(ActionMenu, {
key: 'menu', key: 'menu',
path: this.path, path: this.props.eson._meta.path,
events: this.props.events, events: this.props.events,
type: this.props.data.type, type: this.props.eson._meta.type, // TODO: fix type
menuType, menuType,
open: true, open: true,
@ -527,7 +513,7 @@ export default class JSONNode extends PureComponent {
renderFloatingMenu (items) { renderFloatingMenu (items) {
return h(FloatingMenu, { return h(FloatingMenu, {
key: 'floating-menu', key: 'floating-menu',
path: this.path, path: this.props.eson._meta.path,
events: this.props.events, events: this.props.events,
items items
}) })
@ -609,17 +595,15 @@ export default class JSONNode extends PureComponent {
this.setState({ appendMenu: null }) this.setState({ appendMenu: null })
} }
static getRootName (data, options) { static getRootName (eson, options) {
return typeof options.name === 'string' return typeof options.name === 'string'
? options.name ? options.name
: (data.type === 'Object' || data.type === 'Array') : valueType(eson)
? data.type
: valueType(data.value)
} }
/** @private */ /** @private */
handleChangeProperty = (event) => { handleChangeProperty = (event) => {
const parentPath = initial(this.path) const parentPath = initial(this.props.eson._meta.path)
const oldProp = this.props.prop.name const oldProp = this.props.prop.name
const newProp = unescapeHTML(getInnerText(event.target)) const newProp = unescapeHTML(getInnerText(event.target))
@ -632,8 +616,8 @@ export default class JSONNode extends PureComponent {
handleChangeValue = (event) => { handleChangeValue = (event) => {
const value = this.getValueFromEvent(event) const value = this.getValueFromEvent(event)
if (value !== this.props.data.value) { if (value !== this.props.eson._meta.value) {
this.props.events.onChangeValue(this.path, value) this.props.events.onChangeValue(this.props.eson._meta.path, value)
} }
} }
@ -650,24 +634,24 @@ export default class JSONNode extends PureComponent {
if (keyBinding === 'duplicate') { if (keyBinding === 'duplicate') {
event.preventDefault() event.preventDefault()
this.props.events.onDuplicate(this.path) this.props.events.onDuplicate(this.props.eson._meta.path)
} }
if (keyBinding === 'insert') { if (keyBinding === 'insert') {
event.preventDefault() event.preventDefault()
this.props.events.onInsert(this.path, 'value') this.props.events.onInsert(this.props.eson._meta.path, 'value')
} }
if (keyBinding === 'remove') { if (keyBinding === 'remove') {
event.preventDefault() event.preventDefault()
this.props.events.onRemove(this.path) this.props.events.onRemove(this.props.eson._meta.path)
} }
if (keyBinding === 'expand') { if (keyBinding === 'expand') {
event.preventDefault() event.preventDefault()
const recurse = false const recurse = false
const expanded = !this.props.data.expanded const expanded = !this.props.eson._meta.expanded
this.props.events.onExpand(this.path, expanded, recurse) this.props.events.onExpand(this.props.eson._meta.path, expanded, recurse)
} }
if (keyBinding === 'actionMenu') { if (keyBinding === 'actionMenu') {
@ -682,7 +666,7 @@ export default class JSONNode extends PureComponent {
if (keyBinding === 'insert') { if (keyBinding === 'insert') {
event.preventDefault() event.preventDefault()
this.props.events.onAppend(this.path, 'value') this.props.events.onAppend(this.props.eson._meta.path, 'value')
} }
if (keyBinding === 'actionMenu') { if (keyBinding === 'actionMenu') {
@ -703,9 +687,10 @@ export default class JSONNode extends PureComponent {
/** @private */ /** @private */
handleExpand = (event) => { handleExpand = (event) => {
const recurse = event.ctrlKey const recurse = event.ctrlKey
const expanded = !this.props.data.expanded const path = this.props.eson._meta.path
const expanded = !this.props.eson._meta.expanded
this.props.events.onExpand(this.path, expanded, recurse) this.props.events.onExpand(path, expanded, recurse)
} }
/** /**
@ -724,17 +709,6 @@ export default class JSONNode extends PureComponent {
} }
} }
// FIXME: this construction with passing parents to determine the path is not very nice. Move determining of the path to the ESON model. We cannot generate the path whilst rendering, that defeats the efficiency of PureComponent
getPath (props = this.props) {
const parentPath = props.parent ? props.parent.path : []
return props.prop
? parentPath.concat(props.prop.name)
: typeof props.index !== 'undefined'
? parentPath.concat(props.index)
: parentPath
}
/** /**
* Get the value of the target of an event, and convert it to it's type * Get the value of the target of an event, and convert it to it's type
* @param event * @param event
@ -743,7 +717,7 @@ export default class JSONNode extends PureComponent {
*/ */
getValueFromEvent (event) { getValueFromEvent (event) {
const stringValue = unescapeHTML(getInnerText(event.target)) const stringValue = unescapeHTML(getInnerText(event.target))
return this.props.data.type === 'string' return this.props.eson._meta.type === 'string'
? stringValue ? stringValue
: stringConvert(stringValue) : stringConvert(stringValue)
} }

View File

@ -8,11 +8,11 @@ import Hammer from 'react-hammerjs'
import jump from '../assets/jump.js/src/jump' import jump from '../assets/jump.js/src/jump'
import Ajv from 'ajv' import Ajv from 'ajv'
import { setIn } from '../utils/immutabilityHelpers' import { setIn, updateIn } from '../utils/immutabilityHelpers'
import { parseJSON } from '../utils/jsonUtils' import { parseJSON } from '../utils/jsonUtils'
import { enrichSchemaError } from '../utils/schemaUtils' import { enrichSchemaError } from '../utils/schemaUtils'
import { import {
jsonToEson, esonToJson, getInEson, updateInEson, pathExists, toEson2, jsonToEson, esonToJson, getInEson, updateInEson, pathExists,
expand, expandPath, addErrors, expand, expandPath, addErrors,
search, applySearchResults, nextSearchResult, previousSearchResult, search, applySearchResults, nextSearchResult, previousSearchResult,
applySelection, pathsFromSelection, contentsFromPaths, applySelection, pathsFromSelection, contentsFromPaths,
@ -20,7 +20,7 @@ import {
} from '../eson' } from '../eson'
import { patchEson } from '../patchEson' import { patchEson } from '../patchEson'
import { import {
duplicate, insert, insertBefore, append, remove, removeAll, replace, duplicate, insertBefore, append, remove, removeAll, replace,
createEntry, changeType, changeValue, changeProperty, sort createEntry, changeType, changeValue, changeProperty, sort
} from '../actions' } from '../actions'
import JSONNode from './JSONNode' import JSONNode from './JSONNode'
@ -56,7 +56,9 @@ export default class TreeMode extends Component {
constructor (props) { constructor (props) {
super(props) super(props)
const data = jsonToEson(this.props.data || {}, TreeMode.expandAll, []) const json = this.props.json || {}
const expandCallback = this.props.expand || TreeMode.expandRoot
const eson = expand(toEson2(json), expandCallback)
this.id = Math.round(Math.random() * 1e5) // TODO: create a uuid here? this.id = Math.round(Math.random() * 1e5) // TODO: create a uuid here?
@ -79,9 +81,10 @@ export default class TreeMode extends Component {
} }
this.state = { this.state = {
data, json,
eson,
history: [data], history: [eson],
historyIndex: 0, historyIndex: 0,
events: { events: {
@ -148,11 +151,17 @@ export default class TreeMode extends Component {
// Apply json // Apply json
if (nextProps.json !== currentProps.json) { if (nextProps.json !== currentProps.json) {
this.patch([{ // FIXME: merge _meta from existing eson
op: 'replace', this.setState({
path: '', json: nextProps.json,
value: nextProps.json eson: toEson2(nextProps.json) // FIXME: how to handle expand?
}]) })
// TODO: cleanup
// this.patch([{
// op: 'replace',
// path: '',
// value: nextProps.json
// }])
} }
// Apply JSON Schema // Apply JSON Schema
@ -172,7 +181,7 @@ export default class TreeMode extends Component {
// TODO: apply patch // TODO: apply patch
} }
render () { render() {
const { props, state } = this const { props, state } = this
const Node = (props.mode === 'view') const Node = (props.mode === 'view')
@ -182,21 +191,23 @@ export default class TreeMode extends Component {
: JSONNode : JSONNode
// enrich the data with JSON Schema errors // enrich the data with JSON Schema errors
let data = state.data let eson = state.eson
const errors = this.getErrors() // TODO: reimplement errors
if (errors.length) { // const errors = this.getErrors()
data = addErrors(data, this.getErrors()) // if (errors.length) {
} // data = addErrors(data, this.getErrors())
// }
// enrich the data with search results // enrich the data with search results
// TODO: performance improvements in search would be nice though it's acceptable right now // TODO: reimplement search and selection
const searchResults = this.state.search.text ? search(data, this.state.search.text) : null const searchResults = []
if (searchResults) { // const searchResults = this.state.search.text ? search(data, this.state.search.text) : null
data = applySearchResults(data, searchResults, this.state.search.active) // if (searchResults) {
} // data = applySearchResults(data, searchResults, this.state.search.active)
if (this.state.selection) { // }
data = applySelection(data, this.state.selection) // if (this.state.selection) {
} // data = applySelection(data, this.state.selection)
// }
return h('div', { return h('div', {
className: `jsoneditor jsoneditor-mode-${props.mode}`, className: `jsoneditor jsoneditor-mode-${props.mode}`,
@ -220,12 +231,11 @@ export default class TreeMode extends Component {
onMouseDown: this.handleTouchStart, onMouseDown: this.handleTouchStart,
onTouchStart: this.handleTouchStart, onTouchStart: this.handleTouchStart,
className: 'jsoneditor-list jsoneditor-root' + className: 'jsoneditor-list jsoneditor-root' +
(data.selected ? ' jsoneditor-selected' : '')}, (eson._meta.selected ? ' jsoneditor-selected' : '')},
h(Node, { h(Node, {
data, eson,
events: state.events, events: state.events,
options: props, options: props,
path: [],
prop: null prop: null
}) })
) )
@ -313,7 +323,7 @@ export default class TreeMode extends Component {
*/ */
getErrors () { getErrors () {
if (this.state.compiledSchema) { if (this.state.compiledSchema) {
const valid = this.state.compiledSchema(esonToJson(this.state.data)) const valid = this.state.compiledSchema(this.state.json)
if (!valid) { if (!valid) {
return this.state.compiledSchema.errors.map(enrichSchemaError) return this.state.compiledSchema.errors.map(enrichSchemaError)
} }
@ -547,14 +557,14 @@ export default class TreeMode extends Component {
handleExpand = (path, expanded, recurse) => { handleExpand = (path, expanded, recurse) => {
if (recurse) { if (recurse) {
this.setState({ this.setState({
data: updateInEson(this.state.data, path, function (child) { eson: updateIn(this.state.eson, path, function (child) {
return expand(child, (path) => true, expanded) return expand(child, (path) => true, expanded)
}) })
}) })
} }
else { else {
this.setState({ this.setState({
data: expand(this.state.data, path, expanded) eson: expand(this.state.eson, path, expanded)
}) })
} }
} }
@ -885,10 +895,11 @@ export default class TreeMode extends Component {
set (json) { set (json) {
// FIXME: when both json and expand are being changed via React, this.props must be updated before set(json) is called // FIXME: when both json and expand are being changed via React, this.props must be updated before set(json) is called
// TODO: document option expand // TODO: document option expand
const expand = this.props.expand || TreeMode.expandRoot const expandCallback = this.props.expand || TreeMode.expandRoot
this.setState({ this.setState({
data: jsonToEson(json, expand, []), json: json,
eson: expand(toEson2(json), expandCallback), // FIXME: expand eson
// TODO: do we want to keep history when .set(json) is called? (currently we remove history) // TODO: do we want to keep history when .set(json) is called? (currently we remove history)
history: [], history: [],
@ -901,7 +912,7 @@ export default class TreeMode extends Component {
* @returns {Object | Array | string | number | boolean | null} json * @returns {Object | Array | string | number | boolean | null} json
*/ */
get () { get () {
return esonToJson(this.state.data) return this.state.json
} }
/** /**

View File

@ -5,7 +5,7 @@
* All functions are pure and don't mutate the ESON. * All functions are pure and don't mutate the ESON.
*/ */
import { setIn, getIn, updateIn, deleteIn } from './utils/immutabilityHelpers' import { setIn, getIn, updateIn, deleteIn, transform } from './utils/immutabilityHelpers'
import { isObject } from './utils/typeUtils' import { isObject } from './utils/typeUtils'
import isEqual from 'lodash/isEqual' import isEqual from 'lodash/isEqual'
import times from 'lodash/times' import times from 'lodash/times'
@ -25,6 +25,51 @@ export const SELECTED_END = 2
export const SELECTED_BEFORE = 3 export const SELECTED_BEFORE = 3
export const SELECTED_AFTER = 4 export const SELECTED_AFTER = 4
/**
*
* @param {JSONType} json
* @param {JSONPath} path
* @return {ESON}
*/
// TODO: rename to jsonToEson after refactoring of ESON is finished
export function toEson2 (json, path = []) {
const id = createId()
if (isObject(json)) {
let eson = {}
const keys = Object.keys(json)
keys.forEach((key) => eson[key] = toEson2(json[key], path.concat(key)))
eson._meta = { id, path, type: 'Object', keys }
return eson
}
else if (Array.isArray(json)) {
let eson = {}
json.forEach((value, index) => eson[index] = toEson2(value, path.concat(index)))
eson._meta = { id, path, type: 'Array', length: json.length }
return eson
}
else { // json is a number, string, boolean, or null
return {
_meta: { id, path, type: 'value', value: json }
}
}
}
/**
* Map over an eson array
* @param {ESONArray} esonArray
* @param {function (value, index, array)} callback
* @return {Array}
*/
export function mapEsonArray (esonArray, callback) {
const length = esonArray._meta.length
let result = []
for (let i = 0; i < length; i++) {
result[i] = callback(esonArray[i], i, esonArray)
}
return result
}
/** /**
* Expand function which will expand all nodes * Expand function which will expand all nodes
* @param {Path} path * @param {Path} path
@ -49,7 +94,7 @@ export function jsonToEson (json, expand = expandAll, path: JSONPath = [], type:
expanded: expand(path), expanded: expand(path),
items: json.map((child, index) => { items: json.map((child, index) => {
return { return {
id: getId(), // TODO: use id based on index (only has to be unique within this array) id: createId(), // TODO: use id based on index (only has to be unique within this array)
value: jsonToEson(child, expand, path.concat(index)) value: jsonToEson(child, expand, path.concat(index))
} }
}) })
@ -61,7 +106,7 @@ export function jsonToEson (json, expand = expandAll, path: JSONPath = [], type:
expanded: expand(path), expanded: expand(path),
props: Object.keys(json).map((name, index) => { props: Object.keys(json).map((name, index) => {
return { return {
id: getId(), // TODO: use id based on index (only has to be unique within this array) id: createId(), // TODO: use id based on index (only has to be unique within this array)
name, name,
value: jsonToEson(json[name], expand, path.concat(name)) value: jsonToEson(json[name], expand, path.concat(name))
} }
@ -207,31 +252,25 @@ export function deleteInEson (eson: ESON, jsonPath: JSONPath) : JSONType {
/** /**
* Expand or collapse one or multiple items or properties * Expand or collapse one or multiple items or properties
* @param {ESON} eson * @param {ESON} eson
* @param {function(path: Path) : boolean | Path} callback * @param {function(Path) : boolean | Path} filterCallback
* When a path, the object/array at this path will be expanded/collapsed * When a path, the object/array at this path will be expanded/collapsed
* When a function, all objects and arrays for which callback * When a function, all objects and arrays for which callback
* returns true will be expanded/collapsed * returns true will be expanded/collapsed
* @param {boolean} [expanded=true] New expanded state: true to expand, false to collapse * @param {boolean} [expanded=true] New expanded state: true to expand, false to collapse
* @return {ESON} * @return {ESON}
*/ */
export function expand (eson: ESON, callback: Path | (Path) => boolean, expanded: boolean = true) { export function expand (eson, filterCallback, expanded = true) {
// console.log('expand', callback, expand) // console.log('expand', callback, expand)
if (typeof callback === 'function') { if (typeof filterCallback === 'function') {
return transform(eson, function (value: ESON, path: Path, root: ESON) : ESON { return transform(eson, function (value, path) {
if (value.type === 'Array' || value.type === 'Object') { return (value && value._meta && (value._meta.type === 'Array' || value._meta.type === 'Object') && filterCallback(path))
if (callback(path)) { ? setIn(value, ['_meta', 'expanded'], expanded)
return setIn(value, ['expanded'], expanded) : value
}
}
return value
}) })
} }
else if (Array.isArray(callback)) { else if (Array.isArray(filterCallback)) {
const esonPath: Path = toEsonPath(eson, callback) return setIn(eson, filterCallback.concat(['_meta', 'expanded']), expanded)
return setIn(eson, esonPath.concat(['expanded']), expanded)
} }
else { else {
throw new Error('Callback function or path expected') throw new Error('Callback function or path expected')
@ -511,54 +550,54 @@ function findSharedPath (path1: JSONPath, path2: JSONPath): JSONPath {
return path1.slice(0, i) return path1.slice(0, i)
} }
//
/** // /**
* Recursively transform ESON: a recursive "map" function // * Recursively transform ESON: a recursive "map" function
* @param {ESON} eson // * @param {ESON} eson
* @param {function(value: ESON, path: Path, root: ESON)} callback // * @param {function(value: ESON, path: Path, root: ESON)} callback
* @return {ESON} Returns the transformed eson object // * @return {ESON} Returns the transformed eson object
*/ // */
export function transform (eson: ESON, callback: RecurseCallback) : ESON { // export function transform (eson: ESON, callback: RecurseCallback) : ESON {
return recurseTransform (eson, [], eson, callback) // return recurseTransform (eson, [], eson, callback)
} // }
//
/** // /**
* Recursively transform ESON // * Recursively transform ESON
* @param {ESON} value // * @param {ESON} value
* @param {JSONPath} path // * @param {JSONPath} path
* @param {ESON} root The root object, object at path=[] // * @param {ESON} root The root object, object at path=[]
* @param {function(value: ESON, path: Path, root: ESON)} callback // * @param {function(value: ESON, path: Path, root: ESON)} callback
* @return {ESON} Returns the transformed eson object // * @return {ESON} Returns the transformed eson object
*/ // */
function recurseTransform (value: ESON, path: JSONPath, root: ESON, callback: RecurseCallback) : ESON { // function recurseTransform (value: ESON, path: JSONPath, root: ESON, callback: RecurseCallback) : ESON {
let updatedValue: ESON = callback(value, path, root) // let updatedValue: ESON = callback(value, path, root)
//
if (value.type === 'Array') { // if (value.type === 'Array') {
let updatedItems = updatedValue.items // let updatedItems = updatedValue.items
//
updatedValue.items.forEach((item, index) => { // updatedValue.items.forEach((item, index) => {
const updatedItem = recurseTransform(item.value, path.concat(String(index)), root, callback) // const updatedItem = recurseTransform(item.value, path.concat(String(index)), root, callback)
updatedItems = setIn(updatedItems, [index, 'value'], updatedItem) // updatedItems = setIn(updatedItems, [index, 'value'], updatedItem)
}) // })
//
updatedValue = setIn(updatedValue, ['items'], updatedItems) // updatedValue = setIn(updatedValue, ['items'], updatedItems)
} // }
//
if (value.type === 'Object') { // if (value.type === 'Object') {
let updatedProps = updatedValue.props // let updatedProps = updatedValue.props
//
updatedValue.props.forEach((prop, index) => { // updatedValue.props.forEach((prop, index) => {
const updatedItem = recurseTransform(prop.value, path.concat(prop.name), root, callback) // const updatedItem = recurseTransform(prop.value, path.concat(prop.name), root, callback)
updatedProps = setIn(updatedProps, [index, 'value'], updatedItem) // updatedProps = setIn(updatedProps, [index, 'value'], updatedItem)
}) // })
//
updatedValue = setIn(updatedValue, ['props'], updatedProps) // updatedValue = setIn(updatedValue, ['props'], updatedProps)
} // }
//
// (for type 'string' or 'value' there are no childs to traverse) // // (for type 'string' or 'value' there are no childs to traverse)
//
return updatedValue // return updatedValue
} // }
/** /**
* Recursively loop over a ESON object: a recursive "forEach" function. * Recursively loop over a ESON object: a recursive "forEach" function.
@ -709,7 +748,7 @@ export function compileJSONPointer (path: Path) {
.join('') .join('')
} }
// TODO: move getId and createUniqueId to a separate file // TODO: move createId to a separate file
/** /**
* Do a case insensitive search for a search text in a text * Do a case insensitive search for a search text in a text
@ -725,19 +764,8 @@ export function containsCaseInsensitive (text: string, search: string): boolean
* Get a new "unique" id. Id's are created from an incremental counter. * Get a new "unique" id. Id's are created from an incremental counter.
* @return {number} * @return {number}
*/ */
// TODO: use createUniqueId instead of getId() export function createId () : number {
export function getId () : number {
_id++ _id++
return _id return _id
} }
let _id = 0 let _id = 0
/**
* Find a unique id from an array with properties each having an id field.
* The
* @param {{id: string}} array
*/
// TODO: use createUniqueId instead of getId()
function createUniqueId (array) {
return Math.max(...array.map(item => item.id)) + 1
}

View File

@ -8,7 +8,7 @@ import {
jsonToEson, esonToJson, toEsonPath, jsonToEson, esonToJson, toEsonPath,
getInEson, setInEson, deleteInEson, getInEson, setInEson, deleteInEson,
parseJSONPointer, compileJSONPointer, parseJSONPointer, compileJSONPointer,
expandAll, pathExists, resolvePathIndex, findPropertyIndex, findNextProp, getId expandAll, pathExists, resolvePathIndex, findPropertyIndex, findNextProp, createId
} from './eson' } from './eson'
/** /**
@ -197,7 +197,7 @@ export function remove (data: ESON, path: string) {
* @return {{data: ESON, revert: ESONPatch}} * @return {{data: ESON, revert: ESONPatch}}
* @private * @private
*/ */
export function add (data: ESON, path: string, value: ESON, options, id = getId()) { export function add (data: ESON, path: string, value: ESON, options, id = createId()) {
const pathArray = parseJSONPointer(path) const pathArray = parseJSONPointer(path)
const parentPath = pathArray.slice(0, pathArray.length - 1) const parentPath = pathArray.slice(0, pathArray.length - 1)
const esonPath = toEsonPath(data, parentPath) const esonPath = toEsonPath(data, parentPath)

View File

@ -20,8 +20,12 @@
* ace: Object? * ace: Object?
* }} Options * }} Options
* *
* @typedef {string[]} Path
*
*/ */
// FIXME: redefine all ESON related types
/**************************** GENERIC JSON TYPES ******************************/ /**************************** GENERIC JSON TYPES ******************************/
@ -48,24 +52,32 @@ export type ESONArrayItem = {
} }
export type ESONObject = { export type ESONObject = {
type: 'Object', _meta: {
expanded?: boolean, type: 'Object',
selected?: boolean, path: JSONPath,
props: ESONObjectProperty[] expanded?: boolean,
selected?: boolean,
}
} }
export type ESONArray = { export type ESONArray = {
type: 'Array', _meta: {
expanded?: boolean, type: 'Array',
selected?: boolean, path: JSONPath,
items: ESONArrayItem[] expanded?: boolean,
selected?: boolean,
length: number
}
} }
export type ESONValue = { export type ESONValue = {
type: 'value' | 'string', _meta: {
value?: any, type: 'value' | 'string',
selected?: boolean, path: JSONPath,
searchResult?: SearchResultStatus value: null | boolean | string | number,
selected?: boolean,
searchResult?: SearchResultStatus
}
} }
export type ESON = ESONObject | ESONArray | ESONValue export type ESON = ESONObject | ESONArray | ESONValue

View File

@ -1,7 +1,7 @@
'use strict'; 'use strict';
import clone from 'lodash/clone' import clone from 'lodash/clone'
import { isObjectOrArray } from './typeUtils' import { isObjectOrArray, isObject } from './typeUtils'
/** /**
* Immutability helpers * Immutability helpers
@ -11,6 +11,7 @@ import { isObjectOrArray } from './typeUtils'
* https://www.npmjs.com/package/seamless-immutable * https://www.npmjs.com/package/seamless-immutable
* https://www.npmjs.com/package/ih * https://www.npmjs.com/package/ih
* https://www.npmjs.com/package/mutatis * https://www.npmjs.com/package/mutatis
* https://github.com/mariocasciaro/object-path-immutable
*/ */
@ -70,6 +71,35 @@ export function setIn (object, path, value) {
return updatedObject return updatedObject
} }
} }
export function transform (object, callback, path = []) {
const updated = callback(object, path)
if (Array.isArray(updated)) {
let changed = false
let updatedItems = []
for (let i = 0; i < updated.length; i++) {
updatedItems[i] = transform(updated[i], callback, path.concat(i))
changed = changed || updatedItems[i] !== updated[i]
}
return changed ? updatedItems : updated
}
else if (isObject(updated)) {
let changed = false
let updatedProps = {}
for (let key in updated) {
if (updated.hasOwnProperty(key)) {
updatedProps[key] = transform(updated[key], callback, path.concat(key))
changed = changed || updatedProps[key] !== updated[key]
}
}
return changed ? updatedProps : updated
}
else { // updated is a value
return updated
}
}
/** /**
* helper function to replace a nested property in an object with a new value * helper function to replace a nested property in an object with a new value
* without mutating the object itself. * without mutating the object itself.

View File

@ -10,7 +10,21 @@
export function isObject (value) { export function isObject (value) {
return typeof value === 'object' && return typeof value === 'object' &&
value !== null && value !== null &&
!Array.isArray(value) !Array.isArray(value) &&
(!value._meta || typeof value._meta.value === 'undefined')
}
/**
* Test whether a value is not an object or array, but null, number, string, or
* boolean.
* @param {*} value
* @return {boolean}
*/
export function isValue (value) {
return (value === null ||
typeof value === 'number' ||
typeof value === 'string' ||
typeof value === 'boolean')
} }
/** /**

View File

@ -4,10 +4,12 @@ import { setIn, getIn } from '../src/utils/immutabilityHelpers'
import { import {
jsonToEson, esonToJson, toEsonPath, toJsonPath, pathExists, transform, traverse, jsonToEson, esonToJson, toEsonPath, toJsonPath, pathExists, transform, traverse,
parseJSONPointer, compileJSONPointer, parseJSONPointer, compileJSONPointer,
toEson2,
expand, addErrors, search, applySearchResults, nextSearchResult, previousSearchResult, expand, addErrors, search, applySearchResults, nextSearchResult, previousSearchResult,
applySelection, pathsFromSelection, applySelection, pathsFromSelection,
SELECTED, SELECTED_END SELECTED, SELECTED_END
} from '../src/eson' } from '../src/eson'
import deepMap from "deep-map/lib/index"
const JSON1 = loadJSON('./resources/json1.json') const JSON1 = loadJSON('./resources/json1.json')
const ESON1 = loadJSON('./resources/eson1.json') const ESON1 = loadJSON('./resources/eson1.json')
@ -35,6 +37,25 @@ test('toJsonPath', t => {
t.deepEqual(toJsonPath(ESON1, esonPath), jsonPath) t.deepEqual(toJsonPath(ESON1, esonPath), jsonPath)
}) })
test('toEson2', t => {
t.deepEqual(replaceIds2(toEson2(1)), {_meta: {id: '[ID]', path: [], type: 'value', value: 1}})
t.deepEqual(replaceIds2(toEson2("foo")), {_meta: {id: '[ID]', path: [], type: 'value', value: "foo"}})
t.deepEqual(replaceIds2(toEson2(null)), {_meta: {id: '[ID]', path: [], type: 'value', value: null}})
t.deepEqual(replaceIds2(toEson2(false)), {_meta: {id: '[ID]', path: [], type: 'value', value: false}})
t.deepEqual(replaceIds2(toEson2({a:1, b: 2})), {
_meta: {id: '[ID]', path: [], type: 'Object', keys: ['a', 'b']},
a: {_meta: {id: '[ID]', path: ['a'], type: 'value', value: 1}},
b: {_meta: {id: '[ID]', path: ['b'], type: 'value', value: 2}}
})
printJSON(replaceIds2(toEson2([1,2])))
t.deepEqual(replaceIds2(toEson2([1,2])), {
_meta: {id: '[ID]', path: [], type: 'Array', length: 2},
0: {_meta: {id: '[ID]', path: [0], type: 'value', value: 1}},
1: {_meta: {id: '[ID]', path: [1], type: 'value', value: 2}}
})
})
test('jsonToEson', t => { test('jsonToEson', t => {
function expand (path) { function expand (path) {
return true return true
@ -396,6 +417,11 @@ function replaceIds (data, value = '[ID]') {
} }
} }
// helper function to replace all id properties with a constant value
function replaceIds2 (data, key = 'id', value = '[ID]') {
return deepMap(data, (v, k) => k === key ? value : v)
}
// helper function to print JSON in the console // helper function to print JSON in the console
function printJSON (json, message = null) { function printJSON (json, message = null) {
if (message) { if (message) {

View File

@ -1,5 +1,5 @@
import test from 'ava'; import test from 'ava';
import { getIn, setIn, updateIn, deleteIn, insertAt } from '../src/utils/immutabilityHelpers' import { getIn, setIn, updateIn, deleteIn, insertAt, transform } from '../src/utils/immutabilityHelpers'
test('getIn', t => { test('getIn', t => {
@ -276,3 +276,25 @@ test('insertAt', t => {
const updated = insertAt(obj, ['a', '2'], 8) const updated = insertAt(obj, ['a', '2'], 8)
t.deepEqual(updated, {a: [1,2,8,3]}) t.deepEqual(updated, {a: [1,2,8,3]})
}) })
test('transform (no change)', t => {
const obj = { a: [1,2,3]}
const updated = transform(obj, (value, path) => value)
t.deepEqual(updated, obj)
t.is(updated, obj)
})
test('transform (change based on value)', t => {
const obj = { a: [1,2,3]}
const updated = transform(obj, (value, path) => value === 2 ? 20 : value)
t.deepEqual(updated, { a: [1,20,3]})
})
test('transform (change based on path)', t => {
const obj = { a: [1,2,3]}
const updated = transform(obj, (value, path) => path.join('.') === 'a.1' ? 20 : value)
t.deepEqual(updated, { a: [1,20,3]})
})