Skip to content

Commit

Permalink
feat: export utility functions isContent, isTextContent, `isJSONC…
Browse files Browse the repository at this point in the history
…ontent`, `toTextContent`,

`toJSONContent` (#173)

BREAKING CHANGE:

Not exporting a set of undocumented utility functions anymore: `isValueSelection`,
`isKeySelection`, `isInsideSelection`, `isAfterSelection`, `isMultiSelection`,
`isEditingSelection`, `createValueSelection`, `createKeySelection`, `createInsideSelection`,
`createAfterSelection`, `createMultiSelection`. And not exporting components `SortModal` and
`TransformModal` anymore.
  • Loading branch information
josdejong committed Oct 25, 2022
1 parent 19363b4 commit d21fc6d
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 49 deletions.
80 changes: 63 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Or one-way binding:
}
function handleChange(updatedContent, previousContent, { contentErrors, patchResult }) {
// content is an object { json: JSONData } | { text: string }
// content is an object { json: JSONValue } | { text: string }
console.log('onChange: ', { updatedContent, previousContent, contentErrors, patchResult })
content = updatedContent
}
Expand Down Expand Up @@ -126,7 +126,7 @@ Browser example loading the ES module:
props: {
content,
onChange: (updatedContent, previousContent, { contentErrors, patchResult }) => {
// content is an object { json: JSONData } | { text: string }
// content is an object { json: JSONValue } | { text: string }
console.log('onChange', { updatedContent, previousContent, contentErrors, patchResult })
content = updatedContent
}
Expand Down Expand Up @@ -178,7 +178,7 @@ const editor = new JSONEditor({
props: {
content,
onChange: (updatedContent, previousContent, { contentErrors, patchResult }) => {
// content is an object { json: JSONData } | { text: string }
// content is an object { json: JSONValue } | { text: string }
console.log('onChange', { updatedContent, previousContent, contentErrors, patchResult })
}
}
Expand All @@ -197,7 +197,7 @@ const editor = new JSONEditor({
- `tabSize: number` When indentation is configured as a tab character (`indentation: '\t'`), `tabSize` configures how large a tab character is rendered. Default value is `4`. Only applicable to `text` mode.
- `escapeControlCharacters: boolean`. False by default. When `true`, control characters like newline and tab are rendered as escaped characters `\n` and `\t`. Only applicable for `'tree'` mode, in `'text'` mode control characters are always escaped.
- `escapeUnicodeCharacters: boolean`. False by default. When `true`, unicode characters like ☎ and 😀 are rendered escaped like `\u260e` and `\ud83d\ude00`.
- `validator: function (json: JSONData): ValidationError[]`. Validate the JSON document.
- `validator: function (json: JSONValue): ValidationError[]`. Validate the JSON document.
For example use the built-in JSON Schema validator powered by Ajv:

```js
Expand Down Expand Up @@ -329,7 +329,7 @@ const editor = new JSONEditor({
- `editor.expand(path => true)` expand all
- `editor.expand(path => false)` collapse all
- `editor.expand(path => path.length < 2)` expand all paths up to 2 levels deep
- `transform({ id?: string, selectedPath?: [], onTransform: ({ operations: JSONPatchDocument, json: JSONData, transformedJson: JSONData }) => void, onClose: () => void })` programmatically trigger clicking of the transform button in the main menu, opening the transform model. If a callback `onTransform` is provided, it will replace the build-in logic to apply a transform, allowing you to process the transform operations in an alternative way. If provided, `onClose` callback will trigger when the transform modal closes, both after the user clicked apply or cancel. If an `id` is provided, the transform modal will load the previous status of this `id` instead of the status of the editors transform modal.
- `transform({ id?: string, selectedPath?: [], onTransform: ({ operations: JSONPatchDocument, json: JSONValue, transformedJson: JSONValue }) => void, onClose: () => void })` programmatically trigger clicking of the transform button in the main menu, opening the transform model. If a callback `onTransform` is provided, it will replace the build-in logic to apply a transform, allowing you to process the transform operations in an alternative way. If provided, `onClose` callback will trigger when the transform modal closes, both after the user clicked apply or cancel. If an `id` is provided, the transform modal will load the previous status of this `id` instead of the status of the editors transform modal.
- `scrollTo(path: Path)` Scroll the editor vertically such that the specified path comes into view. The path will be expanded when needed.
- `findElement(path: Path)` Find the DOM element of a given path. Returns `null` when not found.
- `acceptAutoRepair(): Content` In tree mode, invalid JSON is automatically repaired when loaded. When the repair was successful, the repaired contents are rendered but not yet applied to the document itself until the user clicks "Ok" or starts editing the data. Instead of accepting the repair, the user can also click "Repair manually instead". Invoking `.acceptAutoRepair()` will programmatically accept the repair. This will trigger an update, and the method itself also returns the updated contents. In case of `text` mode or when the editor is not in an "accept auto repair" status, nothing will happen, and the contents will be returned as is.
Expand All @@ -338,13 +338,59 @@ const editor = new JSONEditor({
- `focus()`. Give the editor focus.
- `destroy()`. Destroy the editor, remove it from the DOM.

### Utility functions

- Rendering of values:
- `renderValue`
- `renderJSONSchemaEnum`
- Components:
- `BooleanToggle`
- `ColorPicker`
- `EditableValue`
- `EnumValue`
- `ReadonlyValue`
- `TimestampTag`
- Validation:
- `createAjvValidator`
- Query languages:
- `lodashQueryLanguage`
- `javascriptQueryLanguage`
- `jmespathQueryLanguage`
- Content:
- `isContent`
- `isTextContent`
- `isJSONContent`
- `isLargeContent`
- `toTextContent`
- `toJSONContent`
- `estimateSerializedSize`
- Parser:
- `isEqualParser`
- Path:
- `parseJSONPath`
- `stringifyJSONPath`
- Functions from [`immutable-json-patch`](https://github.com/josdejong/immutable-json-patch/):
- `immutableJSONPatch`
- `revertJSONPatch`
- `parseJSONPointer`
- `parsePath`
- `parseFrom`
- `compileJSONPointer`
- `compileJSONPointerProp`
- `getIn`
- `setIn`
- `updateIn`
- `insertAt`
- `existsIn`
- `deleteIn`

### Types

```ts
type JSONData = { [key: string]: JSONData } | JSONData[] | string | number | boolean | null
type JSONValue = { [key: string]: JSONValue } | JSONValue[] | string | number | boolean | null
type TextContent = { text: string } | { json: undefined; text: string }
type JSONContent = { json: JSONData } | { json: JSONData; text: undefined }
type TextContent = { text: string }
type JSONContent = { json: JSONValue }
type Content = JSONContent | TextContent
type JSONParser = JSON
Expand All @@ -360,12 +406,12 @@ type JSONPatchOperation = {
op: 'add' | 'remove' | 'replace' | 'copy' | 'move' | 'test'
path: string
from?: string
value?: JSONData
value?: JSONValue
}
type JSONPatchResult = {
json: JSONData
previousJson: JSONData
json: JSONValue
previousJson: JSONValue
undo: JSONPatchDocument
redo: JSONPatchDocument
}
Expand Down Expand Up @@ -398,8 +444,8 @@ interface QueryLanguage {
id: string
name: string
description: string
createQuery: (json: JSONData, queryOptions: QueryLanguageOptions) => string
executeQuery: (json: JSONData, query: string) => JSONData
createQuery: (json: JSONValue, queryOptions: QueryLanguageOptions) => string
executeQuery: (json: JSONValue, query: string) => JSONValue
}
interface QueryLanguageOptions {
Expand All @@ -419,7 +465,7 @@ interface QueryLanguageOptions {
interface RenderValuePropsOptional {
path?: Path
value?: JSONData
value?: JSONValue
readOnly?: boolean
enforceString?: boolean
selection?: Selection
Expand All @@ -428,15 +474,15 @@ interface RenderValuePropsOptional {
isEditing?: boolean
normalization?: ValueNormalization
onPatch?: TreeModeContext['onPatch']
onPasteJson?: (pastedJson: { path: Path; contents: JSONData }) => void
onPasteJson?: (pastedJson: { path: Path; contents: JSONValue }) => void
onSelect?: (selection: Selection) => void
onFind?: (findAndReplace: boolean) => void
focus?: () => void
}
interface RenderValueProps extends RenderValuePropsOptional {
path: Path
value: JSONData
value: JSONValue
readOnly: boolean
enforceString: boolean | undefined
selection: Selection | undefined
Expand All @@ -445,7 +491,7 @@ interface RenderValueProps extends RenderValuePropsOptional {
isEditing: boolean
normalization: ValueNormalization
onPatch: (patch: JSONPatchDocument, afterPatch?: AfterPatchCallback) => JSONPatchResult
onPasteJson: (pastedJson: { path: Path; contents: JSONData }) => void
onPasteJson: (pastedJson: { path: Path; contents: JSONValue }) => void
onSelect: (selection: Selection) => void
onFind: (findAndReplace: boolean) => void
focus: () => void
Expand Down
34 changes: 14 additions & 20 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import JSONEditor from './components/JSONEditor.svelte'
import SortModal from './components/modals/SortModal.svelte'
import TransformModal from './components/modals/TransformModal.svelte'
import BooleanToggle from './plugins/value/components/BooleanToggle.svelte'
import ColorPicker from './plugins/value/components/ColorPicker.svelte'
import EditableValue from './plugins/value/components/EditableValue.svelte'
Expand All @@ -25,30 +23,27 @@ export { lodashQueryLanguage } from './plugins/query/lodashQueryLanguage.js'
export { javascriptQueryLanguage } from './plugins/query/javascriptQueryLanguage.js'
export { jmespathQueryLanguage } from './plugins/query/jmespathQueryLanguage.js'

// utils
export { SortModal, TransformModal }
export { getJSONSchemaOptions, findSchema, findEnum } from './utils/jsonSchemaUtils.js'
// content
export {
isEqualParser,
isContent,
isTextContent,
isJSONContent,
isLargeContent,
toTextContent,
toJSONContent,
estimateSerializedSize
} from './utils/jsonUtils.js'

// parser
export { isEqualParser } from './utils/jsonUtils.js'

// path
export { parseJSONPath, stringifyJSONPath } from './utils/pathUtils.js'

// immutable-json-patch
export {
isValueSelection,
isKeySelection,
isInsideSelection,
isAfterSelection,
isMultiSelection,
isEditingSelection,
createValueSelection,
createKeySelection,
createInsideSelection,
createAfterSelection,
createMultiSelection
} from './logic/selection.js'
export {
immutableJSONPatch,
revertJSONPatch,
parseJSONPointer,
parsePath,
parseFrom,
Expand All @@ -61,4 +56,3 @@ export {
existsIn,
deleteIn
} from 'immutable-json-patch'
export { immutableJSONPatch, revertJSONPatch } from 'immutable-json-patch'
101 changes: 93 additions & 8 deletions src/lib/utils/jsonUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import type { JSONParser } from '$lib/types.js'
import { deepStrictEqual, strictEqual, deepEqual } from 'assert'
import { parse, stringify } from 'lossless-json'
import { deepStrictEqual, strictEqual, deepEqual, throws } from 'assert'
import { LosslessNumber, parse, stringify } from 'lossless-json'
import {
calculatePosition,
convertValue,
countCharacterOccurrences,
estimateSerializedSize,
isContent,
isEqualParser,
isJSONContent,
isLargeContent,
isTextContent,
normalizeJsonParseError,
parsePartialJson,
toJSONContent,
toTextContent,
validateContentType
} from './jsonUtils.js'

const LosslessJSONParser = { parse, stringify }

describe('jsonUtils', () => {
const jsonString = '{\n' + ' id: 2,\n' + ' name: "Jo"\n' + '}'

Expand Down Expand Up @@ -163,7 +169,7 @@ describe('jsonUtils', () => {
}
})

describe('isLargeContent', () => {
it('isLargeContent', () => {
const text = '[1,2,3,4,5,6,7,8,9,0]'
const textContent = { text }
const jsonContent = { json: JSON.parse(text) }
Expand All @@ -175,16 +181,95 @@ describe('jsonUtils', () => {
strictEqual(isLargeContent(jsonContent, 10), true)
})

describe('isTextContent', () => {
it('isContent', () => {
strictEqual(isContent({ text: '' }), true)
strictEqual(isContent({ json: [] }), true)
strictEqual(isContent({ text: '', json: [] }), true)
strictEqual(isContent(1), false)
strictEqual(isContent({}), false)

const f = () => null
f.text = '[]'
strictEqual(isContent(f), false)

class C {
text: '[]'
}
const c = new C()
strictEqual(isContent(c), false)
})

it('isTextContent', () => {
strictEqual(isTextContent({ text: '' }), true)
strictEqual(isTextContent({ json: [] }), false)
strictEqual(isTextContent({ text: '', json: [] }), true)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
strictEqual(isTextContent(1), false)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
strictEqual(isTextContent({}), false)

const f = () => null
f.text = '[]'
strictEqual(isTextContent(f), false)

class C {
text: '[]'
}
const c = new C()
strictEqual(isTextContent(c), false)
})

it('isJSONContent', () => {
strictEqual(isJSONContent({ text: '' }), false)
strictEqual(isJSONContent({ json: [] }), true)
strictEqual(isJSONContent({ text: '', json: [] }), false) // text has precedence over json
strictEqual(isJSONContent(1), false)
strictEqual(isJSONContent({}), false)

const f = () => null
f.json = []
strictEqual(isJSONContent(f), false)

class C {
json: []
}
const c = new C()
strictEqual(isJSONContent(c), false)
})

it('toTextContent', () => {
const textContent = { text: '[1,2,3]' }
strictEqual(toTextContent(textContent), textContent)
deepStrictEqual(toTextContent({ json: [1, 2, 3] }), textContent)
deepStrictEqual(toTextContent({ json: [1, 2, 3] }, 2), { text: '[\n 1,\n 2,\n 3\n]' })

deepStrictEqual(
toTextContent(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
{ json: [new LosslessNumber('1'), new LosslessNumber('2'), new LosslessNumber('3')] },
2,
LosslessJSONParser
),
{ text: '[\n 1,\n 2,\n 3\n]' }
)
})

it('toJSONContent', () => {
const jsonContent = { json: [1, 2, 3] }

deepStrictEqual(toJSONContent({ text: '[1,2,3]' }), jsonContent)
strictEqual(toJSONContent(jsonContent), jsonContent)

deepStrictEqual(toJSONContent({ text: '[1,2,3]' }, LosslessJSONParser as JSONParser), {
json: [new LosslessNumber('1'), new LosslessNumber('2'), new LosslessNumber('3')]
})

throws(() => {
toJSONContent({ text: '[1,2,3' })
}, new SyntaxError('Unexpected end of JSON input'))

throws(() => {
toJSONContent({ text: '[1,2,3' }, LosslessJSONParser as JSONParser)
}, new SyntaxError("Array item or end of array ']' expected but reached end of input at position 6"))
})

describe('convertValue', () => {
Expand Down
Loading

0 comments on commit d21fc6d

Please sign in to comment.