diff --git a/package.json b/package.json index bab54cd..c78ecfa 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-tabs": "^1.1.12", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "highlight.js": "^11.11.1", "html-to-image": "^1.11.13", "lucide-react": "^0.539.0", "next": "15.4.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ae21e0d..c8c0957 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + highlight.js: + specifier: ^11.11.1 + version: 11.11.1 html-to-image: specifier: ^1.11.13 version: 1.11.13 @@ -1544,6 +1547,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} + engines: {node: '>=12.0.0'} + html-to-image@1.11.13: resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==} @@ -3876,6 +3883,8 @@ snapshots: dependencies: function-bind: 1.1.2 + highlight.js@11.11.1: {} + html-to-image@1.11.13: {} ignore@5.3.2: {} diff --git a/src/app/json-formatter/page.tsx b/src/app/json-formatter/page.tsx new file mode 100644 index 0000000..b9691fa --- /dev/null +++ b/src/app/json-formatter/page.tsx @@ -0,0 +1,232 @@ +"use client"; + +import { useState, useCallback, useEffect } from "react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Textarea } from "@/components/ui/textarea"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import hljs from "highlight.js/lib/core"; +import json from "highlight.js/lib/languages/json"; +import "highlight.js/styles/github-dark.css"; + +// 注册 JSON 语言 +hljs.registerLanguage("json", json); +import { cn } from "@/lib/utils"; +import { Clipboard, Download, AlertTriangle } from "lucide-react"; + +interface FormatOptions { + indent: number; + sortKeys: boolean; + colorize: boolean; +} + +export default function JSONFormatter() { + const [input, setInput] = useState(""); + const [output, setOutput] = useState(""); + const [error, setError] = useState(null); + const [options, setOptions] = useState({ + indent: 2, + sortKeys: false, + colorize: true, + }); + const [highlightedCode, setHighlightedCode] = useState(""); + + const formatJSON = useCallback(() => { + try { + if (!input.trim()) { + setOutput(""); + setError(null); + return; + } + + let parsedJSON = JSON.parse(input); + + if (options.sortKeys) { + parsedJSON = sortObjectKeys(parsedJSON); + } + + const formatted = JSON.stringify(parsedJSON, null, options.indent); + setOutput(formatted); + setError(null); + } catch (err) { + setError((err as Error).message); + setOutput(""); + } + }, [input, options]); + + useEffect(() => { + if (options.colorize && output) { + const highlighted = hljs.highlight(output, { language: "json" }).value; + setHighlightedCode(highlighted); + } + }, [output, options.colorize]); + + const sortObjectKeys = (obj: any): any => { + if (obj === null || typeof obj !== "object") { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map(sortObjectKeys); + } + + return Object.keys(obj) + .sort() + .reduce((result: Record, key) => { + result[key] = sortObjectKeys(obj[key]); + return result; + }, {}); + }; + + const copyToClipboard = async () => { + try { + await navigator.clipboard.writeText(output); + } catch (err) { + console.error("Failed to copy text:", err); + } + }; + + const downloadJSON = () => { + const blob = new Blob([output], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "formatted.json"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + return ( +
+ + + JSON 格式化工具 + + 将 JSON 文本格式化,支持语法高亮、键值排序等功能 + + + +
+
+
+ +
+
+ + + setOptions({ + ...options, + indent: parseInt(e.target.value) || 0, + }) + } + className="w-20" + /> +
+
+ + setOptions({ ...options, sortKeys: checked }) + } + /> + +
+
+ + setOptions({ ...options, colorize: checked }) + } + /> + +
+
+
+
+ +
+
+ +