From 1343e7c3fc5578ecac03c46a77bc706c92a8275f Mon Sep 17 00:00:00 2001 From: AprilNEA Date: Sat, 19 Jul 2025 14:43:12 +0800 Subject: [PATCH 1/9] feat(utility): add string-inspector utility --- .../utilities/converter/string-inspector.tsx | 340 ++++++++++++++++++ packages/frontend/src/utilities/meta.ts | 7 + pnpm-lock.yaml | 2 + 3 files changed, 349 insertions(+) create mode 100644 packages/frontend/src/utilities/converter/string-inspector.tsx diff --git a/packages/frontend/src/utilities/converter/string-inspector.tsx b/packages/frontend/src/utilities/converter/string-inspector.tsx new file mode 100644 index 0000000..f75e17a --- /dev/null +++ b/packages/frontend/src/utilities/converter/string-inspector.tsx @@ -0,0 +1,340 @@ +/** + * Copyright (c) 2023-2025, AprilNEA LLC. + * + * Dual licensed under: + * - GPL-3.0 (open source) + * - Commercial license (contact us) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * See LICENSE file for details or contact admin@aprilnea.com + */ + +import { msg } from "@lingui/core/macro"; +import { Trans, useLingui } from "@lingui/react/macro"; +import { useDebouncedValue } from "foxact/use-debounced-value"; +import { FilterIcon, ScanTextIcon } from "lucide-react"; +import { useMemo, useState } from "react"; +import TwoSectionLayout from "@/components/layout/two-section"; +import { ClearTool, LoadFileTool, PasteTool } from "@/components/tools"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Separator } from "@/components/ui/separator"; +import { Textarea } from "@/components/ui/textarea"; + +// Sample text in multiple languages to demonstrate Unicode handling +const sampleText = `Blanditiis aliquam laboriosam eius at illum. Qui cumque et iste perferendis ut nam qui officiis nisi in in iusto reiciendis. A iure id praesentium et sed. Cupiditate illo eius sunt ut. Dolores itaque ut neque ipsam mollitia debitis est explicabo repellendus. Velit velit quaerat incidunt eum tempora dicta quod hic vel et ad. Perferendis aliquid aperiam omnis quisquam ut dolore et earum. Praesentium amet illum ea et odit.`; + +interface StringStats { + characters: number; + bytes: number; + words: number; + lines: number; + ascii: number; + unicode: number; + selection: { + location: number; + currentLine: number; + column: number; + }; +} + +interface WordDistribution { + word: string; + count: number; +} + +export default function StringInspectorPage() { + const { t } = useLingui(); + const [input, setInput] = useState(sampleText); + const [selectionStart, setSelectionStart] = useState(0); + const [selectionEnd, setSelectionEnd] = useState(0); + const [caseSensitive, setCaseSensitive] = useState(true); + const [wordFilter, setWordFilter] = useState(""); + + const debouncedInput = useDebouncedValue(input, 100, false); + + // Calculate statistics + const stats: StringStats = useMemo(() => { + const text = debouncedInput; + + // Basic counts + const characters = text.length; + const bytes = new TextEncoder().encode(text).length; + const words = text.trim() ? text.trim().split(/\s+/).length : 0; + const lines = text.split('\n').length; + + // ASCII vs Unicode + let ascii = 0; + let unicode = 0; + for (const char of text) { + if (char.charCodeAt(0) <= 127) { + ascii++; + } else { + unicode++; + } + } + + // Selection information + const beforeSelection = text.substring(0, selectionEnd); + const linesBeforeSelection = beforeSelection.split('\n'); + const currentLine = linesBeforeSelection.length; + const column = linesBeforeSelection[linesBeforeSelection.length - 1].length + 1; + + return { + characters, + bytes, + words, + lines, + ascii, + unicode, + selection: { + location: selectionEnd, + currentLine, + column, + } + }; + }, [debouncedInput, selectionEnd]); + + // Calculate word distribution + const wordDistribution: WordDistribution[] = useMemo(() => { + const text = caseSensitive ? debouncedInput : debouncedInput.toLowerCase(); + const words = text.match(/\b[\w']+\b/g) || []; + + const wordMap = new Map(); + for (const word of words) { + const key = caseSensitive ? word : word.toLowerCase(); + wordMap.set(key, (wordMap.get(key) || 0) + 1); + } + + // Convert to array and sort by frequency + const distribution = Array.from(wordMap.entries()) + .map(([word, count]) => ({ word, count })) + .sort((a, b) => b.count - a.count); + + // Apply filter if present + if (wordFilter) { + const filterLower = wordFilter.toLowerCase(); + return distribution.filter(item => + item.word.toLowerCase().includes(filterLower) + ); + } + + return distribution; + }, [debouncedInput, caseSensitive, wordFilter]); + + const handleTextSelect = (e: React.SyntheticEvent) => { + const target = e.currentTarget; + setSelectionStart(target.selectionStart); + setSelectionEnd(target.selectionEnd); + }; + + const handleSampleText = () => { + setInput(sampleText); + }; + + const inputToolbar = ( + <> + { + setInput(text); + }} + /> + + { + setInput(""); + }, + }} + /> + + ); + + const inputContent = ( + +