Skip to content

feat: check cycle reference #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions examples/basic/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,22 @@ function aPlusB (a: number, b: number) {
return a + b
}

const loopObject = {
foo: 1,
goo: 'string'
} as Record<string, any>

loopObject.self = loopObject

const loopArray = [
loopObject
]

loopArray[1] = loopArray

const example = {
loopObject,
loopArray,
string: 'this is a string',
integer: 42,
array: [19, 19, 810, 'test', NaN],
Expand Down
22 changes: 15 additions & 7 deletions src/components/DataKeyPair.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useTextColor } from '../hooks/useColor'
import { useJsonViewerStore } from '../stores/JsonViewerStore'
import { useTypeComponents } from '../stores/typeRegistry'
import type { DataItemProps } from '../type'
import { isCycleReference } from '../utils'
import { DataBox } from './mui/DataBox'

export type DataKeyPairProps = {
Expand All @@ -35,8 +36,14 @@ export const DataKeyPair: React.FC<DataKeyPairProps> = (props) => {
return hoverPath && path.every((value, index) => value === hoverPath[index])
}, [hoverPath, path])
const setHover = useJsonViewerStore(store => store.setHover)
const root = useJsonViewerStore(store => store.value)
const isTrap = useMemo(() => isCycleReference(root, path, value), [path, root, value])
const defaultCollapsed = useJsonViewerStore(store => store.defaultCollapsed)
// do not inspect when it is a cycle reference, otherwise there will have a loop
const [inspect, setInspect] = useState(
!useJsonViewerStore(store => store.defaultCollapsed)
isTrap
? false
: !defaultCollapsed
)
const [editing, setEditing] = useState(false)
const onChange = useJsonViewerStore(store => store.onChange)
Expand All @@ -46,7 +53,7 @@ export const DataKeyPair: React.FC<DataKeyPairProps> = (props) => {
const { Component, PreComponent, PostComponent, Editor } = useTypeComponents(
value)
const rootName = useJsonViewerStore(store => store.rootName)
const isRoot = useJsonViewerStore(store => store.value) === value
const isRoot = root === value
const isNumberKey = Number.isInteger(Number(key))
const displayKey = isRoot ? rootName : key
const downstreamProps: DataItemProps = useMemo(() => ({
Expand Down Expand Up @@ -169,11 +176,12 @@ export const DataKeyPair: React.FC<DataKeyPairProps> = (props) => {
{PreComponent && <PreComponent {...downstreamProps}/>}
{(isHover && expandable && inspect) && actionIcons}
</DataBox>
{editing
? (Editor && <Editor value={tempValue} setValue={setTempValue}/>)
: Component
? <Component {...downstreamProps}/>
: <Box component='span'
{
editing
? (Editor && <Editor value={tempValue} setValue={setTempValue}/>)
: (Component)
? <Component {...downstreamProps}/>
: <Box component='span'
className='data-value-fallback'>{JSON.stringify(value)}</Box>
}
{PostComponent && <PostComponent {...downstreamProps}/>}
Expand Down
112 changes: 0 additions & 112 deletions src/components/DataTypes/Array.tsx

This file was deleted.

73 changes: 60 additions & 13 deletions src/components/DataTypes/Object.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import React, { useMemo } from 'react'
import { useTextColor } from '../../hooks/useColor'
import { useJsonViewerStore } from '../../stores/JsonViewerStore'
import type { DataItemProps } from '../../type'
import { isCycleReference } from '../../utils'
import { DataKeyPair } from '../DataKeyPair'
import { CircularArrowsIcon } from '../icons/CircularArrowsIcon'

const objectLb = '{'
const arrayLb = '['
Expand All @@ -13,11 +15,16 @@ const arrayRb = ']'

export const PreObjectType: React.FC<DataItemProps<object>> = (props) => {
const metadataColor = useJsonViewerStore(store => store.colorNamespace.base04)
const textColor = useTextColor()
const isArray = useMemo(() => Array.isArray(props.value), [props.value])
const sizeOfValue = useMemo(
() => props.inspect ? `${Object.keys(props.value).length} Items` : '',
[props.inspect, props.value]
)
const rootValue = useJsonViewerStore(store => store.value)
const isTrap = useMemo(
() => isCycleReference(rootValue, props.path, props.value),
[props.path, props.value, rootValue])
return (
<Box
component='span' className='data-object-start'
Expand All @@ -36,6 +43,10 @@ export const PreObjectType: React.FC<DataItemProps<object>> = (props) => {
>
{sizeOfValue}
</Box>
{isTrap
? <CircularArrowsIcon
sx={{ fontSize: 12, color: textColor, pl: sizeOfValue ? 0.5 : 0 }}/>
: null}
</Box>
)
}
Expand Down Expand Up @@ -66,14 +77,48 @@ export const PostObjectType: React.FC<DataItemProps<object>> = (props) => {

export const ObjectType: React.FC<DataItemProps<object>> = (props) => {
const keyColor = useTextColor()
const elements = useMemo(() => (
Object.entries(props.value).map(([key, value]) => {
const path = [...props.path, key]
return (
<DataKeyPair key={key} path={path} value={value}/>
)
})
), [props.path, props.value])
const groupArraysAfterLength = useJsonViewerStore(
store => store.groupArraysAfterLength)
const rootValue = useJsonViewerStore(store => store.value)
const isTrap = useMemo(
() => isCycleReference(rootValue, props.path, props.value),
[props.path, props.value, rootValue]
)
const elements = useMemo(() => {
if (Array.isArray(props.value)) {
if (props.value.length <= groupArraysAfterLength) {
return props.value.map((value, index) => {
const path = [...props.path, index]
return (
<DataKeyPair key={index} path={path} value={value}/>
)
})
}
const value = props.value.reduce<unknown[][]>((array, value, index) => {
const target = Math.floor(index / groupArraysAfterLength)
if (array[target]) {
array[target].push(value)
} else {
array[target] = [value]
}
return array
}, [])

return value.map((list, index) => {
const path = [...props.path]
return (
<DataKeyPair key={index} path={path} value={list} nested/>
)
})
} else {
return Object.entries(props.value).map(([key, value]) => {
const path = [...props.path, key]
return (
<DataKeyPair key={key} path={path} value={value}/>
)
})
}
}, [props.value, props.path, groupArraysAfterLength])
return (
<Box
className='data-object'
Expand All @@ -86,11 +131,13 @@ export const ObjectType: React.FC<DataItemProps<object>> = (props) => {
{
props.inspect
? elements
: (
<Box component='span' className='data-object-body'>
...
</Box>
)
: !isTrap
? (
<Box component='span' className='data-object-body'>
...
</Box>
)
: null
}
</Box>
)
Expand Down
10 changes: 10 additions & 0 deletions src/components/icons/CircularArrowsIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { SvgIcon, SvgIconProps } from '@mui/material'
import type React from 'react'

export const CircularArrowsIcon: React.FC<SvgIconProps> = (props) => {
return (
<SvgIcon {...props}>
<path d='M 12 2 C 10.615 1.998 9.214625 2.2867656 7.890625 2.8847656 L 8.9003906 4.6328125 C 9.9043906 4.2098125 10.957 3.998 12 4 C 15.080783 4 17.738521 5.7633175 19.074219 8.3222656 L 17.125 9 L 21.25 11 L 22.875 7 L 20.998047 7.6523438 C 19.377701 4.3110398 15.95585 2 12 2 z M 6.5097656 4.4882812 L 2.2324219 5.0820312 L 3.734375 6.3808594 C 1.6515335 9.4550558 1.3615962 13.574578 3.3398438 17 C 4.0308437 18.201 4.9801562 19.268234 6.1601562 20.115234 L 7.1699219 18.367188 C 6.3019219 17.710187 5.5922656 16.904 5.0722656 16 C 3.5320014 13.332354 3.729203 10.148679 5.2773438 7.7128906 L 6.8398438 9.0625 L 6.5097656 4.4882812 z M 19.929688 13 C 19.794687 14.08 19.450734 15.098 18.927734 16 C 17.386985 18.668487 14.531361 20.090637 11.646484 19.966797 L 12.035156 17.9375 L 8.2402344 20.511719 L 10.892578 23.917969 L 11.265625 21.966797 C 14.968963 22.233766 18.681899 20.426323 20.660156 17 C 21.355156 15.801 21.805219 14.445 21.949219 13 L 19.929688 13 z'/>
</SvgIcon>
)
}
14 changes: 0 additions & 14 deletions src/stores/typeRegistry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@ import { Box } from '@mui/material'
import { DevelopmentError } from '@textea/dev-kit/utils'
import React, { useMemo } from 'react'

import {
ArrayType,
PostArrayType,
PreArrayType
} from '../components/DataTypes/Array'
import { createEasyType } from '../components/DataTypes/createEasyType'
import {
FunctionType, PostFunctionType,
Expand Down Expand Up @@ -205,15 +200,6 @@ registerType<number>(
}
)

registerType<unknown[]>(
{
is: (value): value is unknown[] => Array.isArray(value),
Component: ArrayType,
PreComponent: PreArrayType,
PostComponent: PostArrayType
}
)

// fallback for all data like 'object'
registerType<object>(
{
Expand Down
28 changes: 28 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,31 @@ export const applyValue = (obj: any, path: (string | number)[], value: any) => {
}
return obj
}

export const isCycleReference = (root: any, path: (string | number)[], value: unknown): boolean => {
if (root === null || value === null) {
return false
}
if (typeof root !== 'object') {
return false
}
if (typeof value !== 'object') {
return false
}
if (Object.is(root, value) && path.length !== 0) {
return true
}
const arr = [...path]
let currentRoot = root
while (currentRoot !== value || arr.length !== 0) {
if (typeof currentRoot !== 'object' || currentRoot === null) {
return false
}
const target = arr.shift()!
if (Object.is(currentRoot, value)) {
return true
}
currentRoot = currentRoot[target]
}
return false
}