Browse Source

improve json parser

AykutSarac 2 years ago
parent
commit
a94d951c35

+ 6 - 4
src/components/CustomNode/TextNode.tsx

@@ -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>

+ 2 - 3
src/components/CustomNode/index.tsx

@@ -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}
           />

+ 3 - 3
src/components/CustomNode/styles.tsx

@@ -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`

+ 32 - 26
src/components/Graph/index.tsx

@@ -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), []);
 

+ 2 - 3
src/components/Sidebar/index.tsx

@@ -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;

+ 0 - 87
src/containers/Editor/LiveEditor/helpers.ts

@@ -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;
-}

+ 7 - 3
src/containers/Modals/NodeModal/index.tsx

@@ -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;
             },

+ 4 - 8
src/hooks/store/useConfig.tsx

@@ -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;

+ 25 - 30
src/hooks/store/useGraph.tsx

@@ -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 - 0
src/types.d.ts

@@ -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;
+}

+ 0 - 2
src/utils/getChildrenEdges.ts

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

+ 15 - 0
src/utils/getNextLayout.ts

@@ -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";
+  }
+}

+ 0 - 2
src/utils/getOutgoers.ts

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

+ 0 - 105
src/utils/json-editor-parser.ts

@@ -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 - 0
src/utils/jsonParser.ts

@@ -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;
+}