improve json parser

This commit is contained in:
AykutSarac 2022-08-31 20:48:40 +03:00
parent 633f5bb8f1
commit a94d951c35
15 changed files with 288 additions and 276 deletions

View File

@ -1,6 +1,5 @@
import React from "react";
import { MdCompareArrows } from "react-icons/md";
import { NodeData } from "reaflow/dist/types";
import { ConditionalWrapper, CustomNodeProps } from "src/components/CustomNode";
import useConfig from "src/hooks/store/useConfig";
import useGraph from "src/hooks/store/useGraph";
@ -26,12 +25,15 @@ const StyledExpand = styled.button`
border-left: 1px solid ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
`;
const TextNode: React.FC<CustomNodeProps<string> & { node: NodeData }> = ({
const TextNode: React.FC<
CustomNodeProps<string> & { node: NodeData; hasCollapse: boolean }
> = ({
node,
width,
height,
value,
isParent = false,
hasCollapse = false,
x,
y,
}) => {
@ -60,7 +62,7 @@ const TextNode: React.FC<CustomNodeProps<string> & { node: NodeData }> = ({
<ConditionalWrapper condition={performanceMode}>
<Styled.StyledText
hideCollapse={hideCollapse}
parent={isParent}
hasCollapse={isParent && hasCollapse}
width={width}
height={height}
>
@ -76,7 +78,7 @@ const TextNode: React.FC<CustomNodeProps<string> & { node: NodeData }> = ({
</Styled.StyledKey>
</Styled.StyledText>
</ConditionalWrapper>
{isParent && !hideCollapse && (
{isParent && hasCollapse && !hideCollapse && (
<StyledExpand onClick={handleExpand}>
<MdCompareArrows size={18} />
</StyledExpand>

View File

@ -39,11 +39,9 @@ export const CustomNode = (nodeProps: NodeProps) => {
<Node {...nodeProps} label={<Label style={baseLabelStyle} />}>
{({ width, height, x, y, node }) => {
if (properties.text instanceof Object) {
const entries = Object.entries<string>(properties.text);
return (
<ObjectNode
value={entries}
value={properties.text}
width={width}
height={height}
x={x}
@ -59,6 +57,7 @@ export const CustomNode = (nodeProps: NodeProps) => {
value={properties.text}
width={width}
height={height}
hasCollapse={properties.data.hasChild}
x={x}
y={y}
/>

View File

@ -26,7 +26,7 @@ export const StyledTextWrapper = styled.div`
export const StyledText = styled.div<{
width: number;
height: number;
parent?: boolean;
hasCollapse?: boolean;
hideCollapse?: boolean;
}>`
display: flex;
@ -36,8 +36,8 @@ export const StyledText = styled.div<{
height: ${({ height }) => height};
min-height: 50;
color: ${({ theme }) => theme.TEXT_NORMAL};
padding-right: ${({ parent, hideCollapse }) =>
parent && !hideCollapse && "20px"};
padding-right: ${({ hasCollapse, hideCollapse }) =>
hasCollapse && !hideCollapse && "20px"};
`;
export const StyledForeignObject = styled.foreignObject`

View File

@ -4,19 +4,19 @@ import {
TransformComponent,
TransformWrapper,
} from "react-zoom-pan-pinch";
import { Canvas, Edge, ElkRoot, NodeData } from "reaflow";
import { Canvas, Edge, ElkRoot } from "reaflow";
import { CustomNode } from "src/components/CustomNode";
import { NodeModal } from "src/containers/Modals/NodeModal";
import { getEdgeNodes } from "src/containers/Editor/LiveEditor/helpers";
import useConfig from "src/hooks/store/useConfig";
import styled from "styled-components";
import shallow from "zustand/shallow";
import useGraph from "src/hooks/store/useGraph";
import { parser } from "src/utils/jsonParser";
interface LayoutProps {
isWidget: boolean;
openModal: () => void;
setSelectedNode: (node: object) => void;
setSelectedNode: (node: [string, string][]) => void;
}
const StyledEditorWrapper = styled.div<{ isWidget: boolean }>`
@ -40,6 +40,7 @@ const MemoizedGraph = React.memo(function Layout({
setSelectedNode,
}: LayoutProps) {
const json = useConfig((state) => state.json);
const setConfig = useConfig((state) => state.setConfig);
const nodes = useGraph((state) => state.nodes);
const edges = useGraph((state) => state.edges);
const setGraphValue = useGraph((state) => state.setGraphValue);
@ -49,7 +50,6 @@ const MemoizedGraph = React.memo(function Layout({
height: 2000,
});
const setConfig = useConfig((state) => state.setConfig);
const [expand, layout] = useConfig(
(state) => [state.expand, state.layout],
shallow
@ -63,17 +63,21 @@ const MemoizedGraph = React.memo(function Layout({
[openModal, setSelectedNode]
);
const onInit = (ref: ReactZoomPanPinchRef) => {
setConfig("zoomPanPinch", ref);
};
const onInit = React.useCallback(
(ref: ReactZoomPanPinchRef) => {
setConfig("zoomPanPinch", ref);
},
[setConfig]
);
const onLayoutChange = (layout: ElkRoot) => {
if (layout.width && layout.height)
setSize({ width: layout.width, height: layout.height });
};
const onLayoutChange = React.useCallback((layout: ElkRoot) => {
if (layout.width && layout.height) {
setSize({ width: layout.width + 400, height: layout.height + 400 });
}
}, []);
React.useEffect(() => {
const { nodes, edges } = getEdgeNodes(json, expand);
const { nodes, edges } = parser(json, expand);
setGraphValue("nodes", nodes);
setGraphValue("edges", edges);
@ -82,13 +86,13 @@ const MemoizedGraph = React.memo(function Layout({
return (
<StyledEditorWrapper isWidget={isWidget}>
<TransformWrapper
onInit={onInit}
maxScale={1.8}
minScale={0.4}
maxScale={2}
minScale={0.5}
initialScale={0.7}
wheel={{ step: 0.1 }}
wheel={{ step: 0.05 }}
zoomAnimation={{ animationType: "linear" }}
doubleClick={{ disabled: true }}
onInit={onInit}
>
<TransformComponent
wrapperStyle={{
@ -100,23 +104,23 @@ const MemoizedGraph = React.memo(function Layout({
<Canvas
nodes={nodes}
edges={edges}
maxWidth={size.width + 400}
maxHeight={size.height + 400}
maxWidth={size.width}
maxHeight={size.height}
direction={layout}
key={layout}
onLayoutChange={onLayoutChange}
node={(props) => (
<CustomNode {...props} onClick={handleNodeClick} />
)}
edge={(props) => (
<Edge {...props} containerClassName={`edge-${props.id}`} />
)}
key={layout}
zoomable={false}
animated={false}
readonly={true}
dragEdge={null}
dragNode={null}
fit={true}
node={(props) => (
<CustomNode {...props} onClick={handleNodeClick} />
)}
edge={(props) => (
<Edge {...props} containerClassName={`edge-${props.id}`} />
)}
/>
</TransformComponent>
</TransformWrapper>
@ -126,7 +130,9 @@ const MemoizedGraph = React.memo(function Layout({
export const Graph = ({ isWidget = false }: { isWidget?: boolean }) => {
const [isModalVisible, setModalVisible] = React.useState(false);
const [selectedNode, setSelectedNode] = React.useState<object>({});
const [selectedNode, setSelectedNode] = React.useState<[string, string][]>(
[]
);
const openModal = React.useCallback(() => setModalVisible(true), []);

View File

@ -2,7 +2,6 @@ import React from "react";
import toast from "react-hot-toast";
import Link from "next/link";
import styled from "styled-components";
import { CanvasDirection } from "reaflow";
import { TiFlowMerge } from "react-icons/ti";
import { CgArrowsMergeAltH, CgArrowsShrinkH } from "react-icons/cg";
import {
@ -21,10 +20,10 @@ import { ImportModal } from "src/containers/Modals/ImportModal";
import { ClearModal } from "src/containers/Modals/ClearModal";
import { ShareModal } from "src/containers/Modals/ShareModal";
import useConfig from "src/hooks/store/useConfig";
import { getNextLayout } from "src/containers/Editor/LiveEditor/helpers";
import { HiHeart } from "react-icons/hi";
import shallow from "zustand/shallow";
import { MdCenterFocusWeak } from "react-icons/md";
import { getNextLayout } from "src/utils/getNextLayout";
const StyledSidebar = styled.div`
display: flex;
@ -129,7 +128,7 @@ const StyledLogo = styled.div`
color: ${({ theme }) => theme.FULL_WHITE};
`;
function rotateLayout(layout: CanvasDirection) {
function rotateLayout(layout: "LEFT" | "RIGHT" | "DOWN" | "UP") {
if (layout === "LEFT") return 90;
if (layout === "UP") return 180;
if (layout === "RIGHT") return 270;

View File

@ -1,87 +0,0 @@
import { CanvasDirection, NodeData, EdgeData } from "reaflow";
import { parser } from "src/utils/json-editor-parser";
export function getEdgeNodes(
graph: string,
isExpanded: boolean = true
): {
nodes: NodeData[];
edges: EdgeData[];
} {
const elements = parser(JSON.parse(graph));
let nodes: NodeData[] = [],
edges: EdgeData[] = [];
for (let i = 0; i < elements.length; i++) {
const el = elements[i];
if (isNode(el)) {
const text = renderText(el.text);
const lines = text.split("\n");
const lineLengths = lines
.map((line) => line.length)
.sort((a, b) => a - b);
const longestLine = lineLengths.reverse()[0];
const height = lines.length * 17.8 < 30 ? 40 : lines.length * 17.8;
nodes.push({
id: el.id,
text: el.text,
data: {
isParent: el.parent,
},
width: isExpanded ? 35 + longestLine * 8 + (el.parent && 60) : 180,
height,
});
} else {
edges.push(el);
}
}
return {
nodes,
edges,
};
}
export function getNextLayout(layout: CanvasDirection) {
switch (layout) {
case "RIGHT":
return "DOWN";
case "DOWN":
return "LEFT";
case "LEFT":
return "UP";
default:
return "RIGHT";
}
}
function renderText(value: string | object) {
if (value instanceof Object) {
let temp = "";
const entries = Object.entries(value);
if (Object.keys(value).every((val) => !isNaN(+val))) {
return Object.values(value).join("");
}
entries.forEach((entry) => {
temp += `${entry[0]}: ${String(entry[1])}\n`;
});
return temp;
}
return value;
}
function isNode(element: NodeData | EdgeData) {
if ("text" in element) return true;
return false;
}

View File

@ -30,9 +30,13 @@ export const NodeModal = ({
visible,
closeModal,
}: NodeModalProps) => {
const nodeData = Array.isArray(selectedNode)
? Object.fromEntries(selectedNode)
: selectedNode;
const handleClipboard = () => {
toast.success("Content copied to clipboard!");
navigator.clipboard.writeText(JSON.stringify(selectedNode));
navigator.clipboard.writeText(JSON.stringify(nodeData));
closeModal();
};
@ -42,8 +46,8 @@ export const NodeModal = ({
<Modal.Content>
<StyledTextarea
defaultValue={JSON.stringify(
selectedNode,
(k, v) => {
nodeData,
(_, v) => {
if (typeof v === "string") return v.replaceAll('"', "");
return v;
},

View File

@ -1,7 +1,6 @@
import create from "zustand";
import { defaultJson } from "src/constants/data";
import { ReactZoomPanPinchRef } from "react-zoom-pan-pinch";
import { CanvasDirection } from "reaflow";
interface ConfigActions {
setJson: (json: string) => void;
@ -15,7 +14,7 @@ interface ConfigActions {
export interface Config {
json: string;
cursorMode: "move" | "navigation";
layout: CanvasDirection;
layout: "LEFT" | "RIGHT" | "DOWN" | "UP";
expand: boolean;
hideEditor: boolean;
zoomPanPinch?: ReactZoomPanPinchRef;
@ -34,7 +33,7 @@ const initialStates: Config = {
const useConfig = create<Config & ConfigActions>()((set, get) => ({
...initialStates,
getJson: () => get().json,
setJson: (json: string) => set((state) => ({ ...state, json })),
setJson: (json: string) => set({ json }),
zoomIn: () => {
const zoomPanPinch = get().zoomPanPinch;
if (zoomPanPinch) {
@ -57,13 +56,10 @@ const useConfig = create<Config & ConfigActions>()((set, get) => ({
},
centerView: () => {
const zoomPanPinch = get().zoomPanPinch;
if (zoomPanPinch) zoomPanPinch.resetTransform();
if (zoomPanPinch) zoomPanPinch.centerView(0.6);
},
setConfig: (setting: keyof Config, value: unknown) =>
set((state) => ({
...state,
[setting]: value,
})),
set({ [setting]: value }),
}));
export default useConfig;

View File

@ -1,5 +1,4 @@
import create from "zustand";
import { EdgeData, NodeData } from "reaflow/dist/types";
import { Graph } from "src/components/Graph";
import { getChildrenEdges } from "src/utils/getChildrenEdges";
import { getOutgoers } from "src/utils/getOutgoers";
@ -24,7 +23,7 @@ const initialStates: Graph = {
collapsedEdges: [],
};
const useGraph = create<Graph & GraphActions>((set) => ({
const useGraph = create<Graph & GraphActions>((set, get) => ({
...initialStates,
setGraphValue: (key, value) =>
set({
@ -32,38 +31,34 @@ const useGraph = create<Graph & GraphActions>((set) => ({
collapsedEdges: [],
[key]: value,
}),
expandNodes: (nodeId) =>
set((state) => {
const childrenNodes = getOutgoers(nodeId, state.nodes, state.edges);
const childrenEdges = getChildrenEdges(childrenNodes, state.edges);
expandNodes: (nodeId) => {
const childrenNodes = getOutgoers(nodeId, get().nodes, get().edges);
const childrenEdges = getChildrenEdges(childrenNodes, get().edges);
const nodeIds = childrenNodes.map((node) => node.id);
const edgeIds = childrenEdges.map((edge) => edge.id);
const nodeIds = childrenNodes.map((node) => node.id);
const edgeIds = childrenEdges.map((edge) => edge.id);
return {
...state,
collapsedNodes: state.collapsedNodes.filter(
(nodeId) => !nodeIds.includes(nodeId)
),
collapsedEdges: state.collapsedEdges.filter(
(edgeId) => !edgeIds.includes(edgeId)
),
};
}),
collapseNodes: (nodeId) =>
set((state) => {
const childrenNodes = getOutgoers(nodeId, state.nodes, state.edges);
const childrenEdges = getChildrenEdges(childrenNodes, state.edges);
set({
collapsedNodes: get().collapsedNodes.filter(
(nodeId) => !nodeIds.includes(nodeId)
),
collapsedEdges: get().collapsedEdges.filter(
(edgeId) => !edgeIds.includes(edgeId)
),
});
},
collapseNodes: (nodeId) => {
const childrenNodes = getOutgoers(nodeId, get().nodes, get().edges);
const childrenEdges = getChildrenEdges(childrenNodes, get().edges);
const nodeIds = childrenNodes.map((node) => node.id);
const edgeIds = childrenEdges.map((edge) => edge.id);
const nodeIds = childrenNodes.map((node) => node.id);
const edgeIds = childrenEdges.map((edge) => edge.id);
return {
...state,
collapsedNodes: state.collapsedNodes.concat(nodeIds),
collapsedEdges: state.collapsedEdges.concat(edgeIds),
};
}),
set({
collapsedNodes: get().collapsedNodes.concat(nodeIds),
collapsedEdges: get().collapsedEdges.concat(edgeIds),
});
},
}));
export default useGraph;

33
src/types.d.ts vendored Normal file
View File

@ -0,0 +1,33 @@
type CanvasDirection = "LEFT" | "RIGHT" | "DOWN" | "UP"
interface NodeData<T = any> {
id: string;
disabled?: boolean;
text?: any;
height?: number;
width?: number;
parent?: string;
ports?: PortData[];
icon?: IconData;
nodePadding?: number | [number, number] | [number, number, number, number];
data?: T;
className?: string;
layoutOptions?: ElkNodeLayoutOptions;
selectionDisabled?: boolean;
}
interface EdgeData<T = any> {
id: string;
disabled?: boolean;
text?: any;
from?: string;
to?: string;
fromPort?: string;
toPort?: string;
data?: T;
className?: string;
containerClassName?: string;
arrowHeadType?: any;
parent?: string;
selectionDisabled?: boolean;
}

View File

@ -1,5 +1,3 @@
import { NodeData, EdgeData } from "reaflow/dist/types";
export const getChildrenEdges = (
nodes: NodeData[],
edges: EdgeData[]

View File

@ -0,0 +1,15 @@
export function getNextLayout(layout: "LEFT" | "RIGHT" | "DOWN" | "UP") {
switch (layout) {
case "RIGHT":
return "DOWN";
case "DOWN":
return "LEFT";
case "LEFT":
return "UP";
default:
return "RIGHT";
}
}

View File

@ -1,5 +1,3 @@
import { NodeData, EdgeData } from "reaflow/dist/types";
export const getOutgoers = (
nodeId: string,
nodes: NodeData[],

View File

@ -1,105 +0,0 @@
/**
* Copyright (C) 2022 Aykut Saraç - All Rights Reserved
*/
import toast from "react-hot-toast";
const filterChild = ([_, v]) => {
const isNull = v === null;
const isArray = Array.isArray(v) && v.length;
const isObject = v instanceof Object;
return !isNull && (isArray || isObject);
};
const filterValues = ([k, v]) => {
if (Array.isArray(v) || v instanceof Object) return false;
return true;
};
function generateChildren(object: Object, nextId: () => string) {
if (!(object instanceof Object)) object = [object];
return Object.entries(object)
.filter(filterChild)
.flatMap(([k, v]) => {
// const isObject = v instanceof Object && !Array.isArray(v);
// if (isObject) {
// return [
// {
// id: nextId(),
// text: k,
// parent: true,
// children: generateChildren(v, nextId),
// },
// ];
// }
return [
{
id: nextId(),
text: k,
parent: true,
children: extract(v, nextId),
},
];
});
}
function generateNodeData(object: Object | number) {
const isObject = object instanceof Object;
if (isObject) {
const entries = Object.entries(object).filter(filterValues);
return Object.fromEntries(entries);
}
return String(object);
}
const extract = (
os: string[] | object[] | null,
nextId = (
(id) => () =>
String(++id)
)(0)
) => {
if (!os) return [];
return [os].flat().map((o) => ({
id: nextId(),
text: generateNodeData(o),
children: generateChildren(o, nextId),
parent: false,
}));
};
const flatten = (xs: { id: string; children: never[] }[]) =>
xs.flatMap(({ children, ...rest }) => [rest, ...flatten(children)]);
const relationships = (xs: { id: string; children: never[] }[]) => {
return xs.flatMap(({ id: from, children = [] }) => [
...children.map(({ id: to }) => ({
id: `e${from}-${to}`,
from,
to,
})),
...relationships(children),
]);
};
export const parser = (input: string | string[]) => {
try {
if (!Array.isArray(input)) input = [input];
const mappedElements = extract(input);
const res = [...flatten(mappedElements), ...relationships(mappedElements)];
return res;
} catch (error) {
console.error(error);
toast.error("An error occured while parsing JSON data!");
return [];
}
};

159
src/utils/jsonParser.ts Normal file
View File

@ -0,0 +1,159 @@
import toast from "react-hot-toast";
const calculateSize = (
text: string | [string, string][],
isParent = false,
isExpanded: boolean
) => {
let value = "";
if (typeof text === "string") value = text;
else value = text.map(([k, v]) => `${k}: ${v}`).join("\n");
const lineCount = value.split("\n");
const lineLengths = lineCount.map((line) => line.length);
const longestLine = lineLengths.sort((a, b) => b - a)[0];
const getWidth = () => {
if (isExpanded) return 35 + longestLine * 8 + (isParent ? 60 : 0);
if (isParent) return 150;
return 200;
};
const getHeight = () => {
if (lineCount.length * 17.8 < 30) return 40;
return (lineCount.length + 1) * 18;
};
return {
width: getWidth(),
height: getHeight(),
};
};
const filterChild = ([_, v]) => {
const isNull = v === null;
const isArray = Array.isArray(v) && v.length;
const isObject = v instanceof Object;
return !isNull && (isArray || isObject);
};
const filterValues = ([k, v]) => {
if (Array.isArray(v) || v instanceof Object) return false;
return true;
};
function generateChildren(
object: Object,
isExpanded = true,
nextId: () => string
) {
if (!(object instanceof Object)) object = [object];
return Object.entries(object)
.filter(filterChild)
.flatMap(([key, v]) => {
const { width, height } = calculateSize(key, true, isExpanded);
const children = extract(v, isExpanded, nextId);
return [
{
id: nextId(),
text: key,
children,
width,
height,
data: {
isParent: true,
hasChild: !!children.length,
},
},
];
});
}
function generateNodeData(object: Object) {
if (object instanceof Object) {
const entries = Object.entries(object).filter(filterValues);
return entries;
}
return String(object);
}
const extract = (
os: string[] | object[] | null,
isExpanded = true,
nextId = (
(id) => () =>
String(++id)
)(0)
) => {
if (!os) return [];
return [os].flat().map((o) => {
const text = generateNodeData(o);
const { width, height } = calculateSize(text, false, isExpanded);
return {
id: nextId(),
text,
width,
height,
children: generateChildren(o, isExpanded, nextId),
data: {
isParent: false,
hasChild: false,
},
};
});
};
const flatten = (xs: { id: string; children: never[] }[]) =>
xs.flatMap(({ children, ...rest }) => [rest, ...flatten(children)]);
const relationships = (xs: { id: string; children: never[] }[]) => {
return xs.flatMap(({ id: from, children = [] }) => [
...children.map(({ id: to }) => ({
id: `e${from}-${to}`,
from,
to,
})),
...relationships(children),
]);
};
export const parser = (jsonStr: string, isExpanded = true) => {
try {
let json = JSON.parse(jsonStr);
if (!Array.isArray(json)) json = [json];
const nodes: NodeData[] = [];
const edges: EdgeData[] = [];
const mappedElements = extract(json, isExpanded);
const res = [...flatten(mappedElements), ...relationships(mappedElements)];
res.forEach((data) => {
if (isNode(data)) {
nodes.push(data);
} else {
edges.push(data);
}
});
return { nodes, edges };
} catch (error) {
console.error(error);
toast.error("An error occured while parsing JSON data!");
return {
nodes: [],
edges: [],
};
}
};
function isNode(element: NodeData | EdgeData) {
if ("text" in element) return true;
return false;
}