- {/* 2-Column Grid Layout matching the reference image */}
-
- {/* LEFT COLUMN: Input */}
-
-
-
-
-
-
-
);
diff --git a/src/pages/DevUtilities/devutilities/CssAnimationGenerator.jsx b/src/pages/DevUtilities/devutilities/CssAnimationGenerator.jsx
index aefbc77..c90e686 100644
--- a/src/pages/DevUtilities/devutilities/CssAnimationGenerator.jsx
+++ b/src/pages/DevUtilities/devutilities/CssAnimationGenerator.jsx
@@ -13,7 +13,160 @@ import {
FaChevronLeft,
FaUndo,
} from "react-icons/fa";
-import CubicBezierEditor from "./CubicBezierEditor";
+import { useRef, useEffect } from 'react';
+
+function CubicBezierEditor({ bezier, onChange, dark }) {
+ const canvasRef = useRef(null);
+ const [dragging, setDragging] = useState(null); // 'p1' or 'p2' or null
+
+ const getCanvasCoords = (e) => {
+ if (!canvasRef.current) return { x: 0, y: 0 };
+ const rect = canvasRef.current.getBoundingClientRect();
+ const clientX = e.clientX ?? (e.touches && e.touches[0].clientX) ?? 0;
+ const clientY = e.clientY ?? (e.touches && e.touches[0].clientY) ?? 0;
+ return {
+ x: clientX - rect.left,
+ y: clientY - rect.top
+ };
+ };
+
+ const handleDown = (e) => {
+ const pos = getCanvasCoords(e);
+ const p1x = bezier[0] * 200;
+ const p1y = 200 - (bezier[1] + 0.5) * 100;
+ const p2x = bezier[2] * 200;
+ const p2y = 200 - (bezier[3] + 0.5) * 100;
+
+ const dist1 = Math.hypot(pos.x - p1x, pos.y - p1y);
+ const dist2 = Math.hypot(pos.x - p2x, pos.y - p2y);
+
+ if (dist1 < 20 && dist1 <= dist2) {
+ setDragging('p1');
+ } else if (dist2 < 20) {
+ setDragging('p2');
+ }
+ };
+
+ const handleMove = (e) => {
+ if (!dragging) return;
+ const pos = getCanvasCoords(e);
+ let newX = pos.x / 200;
+ let newY = (200 - pos.y) / 100 - 0.5;
+
+ newX = Math.max(0, Math.min(1, newX));
+ newY = Math.max(-0.5, Math.min(1.5, newY));
+
+ newX = Math.round(newX * 100) / 100;
+ newY = Math.round(newY * 100) / 100;
+
+ if (dragging === 'p1') {
+ onChange([newX, newY, bezier[2], bezier[3]]);
+ } else if (dragging === 'p2') {
+ onChange([bezier[0], bezier[1], newX, newY]);
+ }
+ };
+
+ const handleUp = () => {
+ setDragging(null);
+ };
+
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+ const ctx = canvas.getContext('2d');
+
+ ctx.clearRect(0, 0, 200, 200);
+
+ ctx.fillStyle = dark ? '#18181b' : '#fafafa';
+ ctx.fillRect(0, 0, 200, 200);
+
+ ctx.strokeStyle = dark ? '#3f3f46' : '#e4e4e7';
+ ctx.lineWidth = 1;
+ ctx.beginPath(); ctx.moveTo(0, 150); ctx.lineTo(200, 150); ctx.stroke();
+ ctx.beginPath(); ctx.moveTo(0, 50); ctx.lineTo(200, 50); ctx.stroke();
+ ctx.beginPath(); ctx.moveTo(0, 50); ctx.lineTo(0, 150); ctx.stroke();
+ ctx.beginPath(); ctx.moveTo(200, 50); ctx.lineTo(200, 150); ctx.stroke();
+
+ const toPx = (x, y) => ({
+ x: x * 200,
+ y: 200 - (y + 0.5) * 100
+ });
+
+ const p0 = toPx(0, 0);
+ const p1 = toPx(bezier[0], bezier[1]);
+ const p2 = toPx(bezier[2], bezier[3]);
+ const p3 = toPx(1, 1);
+
+ ctx.strokeStyle = dark ? '#52525b' : '#a1a1aa';
+ ctx.lineWidth = 2;
+ ctx.setLineDash([4, 4]);
+ ctx.beginPath(); ctx.moveTo(p0.x, p0.y); ctx.lineTo(p1.x, p1.y); ctx.stroke();
+ ctx.beginPath(); ctx.moveTo(p3.x, p3.y); ctx.lineTo(p2.x, p2.y); ctx.stroke();
+ ctx.setLineDash([]);
+
+ ctx.strokeStyle = '#3b82f6';
+ ctx.lineWidth = 3;
+ ctx.beginPath();
+ ctx.moveTo(p0.x, p0.y);
+ ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
+ ctx.stroke();
+
+ ctx.fillStyle = '#ffffff';
+ ctx.strokeStyle = '#ec4899';
+ ctx.lineWidth = 2;
+
+ ctx.beginPath(); ctx.arc(p1.x, p1.y, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
+ ctx.beginPath(); ctx.arc(p2.x, p2.y, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
+
+ ctx.fillStyle = dark ? '#d4d4d8' : '#3f3f46';
+ ctx.beginPath(); ctx.arc(p0.x, p0.y, 3, 0, Math.PI * 2); ctx.fill();
+ ctx.beginPath(); ctx.arc(p3.x, p3.y, 3, 0, Math.PI * 2); ctx.fill();
+
+ }, [bezier, dark]);
+
+ useEffect(() => {
+ if (dragging) {
+ window.addEventListener('mousemove', handleMove);
+ window.addEventListener('mouseup', handleUp);
+ window.addEventListener('touchmove', handleMove, { passive: false });
+ window.addEventListener('touchend', handleUp);
+ return () => {
+ window.removeEventListener('mousemove', handleMove);
+ window.removeEventListener('mouseup', handleUp);
+ window.removeEventListener('touchmove', handleMove);
+ window.removeEventListener('touchend', handleUp);
+ };
+ }
+ }, [dragging, bezier]);
+
+ return (
+
+
+
+
+
cubic-bezier(
+
+ {bezier[0].toFixed(2)}, {bezier[1].toFixed(2)}
+
+
,
+
+ {bezier[2].toFixed(2)}, {bezier[3].toFixed(2)}
+
+
)
+
+
+ );
+}
+
export default function CssAnimationGenerator() {
const { dark } = useTheme();
diff --git a/src/pages/DevUtilities/devutilities/CubicBezierEditor.jsx b/src/pages/DevUtilities/devutilities/CubicBezierEditor.jsx
deleted file mode 100644
index a9fb3cc..0000000
--- a/src/pages/DevUtilities/devutilities/CubicBezierEditor.jsx
+++ /dev/null
@@ -1,170 +0,0 @@
-import React, { useRef, useEffect, useState } from 'react';
-
-export default function CubicBezierEditor({ bezier, onChange, dark }) {
- const canvasRef = useRef(null);
- const [dragging, setDragging] = useState(null); // 'p1' or 'p2' or null
-
- const getCanvasCoords = (e) => {
- if (!canvasRef.current) return { x: 0, y: 0 };
- const rect = canvasRef.current.getBoundingClientRect();
- const clientX = e.clientX ?? (e.touches && e.touches[0].clientX) ?? 0;
- const clientY = e.clientY ?? (e.touches && e.touches[0].clientY) ?? 0;
- return {
- x: clientX - rect.left,
- y: clientY - rect.top
- };
- };
-
- const handleDown = (e) => {
- // e.preventDefault() can be useful, but let's just do it directly on touch start if needed
- const pos = getCanvasCoords(e);
- const p1x = bezier[0] * 200;
- const p1y = 200 - (bezier[1] + 0.5) * 100;
- const p2x = bezier[2] * 200;
- const p2y = 200 - (bezier[3] + 0.5) * 100;
-
- const dist1 = Math.hypot(pos.x - p1x, pos.y - p1y);
- const dist2 = Math.hypot(pos.x - p2x, pos.y - p2y);
-
- if (dist1 < 20 && dist1 <= dist2) {
- setDragging('p1');
- } else if (dist2 < 20) {
- setDragging('p2');
- }
- };
-
- const handleMove = (e) => {
- if (!dragging) return;
- const pos = getCanvasCoords(e);
- let newX = pos.x / 200;
- let newY = (200 - pos.y) / 100 - 0.5;
-
- // clamp X between 0 and 1
- newX = Math.max(0, Math.min(1, newX));
- // clamp Y to the visible bounds
- newY = Math.max(-0.5, Math.min(1.5, newY));
-
- // round to 2 decimals
- newX = Math.round(newX * 100) / 100;
- newY = Math.round(newY * 100) / 100;
-
- if (dragging === 'p1') {
- onChange([newX, newY, bezier[2], bezier[3]]);
- } else if (dragging === 'p2') {
- onChange([bezier[0], bezier[1], newX, newY]);
- }
- };
-
- const handleUp = () => {
- setDragging(null);
- };
-
- useEffect(() => {
- const canvas = canvasRef.current;
- if (!canvas) return;
- const ctx = canvas.getContext('2d');
-
- ctx.clearRect(0, 0, 200, 200);
-
- // Draw grid/background
- ctx.fillStyle = dark ? '#18181b' : '#fafafa';
- ctx.fillRect(0, 0, 200, 200);
-
- // Draw axes
- ctx.strokeStyle = dark ? '#3f3f46' : '#e4e4e7';
- ctx.lineWidth = 1;
- // Y=0 line (which is y=150)
- ctx.beginPath(); ctx.moveTo(0, 150); ctx.lineTo(200, 150); ctx.stroke();
- // Y=1 line (which is y=50)
- ctx.beginPath(); ctx.moveTo(0, 50); ctx.lineTo(200, 50); ctx.stroke();
- // X=0 and X=1 lines
- ctx.beginPath(); ctx.moveTo(0, 50); ctx.lineTo(0, 150); ctx.stroke();
- ctx.beginPath(); ctx.moveTo(200, 50); ctx.lineTo(200, 150); ctx.stroke();
-
- // Coordinates mapping function
- const toPx = (x, y) => ({
- x: x * 200,
- y: 200 - (y + 0.5) * 100
- });
-
- const p0 = toPx(0, 0);
- const p1 = toPx(bezier[0], bezier[1]);
- const p2 = toPx(bezier[2], bezier[3]);
- const p3 = toPx(1, 1);
-
- // Draw lines to control points
- ctx.strokeStyle = dark ? '#52525b' : '#a1a1aa';
- ctx.lineWidth = 2;
- ctx.setLineDash([4, 4]);
- ctx.beginPath(); ctx.moveTo(p0.x, p0.y); ctx.lineTo(p1.x, p1.y); ctx.stroke();
- ctx.beginPath(); ctx.moveTo(p3.x, p3.y); ctx.lineTo(p2.x, p2.y); ctx.stroke();
- ctx.setLineDash([]);
-
- // Draw bezier curve
- ctx.strokeStyle = '#3b82f6'; // blue-500
- ctx.lineWidth = 3;
- ctx.beginPath();
- ctx.moveTo(p0.x, p0.y);
- ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
- ctx.stroke();
-
- // Draw control points handles
- ctx.fillStyle = '#ffffff';
- ctx.strokeStyle = '#ec4899'; // pink-500
- ctx.lineWidth = 2;
-
- // p1 handle
- ctx.beginPath(); ctx.arc(p1.x, p1.y, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
- // p2 handle
- ctx.beginPath(); ctx.arc(p2.x, p2.y, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
-
- // Anchor points (p0, p3)
- ctx.fillStyle = dark ? '#d4d4d8' : '#3f3f46';
- ctx.beginPath(); ctx.arc(p0.x, p0.y, 3, 0, Math.PI * 2); ctx.fill();
- ctx.beginPath(); ctx.arc(p3.x, p3.y, 3, 0, Math.PI * 2); ctx.fill();
-
- }, [bezier, dark]);
-
- // Handle global mouse/touch events
- useEffect(() => {
- if (dragging) {
- window.addEventListener('mousemove', handleMove);
- window.addEventListener('mouseup', handleUp);
- window.addEventListener('touchmove', handleMove, { passive: false });
- window.addEventListener('touchend', handleUp);
- return () => {
- window.removeEventListener('mousemove', handleMove);
- window.removeEventListener('mouseup', handleUp);
- window.removeEventListener('touchmove', handleMove);
- window.removeEventListener('touchend', handleUp);
- };
- }
- }, [dragging, bezier]); // depend on bezier to capture latest state in handleMove closure
-
- return (
-
-
-
-
-
cubic-bezier(
-
- {bezier[0].toFixed(2)}, {bezier[1].toFixed(2)}
-
-
,
-
- {bezier[2].toFixed(2)}, {bezier[3].toFixed(2)}
-
-
)
-
-
- );
-}
diff --git a/src/pages/DevUtilities/devutilities/JsonFormatter.jsx b/src/pages/DevUtilities/devutilities/JsonFormatter.jsx
deleted file mode 100644
index 341a464..0000000
--- a/src/pages/DevUtilities/devutilities/JsonFormatter.jsx
+++ /dev/null
@@ -1,227 +0,0 @@
-import { useState } from "react";
-import { Link } from "react-router-dom";
-import { toast } from "sonner";
-import { useTheme } from "../../../context/ThemeContext";
-
-const JsonFormatter = () => {
- const { dark } = useTheme();
- const [input, setInput] = useState("");
- const [output, setOutput] = useState("");
-
- const handleFormat = () => {
- try {
- if (!input.trim()) return;
- const parsed = JSON.parse(input);
- setOutput(JSON.stringify(parsed, null, 2));
- } catch (error) {
- toast.error("Invalid JSON format");
- }
- };
-
- const handleMinify = () => {
- try {
- if (!input.trim()) return;
- const parsed = JSON.parse(input);
- setOutput(JSON.stringify(parsed));
- } catch (error) {
- toast.error("Invalid JSON format");
- }
- };
-
- const handleClear = () => {
- setInput("");
- setOutput("");
- };
-const handleSample = () => {
- const sampleJson = {
- name: "John Doe",
- role: "Frontend Developer",
- skills: ["React", "Next.js", "TypeScript"],
- experience: 3,
- active: true,
- };
-
- setInput(JSON.stringify(sampleJson));
- setOutput("");
-};
- const handleCopy = async () => {
- try {
- if (!output) return;
- await navigator.clipboard.writeText(output);
- toast.success("Copied to clipboard");
- } catch (error) {
- toast.error("Failed to copy");
- }
- };
-
- const buttons = [
- { label: "Format", onClick: handleFormat },
- { label: "Minify", onClick: handleMinify },
- { label: "Clear", onClick: handleClear },
- ];
- return (
-
-
JSON Formatter β DevTasks
-
-
-
-
-
-
-
-
- {/* Header */}
-
-
-
-
-
- JSON Formatter
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default JsonFormatter;
diff --git a/src/pages/DevUtilities/devutilities/JsonYamlCsvXmlConverter.jsx b/src/pages/DevUtilities/devutilities/JsonYamlCsvXmlConverter.jsx
index cb8d3be..73ca816 100644
--- a/src/pages/DevUtilities/devutilities/JsonYamlCsvXmlConverter.jsx
+++ b/src/pages/DevUtilities/devutilities/JsonYamlCsvXmlConverter.jsx
@@ -59,6 +59,10 @@ const formatYamlParseError = (error) => {
return message;
};
+const formatXmlParseError = (msg) => {
+ return msg || "Invalid XML syntax.";
+};
+
const formatErrorFor = (format, error, source) => {
if (format === "json") return { title: "Invalid JSON", message: formatJsonParseError(error, source) };
if (format === "yaml") return { title: "Invalid YAML", message: formatYamlParseError(error) };
@@ -121,11 +125,12 @@ function elementToValue(rootEl) {
groups.get(tag).push(child);
}
- for (const [tag, children] of groups) {
- result[tag] =
- children.length === 1
- ? valueMap.get(children[0])
- : children.map(c => valueMap.get(c));
+ for (const [tag, elements] of groups.entries()) {
+ if (elements.length === 1) {
+ result[tag] = valueMap.get(elements[0]);
+ } else {
+ result[tag] = elements.map(child => valueMap.get(child));
+ }
}
valueMap.set(el, result);
@@ -134,471 +139,267 @@ function elementToValue(rootEl) {
return valueMap.get(rootEl);
}
-function xmlToJson(xmlString) {
+function xmlToObj(xmlString) {
const doc = parseXmlDoc(xmlString);
const root = doc.documentElement;
return { [root.tagName]: elementToValue(root) };
}
-function buildElement(doc, rootTagName, rootValue) {
- const rootEl = doc.createElement(rootTagName);
- if (rootValue === null || rootValue === undefined) return rootEl;
+function objToXmlString(obj) {
+ const allNodes = [];
+ const explicitStack = [{ parentNode: null, key: null, val: obj }];
- const queue = [{ el: rootEl, value: rootValue }];
- let head = 0;
+ let rootNode = null;
- while (head < queue.length) {
- const { el, value } = queue[head++];
-
- if (value === null || value === undefined || typeof value !== "object") {
- el.textContent = String(value ?? "");
- continue;
- }
+ while (explicitStack.length > 0) {
+ const item = explicitStack.pop();
+ const { parentNode, key, val } = item;
+
+ if (val && typeof val === "object" && !Array.isArray(val)) {
+ let nodeName = key;
+ let actualVal = val;
+
+ if (!parentNode) {
+ const rootKeys = Object.keys(val).filter(k => !k.startsWith("@"));
+ if (rootKeys.length === 0) throw new Error("Object must have a root element tag.");
+ nodeName = rootKeys[0];
+ actualVal = val[nodeName];
+ }
- if (Array.isArray(value)) {
- el.textContent = JSON.stringify(value);
- continue;
- }
+ const el = { tag: nodeName, attrs: [], children: [], text: "" };
+ if (!parentNode) {
+ rootNode = el;
+ } else {
+ parentNode.children.push(el);
+ }
- for (const [key, val] of Object.entries(value)) {
- if (key.startsWith("@")) {
- el.setAttribute(key.slice(1), String(val));
- } else if (key === "#text") {
- el.appendChild(doc.createTextNode(String(val)));
- } else if (Array.isArray(val)) {
- for (const item of val) {
- const childEl = doc.createElement(key);
- el.appendChild(childEl);
- queue.push({ el: childEl, value: item });
+ allNodes.push({ el, val: actualVal });
+
+ const childKeys = Object.keys(actualVal || {});
+ for (let i = childKeys.length - 1; i >= 0; i--) {
+ const ck = childKeys[i];
+ const cv = actualVal[ck];
+
+ if (ck.startsWith("@")) {
+ el.attrs.push({ name: ck.slice(1), value: String(cv) });
+ } else if (ck === "#text") {
+ el.text = String(cv);
+ } else if (Array.isArray(cv)) {
+ for (let j = cv.length - 1; j >= 0; j--) {
+ explicitStack.push({ parentNode: el, key: ck, val: cv[j] });
+ }
+ } else {
+ explicitStack.push({ parentNode: el, key: ck, val: cv });
}
- } else {
- const childEl = doc.createElement(key);
- el.appendChild(childEl);
- queue.push({ el: childEl, value: val });
+ }
+ } else {
+ const el = { tag: key || "item", attrs: [], children: [], text: String(val ?? "") };
+ if (parentNode) {
+ parentNode.children.push(el);
}
}
}
- return rootEl;
-}
-
-function jsonToXml(jsonString) {
- const parsed = JSON.parse(jsonString);
-
- const rootKeys = Object.keys(parsed);
- if (rootKeys.length === 0) {
- throw new Error("JSON object must have at least one key to become an XML root element.");
- }
- if (rootKeys.length > 1) {
- throw new Error(
- `XML requires exactly one root element. Found ${rootKeys.length} root keys: ${rootKeys.join(", ")}.`
- );
- }
-
- const rootTag = rootKeys[0];
- const domDoc = document.implementation.createDocument("", "", null);
- domDoc.appendChild(buildElement(domDoc, rootTag, parsed[rootTag]));
-
- return new XMLSerializer().serializeToString(domDoc).replace(/ xmlns=""/g, "");
-}
-
-function prettyPrintXml(xmlString) {
- const doc = parseXmlDoc(xmlString);
-
- function serializeNode(node, depth) {
- const pad = " ".repeat(depth);
-
- if (node.nodeType === Node.TEXT_NODE) return null;
- if (node.nodeType !== Node.ELEMENT_NODE) return null;
-
- const attrs = Array.from(node.attributes)
- .map((a) => ` ${a.name}="${a.value}"`)
- .join("");
-
- const hasElementChildren = Array.from(node.childNodes).some(
- (c) => c.nodeType === Node.ELEMENT_NODE
- );
-
- if (!hasElementChildren && !node.textContent.trim()) {
- return `${pad}<${node.tagName}${attrs} />`;
+ function serializeNode(node) {
+ const attrStr = node.attrs.map(a => ` ${a.name}="${a.value.replace(/"/g, """)}"`).join("");
+ if (node.children.length === 0 && !node.text) {
+ return `<${node.tag}${attrStr}/>`;
}
-
- if (!hasElementChildren) {
- return `${pad}<${node.tagName}${attrs}>${node.textContent.trim()}${node.tagName}>`;
- }
-
- const children = Array.from(node.childNodes)
- .map((c) => serializeNode(c, depth + 1))
- .filter(Boolean);
-
- return [
- `${pad}<${node.tagName}${attrs}>`,
- ...children,
- `${pad}${node.tagName}>`,
- ].join("\n");
- }
-
- return `\n${serializeNode(doc.documentElement, 0)}`;
-}
-
-function formatXmlParseError(rawMessage) {
- const firstLine = (rawMessage ?? "")
- .split("\n")
- .map((l) => l.trim())
- .find(Boolean);
-
- if (!firstLine) return "Invalid XML syntax.";
-
- const match = firstLine.match(/line\s+(\d+)\s+at\s+column\s+(\d+)[:\s]+(.+)/i);
- if (match) {
- return `${match[3].trim()} (line ${match[1]}, column ${match[2]})`;
+ const childrenStr = node.children.map(serializeNode).join("");
+ const textStr = node.text ? node.text.replace(/&/g, "&").replace(//g, ">") : "";
+ return `<${node.tag}${attrStr}>${childrenStr}${textStr}${node.tag}>`;
}
- return firstLine;
+ if (!rootNode) return "";
+ return `\n${serializeNode(rootNode)}`;
}
-// βββ CSV Parser βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-//
-// Uses a single-pass character tokenizer instead of line.split() so that
-// quoted fields containing newlines, commas, or the chosen delimiter are
-// handled correctly without any third-party library.
-
-const tokenizeCsv = (text, delimiter) => {
- text = text.replace(/\r\n/g, "\n");
- const rows = [];
- let row = [];
- let field = "";
- let inQuotes = false;
-
- for (let i = 0; i < text.length; i++) {
- const ch = text[i];
-
- if (inQuotes) {
- if (ch === '"' && text[i + 1] === '"') {
- field += '"';
- i++;
- } else if (ch === '"') {
- inQuotes = false;
+// βββ CSV Parser & Serializer Helpers ββββββββββββββββββββββββββββββββββββββββββ
+
+function parseCsv(csvText, delimiter = ",", hasHeaders = true) {
+ const lines = [];
+ let currentRow = [];
+ let currentField = "";
+ let insideQuotes = false;
+
+ for (let i = 0; i < csvText.length; i++) {
+ const char = csvText[i];
+ const nextChar = csvText[i + 1];
+
+ if (insideQuotes) {
+ if (char === '"') {
+ if (nextChar === '"') {
+ currentField += '"';
+ i++;
+ } else {
+ insideQuotes = false;
+ }
} else {
- field += ch; // KEEP newlines safely inside quotes
+ currentField += char;
+ }
+ } else {
+ if (char === '"') {
+ insideQuotes = true;
+ } else if (char === delimiter) {
+ currentRow.push(currentField.trim());
+ currentField = "";
+ } else if (char === "\r" || char === "\n") {
+ currentRow.push(currentField.trim());
+ if (currentRow.some(field => field !== "")) {
+ lines.push(currentRow);
+ }
+ currentRow = [];
+ currentField = "";
+ if (char === "\r" && nextChar === "\n") {
+ i++;
+ }
+ } else {
+ currentField += char;
}
- continue;
- }
-
- if (ch === '"') {
- inQuotes = true;
- continue;
- }
-
- if (text.slice(i, i + delimiter.length) === delimiter) {
- row.push(field);
- field = "";
- i += delimiter.length - 1;
- continue;
- }
-
- if (ch === "\n") {
- row.push(field);
- rows.push(row);
- row = [];
- field = "";
- continue;
}
-
- if (ch === "\r") continue;
-
- field += ch;
- }
-
- row.push(field);
-rows.push(row);
- return rows;
-};
-
-/**
- * Parse CSV text into an array of objects.
- *
- * - hasHeader=true β first row becomes object keys
- * - hasHeader=false β keys are auto-generated: field1, field2, β¦
- * - Returns [] for empty or header-only input (no error)
- */
-const parseCsv = (csv, delimiter = ",", hasHeader = true) => {
- const rows = tokenizeCsv(csv, delimiter);
-
- if (rows.length === 0) return [];
-
- const colCount = Math.max(...rows.map(r => r.length));
-
- const normalizeRow = (row) => {
- const out = new Array(colCount).fill("");
- row.forEach((v, i) => {
- out[i] = v;
- });
- return out;
- };
-
- const normalized = rows.map(normalizeRow);
-
-const inferType = (v) => {
- const val = v.trim();
-
- if (val === "") return "";
-
- if (val === "true") return true;
- if (val === "false") return false;
-
- // number detection
- if (/^-?\d+(\.\d+)?$/.test(val)) return Number(val);
-
- // array detection (pipe-separated)
- if (val.includes("|")) {
- return val.split("|").map(x => inferType(x));
}
- // JSON detection (safe parse)
- if (
- (val.startsWith("{") && val.endsWith("}")) ||
- (val.startsWith("[") && val.endsWith("]"))
- ) {
- try {
- return JSON.parse(val);
- } catch {
- return val;
+ if (currentField || currentRow.length > 0) {
+ currentRow.push(currentField.trim());
+ if (currentRow.some(field => field !== "")) {
+ lines.push(currentRow);
}
}
- return val;
-};
- if (hasHeader) {
- const headers = normalized[0].map((h, i) => {
- const clean = h.trim().replace(/^"|"$/g, "");
- return clean || `col_${i}`;
-});
- if (normalized.length === 1) return [];
+ if (lines.length === 0) return [];
- return normalized.slice(1).map(row => {
+ if (!hasHeaders) {
+ return lines.map((row) => {
const obj = {};
- headers.forEach((h, i) => {
- obj[h] = inferType(row[i]);
- });
+ row.forEach((val, idx) => { obj[`column${idx + 1}`] = val; });
return obj;
});
}
- return normalized.map(row => {
+ const headers = lines[0].map(h => h || "unnamed");
+ const dataRows = lines.slice(1);
+
+ return dataRows.map((row) => {
const obj = {};
- row.forEach((v, i) => {
- obj[`field${i + 1}`] = inferType(v);
+ headers.forEach((header, idx) => {
+ obj[header] = row[idx] !== undefined ? row[idx] : "";
});
return obj;
});
-};
-
-// βββ CSV Serializer βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
-/**
- * Quote a single CSV field only when necessary (RFC 4180).
- */
-const escapeCsvField = (value, delimiter) => {
- const str = value === null || value === undefined ? "" : String(value);
+}
- const mustQuote =
- str.includes(delimiter) ||
- str.includes('"') ||
- str.includes("\n") ||
- str.includes("\r") ||
- str.startsWith(" ") ||
- str.endsWith(" ");
+function serializeCsv(data, delimiter = ",", includeHeaders = true) {
+ if (!Array.isArray(data) || data.length === 0) return "";
+ const flatData = data.map(item => {
+ if (item && typeof item === "object") return item;
+ return { value: item };
+ });
- if (!mustQuote) return str;
+ const headers = Array.from(new Set(flatData.flatMap(Object.keys)));
+ const escapeCsvValue = (val) => {
+ const str = String(val ?? "");
+ if (str.includes(delimiter) || str.includes('"') || str.includes("\n") || str.includes("\r")) {
+ return `"${str.replace(/"/g, '""')}"`;
+ }
+ return str;
+ };
- return `"${str.replace(/"/g, '""')}"`;
-};
-const safeStringify = (val) => {
- try {
- return JSON.stringify(val);
- } catch {
- return String(val);
+ const rows = [];
+ if (includeHeaders) {
+ rows.push(headers.map(escapeCsvValue).join(delimiter));
}
-};
-/**
- * Recursively flatten a nested object into dot-notation keys.
- * Arrays are serialized as pipe-separated strings (a|b|c).
- * Scalars (string/number/boolean/null) at the top level get key "value".
- */
-const flattenObject = (obj, prefix = "") => {
- const result = {};
- const safeValue = (v) =>
- v === null || v === undefined
- ? ""
- : typeof v === "object"
- ? safeStringify(v)
- : String(v);
+ flatData.forEach((item) => {
+ const row = headers.map(header => escapeCsvValue(item[header]));
+ rows.push(row.join(delimiter));
+ });
- const safeArray = (arr) => arr.map(safeValue).join("|");
+ return rows.join("\n");
+}
- const walk = (current, currentKey) => {
- if (current === null || current === undefined) {
- result[currentKey] = "";
- return;
- }
+// βββ Format XML Pretty-Printer βββββββββββββββββββββββββββββββββββββββββββββββ
- if (Array.isArray(current)) {
- result[currentKey] = current.map(safeValue).join("|");
- return;
- }
+function prettyPrintXml(xmlString) {
+ try {
+ const doc = parseXmlDoc(xmlString);
+ let indent = "";
+ const lines = [];
- if (typeof current !== "object") {
- result[currentKey] = current;
- return;
- }
+ const allNodes = [];
+ const explicitStack = [{ parent: null, node: doc.documentElement, depth: 0 }];
- for (const key of Object.keys(current)) {
- const val = current[key];
- const newKey = currentKey ? `${currentKey}.${key}` : key;
+ while (explicitStack.length > 0) {
+ const { parent, node, depth } = explicitStack.pop();
+ allNodes.push({ parent, node, depth });
- if (val === null || val === undefined) {
- result[newKey] = "";
- } else if (Array.isArray(val)) {
- result[newKey] = safeArray(val);
-}else if (typeof val === "object") {
- walk(val, newKey);
- } else {
- result[newKey] = val;
+ for (let i = node.children.length - 1; i >= 0; i--) {
+ explicitStack.push({ parent: node, node: node.children[i], depth: depth + 1 });
}
}
- };
- if (obj === null || obj === undefined) {
- return prefix ? { [prefix]: "" } : {};
- }
+ const renderedMap = new Map();
- walk(obj, prefix);
+ for (let i = allNodes.length - 1; i >= 0; i--) {
+ const { node, depth } = allNodes[i];
+ const pad = " ".repeat(depth);
- return result;
-};
-/**
- * Serialize a JS value to CSV text.
- *
- * Accepts:
- * - Array of objects (standard case)
- * - Single object (wrapped in array automatically)
- * - Array of scalars (produces a single "value" column)
- *
- * includeHeader mirrors the hasHeader setting so CSVβCSV round-trips cleanly.
- */
-const serializeCsv = (data, delimiter = ",", includeHeader = true) => {
- const arr = Array.isArray(data)
- ? data
- : data !== null && data !== undefined
- ? [data]
- : [];
-
- if (!arr.length) {
- throw new Error("No data available for CSV conversion.");
- }
-
- const flatRows = arr.map(row => {
- if (row === null || row === undefined) return { value: "" };
- if (typeof row !== "object") return { value: String(row) };
- return flattenObject(row);
- });
-
- const keySet = new Set();
+ const attrStr = Array.from(node.attributes)
+ .map(a => ` ${a.name}="${a.value.replace(/"/g, """)}"`)
+ .join("");
-flatRows.forEach(row => {
- Object.keys(row).forEach(k => keySet.add(k));
-});
-
-const keys = Array.from(keySet);
- if (!keys.length) {
- throw new Error("Cannot convert empty structure to CSV.");
- }
-
- const lines = flatRows.map(row =>
- keys.map(k => escapeCsvField(row[k], delimiter)).join(delimiter)
- );
-
- return includeHeader
- ? [keys.join(delimiter), ...lines].join("\n")
- : lines.join("\n");
-};
+ if (node.children.length === 0) {
+ const text = node.textContent.trim();
+ if (text) {
+ renderedMap.set(node, `${pad}<${node.tagName}${attrStr}>${text}${node.tagName}>`);
+ } else {
+ renderedMap.set(node, `${pad}<${node.tagName}${attrStr}/>`);
+ }
+ continue;
+ }
-// βββ Core Conversion Engine βββββββββββββββββββββββββββββββββββββββββββββββββββ
+ const childrenStr = Array.from(node.children)
+ .map(c => renderedMap.get(c))
+ .join("\n");
-/**
- * Parse source text into an intermediate JS value.
- * Throws with a format-specific error on failure.
- */
-const parseSource = (text, format, delimiter, hasHeader) => {
- switch (format) {
- case "json": return JSON.parse(text);
- case "yaml": {
- const res = load(text);
- return res ?? {};
+ renderedMap.set(node, `${pad}<${node.tagName}${attrStr}>\n${childrenStr}\n${pad}${node.tagName}>`);
}
- case "csv": return parseCsv(text, delimiter, hasHeader);
- case "xml": return xmlToJson(text);
- default: throw new Error(`Unknown source format: ${format}`);
- }
-};
-/**
- * Serialize an intermediate JS value to the target format text.
- * Throws if the value is incompatible with the target (e.g. empty array β CSV).
- */
-const serializeTarget = (value, format, delimiter, hasHeader) => {
- switch (format) {
- case "json": return JSON.stringify(value ?? null, null, 2);
- case "yaml": return dump(value ?? null, YAML_OPTIONS);
- case "csv": return serializeCsv(value, delimiter, hasHeader);
- case "xml": return prettyPrintXml(jsonToXml(JSON.stringify(value)));
- default: throw new Error(`Unknown target format: ${format}`);
+ return `\n${renderedMap.get(doc.documentElement)}`;
+ } catch {
+ return xmlString;
}
-};
+}
-/**
- * Full conversion pipeline. Never throws β returns { result, error }.
- *
- * Error classification:
- * - Parse error β blame source format
- * - Serialize error β blame the conversion (incompatible data shape)
- */
-const runConvert = (sourceText, srcFmt, tgtFmt, delimiter, hasHeader) => {
- if (!sourceText.trim()) return { result: "", error: null };
-
- let parsed;
- try {
- parsed = parseSource(sourceText, srcFmt, delimiter, hasHeader);
- } catch (err) {
- return { result: "", error: formatErrorFor(srcFmt, err, sourceText) };
- }
+// βββ Main Conversions Engine βββββββββββββββββββββββββββββββββββββββββββββββββ
- // Same format β beautify/normalize only
- if (srcFmt === tgtFmt) {
- try {
- // For CSV same-format, re-serialize with the correct includeHeader
- return { result: serializeTarget(parsed, tgtFmt, delimiter, hasHeader), error: null };
- } catch (err) {
- return { result: "", error: formatErrorFor(srcFmt, err, sourceText) };
- }
+function parseSource(text, format, delimiter, hasHeaders) {
+ if (format === "json") return JSON.parse(text);
+ if (format === "yaml") return load(text);
+ if (format === "csv") return parseCsv(text, delimiter, hasHeaders);
+ if (format === "xml") return xmlToObj(text);
+ throw new Error(`Unsupported source format: ${format}`);
+}
+
+function serializeTarget(obj, format, delimiter, includeHeaders) {
+ if (format === "json") return JSON.stringify(obj, null, 2);
+ if (format === "yaml") return dump(obj, YAML_OPTIONS);
+ if (format === "csv") {
+ const array = Array.isArray(obj) ? obj : [obj];
+ return serializeCsv(array, delimiter, includeHeaders);
}
+ if (format === "xml") return objToXmlString(obj);
+ throw new Error(`Unsupported target format: ${format}`);
+}
+function runConvert(text, srcFmt, tgtFmt, delimiter, hasHeader) {
try {
+ const parsed = parseSource(text, srcFmt, delimiter, hasHeader);
const result = serializeTarget(parsed, tgtFmt, delimiter, hasHeader);
return { result, error: null };
} catch (err) {
- // Serialization failure β data shape incompatible with target
- return {
- result: "",
- error: {
- title: `Cannot convert to ${tgtFmt.toUpperCase()}`,
- message: err instanceof Error ? err.message : `Failed to serialize as ${tgtFmt}.`,
- },
- };
+ return { result: "", error: formatErrorFor(srcFmt, err, text) };
}
-};
+}
// βββ Sample Data Generator ββββββββββββββββββββββββββββββββββββββββββββββββββββ
@@ -607,7 +408,7 @@ const getSample = (format, delimiter) => {
case "json": return JSON.stringify(SAMPLE_OBJECT, null, 2);
case "yaml": return dump(SAMPLE_OBJECT, YAML_OPTIONS);
case "csv": return serializeCsv(SAMPLE_CSV_ROWS, delimiter, true);
- case "xml": return prettyPrintXml(jsonToXml(JSON.stringify({ devtasks: SAMPLE_OBJECT })));
+ case "xml": return prettyPrintXml(objToXmlString({ devtasks: SAMPLE_OBJECT }));
default: return "";
}
};
@@ -624,7 +425,7 @@ const buildTheme = (dark) => ({
? "bg-zinc-950 border-red-500/70 text-white focus:border-red-400 focus:ring-1 focus:ring-red-400"
: "bg-neutral-50 border-red-400 text-black focus:border-red-500 focus:ring-1 focus:ring-red-500",
textareaReadonly: dark
- ? "bg-zinc-900 border-zinc-800 text-zinc-300 cursor-default"
+ ? "bg-zinc-905 border-zinc-800 text-zinc-300 cursor-default"
: "bg-white border-neutral-200 text-zinc-700 cursor-default",
softBtn: dark
? "bg-zinc-800 border-zinc-700 text-zinc-300 hover:text-white hover:border-zinc-500"
@@ -647,9 +448,6 @@ const buildTheme = (dark) => ({
? "bg-zinc-800 border-zinc-700 text-zinc-100 focus:border-white"
: "bg-white border-neutral-300 text-black focus:border-black",
checkLabel: dark ? "text-zinc-300" : "text-zinc-600",
- swapBtn: dark
- ? "bg-zinc-800 border-zinc-700 text-zinc-300 hover:text-white hover:border-zinc-500 hover:bg-zinc-700"
- : "bg-white border-neutral-200 text-zinc-500 hover:text-black hover:border-neutral-400",
});
// βββ FormatPills βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@@ -677,6 +475,14 @@ const JsonYamlCsvXmlConverter = () => {
const { dark } = useTheme();
const t = buildTheme(dark);
+ // Active main tab: 'formatter' or 'converter'
+ const [activeTab, setActiveTab] = useState("formatter");
+
+ // ββ Formatter State ββ
+ const [formatInput, setFormatInput] = useState("");
+ const [formatOutput, setFormatOutput] = useState("");
+
+ // ββ Converter State ββ
const [srcFmt, setSrcFmt] = useState("json");
const [tgtFmt, setTgtFmt] = useState("yaml");
const [srcText, setSrcText] = useState("");
@@ -685,13 +491,62 @@ const JsonYamlCsvXmlConverter = () => {
const [hasHeader, setHasHeader] = useState(true);
const [error, setError] = useState(null); // { title, message } | null
+ // ββ Formatter Handlers ββ
+ const handleFormatterFormat = () => {
+ try {
+ if (!formatInput.trim()) return;
+ const parsed = JSON.parse(formatInput);
+ setFormatOutput(JSON.stringify(parsed, null, 2));
+ toast.success("JSON Formatted!");
+ } catch (error) {
+ toast.error("Invalid JSON format");
+ }
+ };
+
+ const handleFormatterMinify = () => {
+ try {
+ if (!formatInput.trim()) return;
+ const parsed = JSON.parse(formatInput);
+ setFormatOutput(JSON.stringify(parsed));
+ toast.success("JSON Minified!");
+ } catch (error) {
+ toast.error("Invalid JSON format");
+ }
+ };
+
+ const handleFormatterClear = () => {
+ setFormatInput("");
+ setFormatOutput("");
+ };
+
+ const handleFormatterSample = () => {
+ const sampleJson = {
+ name: "John Doe",
+ role: "Frontend Developer",
+ skills: ["React", "Next.js", "TypeScript"],
+ experience: 3,
+ active: true,
+ };
+ setFormatInput(JSON.stringify(sampleJson, null, 2));
+ setFormatOutput("");
+ };
+
+ const handleFormatterCopy = async () => {
+ if (!formatOutput) return;
+ try {
+ await navigator.clipboard.writeText(formatOutput);
+ toast.success("Copied to clipboard");
+ } catch (error) {
+ toast.error("Failed to copy");
+ }
+ };
+
// ββ Shared conversion runner βββββββββββββββββββββββββββββββββββββββββββββ
const applyConversion = (text, sf, tf, delim, hdr) => {
if (!text.trim()) {
setTgtText("");
setError(null);
- toast.info("Input is empty");
return;
}
const { result, error: err } = runConvert(text, sf, tf, delim, hdr);
@@ -707,9 +562,7 @@ const JsonYamlCsvXmlConverter = () => {
applyConversion(value, srcFmt, tgtFmt, delimiter, hasHeader);
};
- // Source format pill clicked:
- // Clear both panels β existing text belongs to the old format,
- // re-parsing it as a different format would always show a misleading error.
+ // Source format pill clicked
const handleSrcFmtChange = (fmt) => {
if (fmt === srcFmt) return;
setSrcFmt(fmt);
@@ -718,19 +571,18 @@ const JsonYamlCsvXmlConverter = () => {
setError(null);
};
- // Target format pill clicked:
- // Keep the source text; just re-convert it to the new target format.
+ // Target format pill clicked
const handleTgtFmtChange = (fmt) => {
if (fmt === tgtFmt) return;
setTgtFmt(fmt);
applyConversion(srcText, srcFmt, fmt, delimiter, hasHeader);
};
- // Swap: the current output becomes the new input, formats are exchanged.
+ // Swap
const handleSwap = () => {
const prevSrcFmt = srcFmt;
const prevTgtFmt = tgtFmt;
- const prevTgtText = tgtText; // use output as new source
+ const prevTgtText = tgtText;
setSrcFmt(prevTgtFmt);
setTgtFmt(prevSrcFmt);
@@ -744,13 +596,13 @@ const JsonYamlCsvXmlConverter = () => {
applyConversion(prevTgtText, prevTgtFmt, prevSrcFmt, delimiter, hasHeader);
};
- // CSV option: delimiter changed β re-convert current source
+ // CSV option: delimiter changed
const handleDelimiterChange = (delim) => {
setDelimiter(delim);
applyConversion(srcText, srcFmt, tgtFmt, delim, hasHeader);
};
- // CSV option: hasHeader toggled β re-convert current source
+ // CSV option: hasHeader toggled
const handleHeaderChange = (val) => {
setHasHeader(val);
applyConversion(srcText, srcFmt, tgtFmt, delimiter, val);
@@ -810,22 +662,18 @@ const JsonYamlCsvXmlConverter = () => {
const showCsvOptions = srcFmt === "csv" || tgtFmt === "csv";
- // ββ Render βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
return (
-
JSON YAML CSV XML Converter β DevTasks
-
+
JSON & Data Format Suite | DevTasks
+
-
{/* Accent bar */}
{/* Header */}
-
- {/* Title + action buttons */}
+ {/* Title + Tab Selectors */}
{
- JSON Β· YAML Β· CSV Β· XML Converter
+ JSON & Data Format Suite
- Live conversion Β· works offline Β· no data leaves your browser
+ JSON formatting/minifying and multi-format conversion suite.
-
-
-
-
- {/* CSV options row β only shown when either panel is CSV */}
- {showCsvOptions && (
-
-
- CSV Options
-
-
-
-
-
-
-
- {srcFmt.toUpperCase()} β {tgtFmt.toUpperCase()}
-
-
- )}
- {/* Editor panels */}
-
-
+ {/* ββ Tab 1: Formatter & Minifier ββ */}
+ {activeTab === "formatter" && (
+
+
+ {/* Input Column */}
+
+
+
+ JSON Input
+
+
+ Sample JSON
+
+
+
- {/* ββ Source panel ββ */}
-
-
-
-
Source
-
+ {/* Output Column */}
+
+
+
+ Formatted Output
+
+
+ Copy
+
-
handleCopy(srcText, `${srcFmt.toUpperCase()} source`)}
- className={`rounded-xl border px-3 py-1.5 text-xs font-black uppercase tracking-widest transition-all duration-200 active:scale-95 ${t.softBtn}`}
- >
- Copy
-
+
-