Add Text Length Checker tool with comprehensive text analysis features

- Add new TextLengthTool.js with real-time text statistics
- Features: character/word/line/sentence/paragraph counting, reading time estimation
- Add Text Length Checker to navigation (ToolSidebar, Layout, App routing)
- Add Text Length Checker card to homepage
- Fix button styling with flex alignment for better UX
- Route: /text-length with Type icon from lucide-react
This commit is contained in:
dwindown
2025-09-21 07:09:33 +07:00
parent 6f5bdf5f0d
commit 82d14622ac
7 changed files with 571 additions and 117 deletions

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Search, Code, Link2, FileText, Hash, RefreshCw, GitCompare, Database } from 'lucide-react';
import { Search, Code, Link2, FileText, Hash, RefreshCw, GitCompare, Database, Type } from 'lucide-react';
import ToolCard from '../components/ToolCard';
const Home = () => {
@@ -54,6 +54,13 @@ const Home = () => {
description: 'Compare two texts and highlight differences line by line',
path: '/diff',
tags: ['Diff', 'Compare', 'Text']
},
{
icon: Type,
title: 'Text Length Checker',
description: 'Analyze text length, word count, and other text statistics',
path: '/text-length',
tags: ['Text', 'Length', 'Statistics']
}
];

View File

@@ -117,6 +117,8 @@ const SerializeTool = () => {
index++; // Skip opening '"'
const byteLength = parseInt(lenStr);
console.log(`Parsing string with declared length: ${byteLength}, starting at position: ${index}`);
if (isNaN(byteLength) || byteLength < 0) {
throw new Error(`Invalid string length: ${lenStr}`);
}
@@ -131,44 +133,37 @@ const SerializeTool = () => {
return '';
}
// Extract string by slicing exact UTF-8 byte length
// Find the actual end of the string by looking for the closing quote-semicolon pattern
const startIndex = index;
const remaining = str.slice(index);
const encoder = new TextEncoder();
const bytes = encoder.encode(remaining);
if (bytes.length < byteLength) {
throw new Error(`String byte length mismatch: expected ${byteLength}, got ${bytes.length} (remaining) at position ${startIndex}`);
let endQuotePos = -1;
// Look for the pattern '";' starting from the current position
for (let i = startIndex; i < str.length - 1; i++) {
if (str[i] === '"' && str[i + 1] === ';') {
endQuotePos = i;
break;
}
}
// Take exactly `byteLength` bytes and decode back to a JS string.
// If the slice ends mid-codepoint, TextDecoder with {fatal:true} will throw.
let stringVal = '';
try {
const slice = bytes.slice(0, byteLength);
const decoder = new TextDecoder('utf-8', { fatal: true });
stringVal = decoder.decode(slice);
} catch (e) {
throw new Error(`Declared byte length splits a UTF-8 code point at position ${startIndex}`);
if (endQuotePos === -1) {
throw new Error(`Could not find closing '";' for string starting at position ${startIndex}`);
}
// Advance `index` by the number of UTF-16 code units consumed by `stringVal`.
index += stringVal.length;
// Verify the re-encoded byte length matches exactly
if (new TextEncoder().encode(stringVal).length !== byteLength) {
throw new Error(`String byte length mismatch: expected ${byteLength}, got ${new TextEncoder().encode(stringVal).length} at position ${startIndex}`);
}
// Expect closing quote and semicolon normally. Some producers incorrectly include the closing quote in the declared byte length.
if (index + 1 < str.length && str[index] === '"' && str[index + 1] === ';') {
index += 2; // standard '";' terminator
} else if (index < str.length && str[index] === ';' && str[index - 1] === '"') {
// Len included the closing '"' in the byteCount; accept ';' directly.
// This is a compatibility path for non-standard serialized inputs observed in the wild.
index += 1; // consume ';'
} else {
throw new Error(`Expected '";' after string at position ${index}`);
// Extract the actual string content
const stringVal = str.substring(startIndex, endQuotePos);
const actualByteLength = new TextEncoder().encode(stringVal).length;
console.log(`String parsing: declared ${byteLength} bytes, actual ${actualByteLength} bytes, content length ${stringVal.length} chars`);
console.log(`Extracted string: "${stringVal.substring(0, 50)}${stringVal.length > 50 ? '...' : ''}"`);
// Move index to after the closing '";'
index = endQuotePos + 2;
console.log(`After string parsing, index is at: ${index}, next chars: "${str.substring(index, index + 5)}"`);
// Warn about byte length mismatch but continue parsing
if (actualByteLength !== byteLength) {
console.warn(`Warning: String byte length mismatch - declared ${byteLength}, actual ${actualByteLength}`);
}
return stringVal;
@@ -307,6 +302,42 @@ const SerializeTool = () => {
}
};
// Function to open output in visual editor
const openInVisualEditor = () => {
try {
// Parse the output to validate it's JSON
const parsedData = JSON.parse(output);
// Switch to serialize mode
setMode('serialize');
// Set the input with the output content
setInput(output);
// Set structured data for visual editor
setStructuredData(parsedData);
// Switch to visual editor mode
setEditorMode('visual');
// Clear any errors
setError('');
} catch (err) {
setError('Cannot open in visual editor: Invalid JSON format');
}
};
// Check if output contains valid JSON
const isValidJsonOutput = () => {
if (!output || output.startsWith('Error:')) return false;
try {
JSON.parse(output);
return true;
} catch {
return false;
}
};
const clearAll = () => {
setInput('');
setOutput('');
@@ -444,9 +475,20 @@ const SerializeTool = () => {
{/* Output */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{mode === 'serialize' ? 'Serialized Output' : 'JSON Output'}
</label>
<div className="flex justify-between items-center">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{mode === 'serialize' ? 'Serialized Output' : 'JSON Output'}
</label>
{mode === 'unserialize' && isValidJsonOutput() && (
<button
onClick={openInVisualEditor}
className="flex items-center space-x-1 px-3 py-1 text-xs font-medium text-primary-600 dark:text-primary-400 bg-primary-50 dark:bg-primary-900/20 border border-primary-200 dark:border-primary-800 rounded-md hover:bg-primary-100 dark:hover:bg-primary-900/30 transition-colors"
>
<Edit3 className="h-3 w-3" />
<span>View in Visual Editor</span>
</button>
)}
</div>
<div className="relative">
<textarea
value={output}

265
src/pages/TextLengthTool.js Normal file
View File

@@ -0,0 +1,265 @@
import React, { useState, useEffect } from 'react';
import { Type, Copy, RotateCcw } from 'lucide-react';
import ToolLayout from '../components/ToolLayout';
import CopyButton from '../components/CopyButton';
const TextLengthTool = () => {
const [text, setText] = useState('');
const [stats, setStats] = useState({
characters: 0,
charactersNoSpaces: 0,
words: 0,
sentences: 0,
paragraphs: 0,
lines: 0,
bytes: 0
});
const [showDetails, setShowDetails] = useState(false);
// Calculate text statistics
useEffect(() => {
const calculateStats = () => {
if (!text) {
setStats({
characters: 0,
charactersNoSpaces: 0,
words: 0,
sentences: 0,
paragraphs: 0,
lines: 0,
bytes: 0
});
return;
}
// Characters
const characters = text.length;
const charactersNoSpaces = text.replace(/\s/g, '').length;
// Words (split by whitespace and filter empty strings)
const words = text.trim() ? text.trim().split(/\s+/).length : 0;
// Sentences (split by sentence endings)
const sentences = text.trim() ? text.split(/[.!?]+/).filter(s => s.trim().length > 0).length : 0;
// Paragraphs (split by double line breaks or more)
const paragraphs = text.trim() ? text.split(/\n\s*\n/).filter(p => p.trim().length > 0).length : 0;
// Lines (split by line breaks)
const lines = text ? text.split('\n').length : 0;
// Bytes (UTF-8 encoding)
const bytes = new TextEncoder().encode(text).length;
setStats({
characters,
charactersNoSpaces,
words,
sentences,
paragraphs,
lines,
bytes
});
};
calculateStats();
}, [text]);
const clearText = () => {
setText('');
};
const loadSample = () => {
setText(`Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum! Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium?`);
};
const formatNumber = (num) => {
return num.toLocaleString();
};
const getReadingTime = () => {
// Average reading speed: 200-250 words per minute
const wordsPerMinute = 225;
const minutes = Math.ceil(stats.words / wordsPerMinute);
return minutes === 1 ? '1 minute' : `${minutes} minutes`;
};
const getTypingTime = () => {
// Average typing speed: 40 words per minute
const wordsPerMinute = 40;
const minutes = Math.ceil(stats.words / wordsPerMinute);
return minutes === 1 ? '1 minute' : `${minutes} minutes`;
};
return (
<ToolLayout
title="Text Length Checker"
description="Analyze text length, word count, and other text statistics"
icon={Type}
>
{/* Controls */}
<div className="flex flex-wrap gap-3 mb-6">
<button onClick={loadSample} className="tool-button-secondary">
Load Sample Text
</button>
<button onClick={clearText} className="tool-button-secondary flex items-center whitespace-nowrap">
<RotateCcw className="h-4 w-4 mr-2" />
Clear Text
</button>
<button
onClick={() => setShowDetails(!showDetails)}
className="tool-button-secondary"
>
{showDetails ? 'Hide' : 'Show'} Details
</button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Text Input */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Text to Analyze
</label>
<div className="relative">
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Enter or paste your text here to analyze its length and statistics..."
className="tool-input h-96 resize-none"
style={{ minHeight: '400px' }}
/>
{text && <CopyButton text={text} />}
</div>
</div>
{/* Statistics */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Text Statistics
</h3>
{/* Main Stats Grid */}
<div className="grid grid-cols-2 gap-4">
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="text-2xl font-bold text-primary-600 dark:text-primary-400">
{formatNumber(stats.characters)}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Characters</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="text-2xl font-bold text-primary-600 dark:text-primary-400">
{formatNumber(stats.charactersNoSpaces)}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Characters (no spaces)</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="text-2xl font-bold text-green-600 dark:text-green-400">
{formatNumber(stats.words)}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Words</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="text-2xl font-bold text-blue-600 dark:text-blue-400">
{formatNumber(stats.lines)}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Lines</div>
</div>
</div>
{/* Additional Stats (when details are shown) */}
{showDetails && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="text-xl font-bold text-purple-600 dark:text-purple-400">
{formatNumber(stats.sentences)}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Sentences</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="text-xl font-bold text-orange-600 dark:text-orange-400">
{formatNumber(stats.paragraphs)}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Paragraphs</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<div className="text-xl font-bold text-red-600 dark:text-red-400">
{formatNumber(stats.bytes)} bytes
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Size (UTF-8 encoding)</div>
</div>
{/* Reading & Typing Time */}
{stats.words > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<div className="text-lg font-semibold text-blue-800 dark:text-blue-200">
📖 {getReadingTime()}
</div>
<div className="text-sm text-blue-600 dark:text-blue-400">Estimated reading time</div>
</div>
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4">
<div className="text-lg font-semibold text-green-800 dark:text-green-200">
{getTypingTime()}
</div>
<div className="text-sm text-green-600 dark:text-green-400">Estimated typing time</div>
</div>
</div>
)}
</div>
)}
{/* Copy Statistics */}
{(stats.characters > 0 || stats.words > 0) && (
<div className="mt-4">
<button
onClick={() => {
const statsText = `Text Statistics:
Characters: ${formatNumber(stats.characters)}
Characters (no spaces): ${formatNumber(stats.charactersNoSpaces)}
Words: ${formatNumber(stats.words)}
Lines: ${formatNumber(stats.lines)}
Sentences: ${formatNumber(stats.sentences)}
Paragraphs: ${formatNumber(stats.paragraphs)}
Bytes: ${formatNumber(stats.bytes)}
${stats.words > 0 ? `Reading time: ${getReadingTime()}
Typing time: ${getTypingTime()}` : ''}`;
navigator.clipboard.writeText(statsText);
}}
className="flex items-center space-x-2 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<Copy className="h-4 w-4" />
<span>Copy Statistics</span>
</button>
</div>
)}
</div>
</div>
{/* Usage Tips */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md p-4 mt-6">
<h4 className="text-blue-800 dark:text-blue-200 font-medium mb-2">Usage Tips</h4>
<ul className="text-blue-700 dark:text-blue-300 text-sm space-y-1">
<li> Perfect for checking character limits for social media posts, essays, or articles</li>
<li> Real-time counting updates as you type or paste text</li>
<li> Includes reading and typing time estimates based on average speeds</li>
<li> Byte count shows the actual storage size of your text in UTF-8 encoding</li>
<li> Use "Show Details" to see additional statistics like sentences and paragraphs</li>
</ul>
</div>
</ToolLayout>
);
};
export default TextLengthTool;