Improve ObjectEditor and Add TableEditor
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import Layout from './components/Layout';
|
||||
import ErrorBoundary from './components/ErrorBoundary';
|
||||
import Home from './pages/Home';
|
||||
import JsonTool from './pages/JsonTool';
|
||||
import SerializeTool from './pages/SerializeTool';
|
||||
@@ -11,11 +12,13 @@ import BeautifierTool from './pages/BeautifierTool';
|
||||
import DiffTool from './pages/DiffTool';
|
||||
import TextLengthTool from './pages/TextLengthTool';
|
||||
import ObjectEditor from './pages/ObjectEditor';
|
||||
import TableEditor from './pages/TableEditor';
|
||||
|
||||
import './index.css';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Router>
|
||||
<Layout>
|
||||
<Routes>
|
||||
@@ -29,10 +32,12 @@ function App() {
|
||||
<Route path="/diff" element={<DiffTool />} />
|
||||
<Route path="/text-length" element={<TextLengthTool />} />
|
||||
<Route path="/object-editor" element={<ObjectEditor />} />
|
||||
<Route path="/table-editor" element={<TableEditor />} />
|
||||
|
||||
</Routes>
|
||||
</Layout>
|
||||
</Router>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
75
src/components/ErrorBoundary.js
Normal file
75
src/components/ErrorBoundary.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null, errorInfo: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
// Update state so the next render will show the fallback UI
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
// Log the error for debugging
|
||||
this.setState({
|
||||
error: error,
|
||||
errorInfo: errorInfo
|
||||
});
|
||||
|
||||
// You can also log the error to an error reporting service here
|
||||
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
// Fallback UI
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center p-4">
|
||||
<div className="max-w-md w-full bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 text-center">
|
||||
<div className="mb-4">
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 dark:bg-red-900/20">
|
||||
<svg className="h-6 w-6 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||
Something went wrong
|
||||
</h2>
|
||||
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
The application encountered an error. This might be due to browser compatibility issues.
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition-colors"
|
||||
>
|
||||
Reload Page
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => this.setState({ hasError: false, error: null, errorInfo: null })}
|
||||
className="w-full bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 font-medium py-2 px-4 rounded-md transition-colors"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-xs text-gray-500 dark:text-gray-400">
|
||||
If you're using Telegram's built-in browser, try opening this link in your default browser for better compatibility.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Home, Hash, FileSpreadsheet, Wand2, GitCompare, Menu, X, LinkIcon, Code2, ChevronDown, Type, Edit3 } from 'lucide-react';
|
||||
import { Home, Hash, FileSpreadsheet, Wand2, GitCompare, Menu, X, LinkIcon, Code2, ChevronDown, Type, Edit3, Table } from 'lucide-react';
|
||||
import ThemeToggle from './ThemeToggle';
|
||||
import ToolSidebar from './ToolSidebar';
|
||||
|
||||
@@ -36,6 +36,7 @@ const Layout = ({ children }) => {
|
||||
|
||||
const tools = [
|
||||
{ path: '/object-editor', name: 'Object Editor', icon: Edit3, description: 'Visual editor for JSON & PHP objects' },
|
||||
{ path: '/table-editor', name: 'Table Editor', icon: Table, description: 'Import, edit & export tabular data' },
|
||||
{ path: '/url', name: 'URL Tool', icon: LinkIcon, description: 'URL encode/decode' },
|
||||
{ path: '/base64', name: 'Base64 Tool', icon: Hash, description: 'Base64 encode/decode' },
|
||||
{ path: '/csv-json', name: 'CSV/JSON Tool', icon: FileSpreadsheet, description: 'Convert CSV ↔ JSON' },
|
||||
|
||||
@@ -106,9 +106,17 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
delete current[key];
|
||||
}
|
||||
|
||||
// Check if we're removing from root level and it's the last property
|
||||
if (parentPath === 'root' && Object.keys(newData).length === 0) {
|
||||
// Add an empty property to maintain initial state, like TableEditor maintains at least one row
|
||||
newData[''] = '';
|
||||
console.log('🔄 ADDED INITIAL EMPTY PROPERTY - Last root property was deleted');
|
||||
}
|
||||
|
||||
console.log('🗑️ REMOVE PROPERTY - After:', {
|
||||
parentPath,
|
||||
remainingKeys: Array.isArray(current) ? current.length : Object.keys(current)
|
||||
remainingKeys: Array.isArray(current) ? current.length : Object.keys(current),
|
||||
totalRootKeys: Object.keys(newData).length
|
||||
});
|
||||
|
||||
updateData(newData);
|
||||
@@ -223,6 +231,25 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
return current;
|
||||
};
|
||||
|
||||
// Helper function to display string values with proper unescaping
|
||||
const getDisplayValue = (value) => {
|
||||
if (value === null) return 'null';
|
||||
if (value === undefined) return '';
|
||||
|
||||
const stringValue = value.toString();
|
||||
|
||||
// If it's a string, unescape common JSON escape sequences for display
|
||||
if (typeof value === 'string') {
|
||||
return stringValue
|
||||
.replace(/\\"/g, '"') // Unescape quotes
|
||||
.replace(/\\'/g, "'") // Unescape single quotes
|
||||
.replace(/\\\//g, '/') // Unescape forward slashes
|
||||
.replace(/\\\\/g, '\\'); // Unescape backslashes (do this last)
|
||||
}
|
||||
|
||||
return stringValue;
|
||||
};
|
||||
|
||||
const renameKey = (oldKey, newKey, path) => {
|
||||
if (oldKey === newKey || !newKey.trim()) return;
|
||||
|
||||
@@ -401,11 +428,7 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
) : (
|
||||
typeof value === 'string' && value.includes('\n') ? (
|
||||
<textarea
|
||||
value={
|
||||
value === null ? 'null' :
|
||||
value === undefined ? '' :
|
||||
value.toString()
|
||||
}
|
||||
value={getDisplayValue(value)}
|
||||
onChange={(e) => updateValue(e.target.value, path)}
|
||||
className="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0 resize-y"
|
||||
placeholder="Long text value"
|
||||
@@ -414,11 +437,7 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={
|
||||
value === null ? 'null' :
|
||||
value === undefined ? '' :
|
||||
value.toString()
|
||||
}
|
||||
value={getDisplayValue(value)}
|
||||
onChange={(e) => updateValue(e.target.value, path)}
|
||||
className="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0"
|
||||
placeholder="Value"
|
||||
@@ -500,17 +519,9 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border border-gray-300 dark:border-gray-600 rounded-lg p-4 bg-white dark:bg-gray-800 min-h-96 w-full">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="min-h-96 w-full">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">Structured Data Editor</h3>
|
||||
<button
|
||||
onClick={() => addProperty(data, 'root')}
|
||||
className="flex items-center space-x-1 px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors flex-shrink-0"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Add Property</span>
|
||||
<span className="sm:hidden">Add</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-hidden">
|
||||
@@ -524,6 +535,15 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
renderValue(value, key, `root.${key}`, 'root')
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Root level Add Property button */}
|
||||
<button
|
||||
onClick={() => addProperty(data, 'root')}
|
||||
className="flex items-center space-x-1 px-2 py-1 text-sm text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900 rounded"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Add Property</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Search, LinkIcon, Hash, FileSpreadsheet, Wand2, GitCompare, Home, ChevronLeft, ChevronRight, Type, Edit3 } from 'lucide-react';
|
||||
import { Search, LinkIcon, Hash, Table, Wand2, GitCompare, Home, ChevronLeft, ChevronRight, Type, Edit3 } from 'lucide-react';
|
||||
|
||||
const ToolSidebar = () => {
|
||||
const location = useLocation();
|
||||
@@ -10,9 +10,9 @@ const ToolSidebar = () => {
|
||||
const tools = [
|
||||
{ path: '/', name: 'Home', icon: Home, description: 'Back to homepage' },
|
||||
{ path: '/object-editor', name: 'Object Editor', icon: Edit3, description: 'Visual editor for JSON & PHP objects' },
|
||||
{ path: '/table-editor', name: 'Table Editor', icon: Table, description: 'Import, edit & export tabular data' },
|
||||
{ path: '/url', name: 'URL Tool', icon: LinkIcon, description: 'URL encode/decode' },
|
||||
{ path: '/base64', name: 'Base64 Tool', icon: Hash, description: 'Base64 encode/decode' },
|
||||
{ path: '/csv-json', name: 'CSV/JSON Tool', icon: FileSpreadsheet, description: 'Convert CSV ↔ JSON' },
|
||||
{ path: '/beautifier', name: 'Beautifier Tool', icon: Wand2, description: 'Beautify/minify code' },
|
||||
{ path: '/diff', name: 'Diff Tool', icon: GitCompare, description: 'Compare text differences' },
|
||||
{ path: '/text-length', name: 'Text Length Checker', icon: Type, description: 'Analyze text length & stats' },
|
||||
|
||||
@@ -34,4 +34,13 @@
|
||||
.copy-button {
|
||||
@apply absolute top-2 right-2 p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 rounded-md transition-colors duration-200;
|
||||
}
|
||||
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none; /* Safari and Chrome */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import { initBrowserCompat } from './utils/browserCompat';
|
||||
|
||||
// Initialize browser compatibility fixes before React renders
|
||||
initBrowserCompat();
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Search, Code, Link2, FileText, Hash, RefreshCw, GitCompare, Type, Edit3 } from 'lucide-react';
|
||||
import { Search, Code, Link2, FileText, Hash, RefreshCw, GitCompare, Type, Edit3, Table } from 'lucide-react';
|
||||
import ToolCard from '../components/ToolCard';
|
||||
|
||||
const Home = () => {
|
||||
@@ -14,6 +14,13 @@ const Home = () => {
|
||||
path: '/object-editor',
|
||||
tags: ['Visual', 'JSON', 'PHP', 'Objects', 'Editor']
|
||||
},
|
||||
{
|
||||
icon: Table,
|
||||
title: 'Table Editor',
|
||||
description: 'Import, edit, and export tabular data from URLs, files, or paste CSV/JSON',
|
||||
path: '/table-editor',
|
||||
tags: ['Table', 'CSV', 'JSON', 'Data', 'Editor']
|
||||
},
|
||||
{
|
||||
icon: Link2,
|
||||
title: 'URL Encoder/Decoder',
|
||||
@@ -28,13 +35,6 @@ const Home = () => {
|
||||
path: '/base64',
|
||||
tags: ['Base64', 'Encode', 'Binary']
|
||||
},
|
||||
{
|
||||
icon: RefreshCw,
|
||||
title: 'CSV ↔ JSON Converter',
|
||||
description: 'Convert between CSV and JSON formats with custom delimiters',
|
||||
path: '/csv-json',
|
||||
tags: ['CSV', 'JSON', 'Convert']
|
||||
},
|
||||
{
|
||||
icon: FileText,
|
||||
title: 'Code Beautifier/Minifier',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
3637
src/pages/TableEditor.js
Normal file
3637
src/pages/TableEditor.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,16 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Type, Copy, RotateCcw } from 'lucide-react';
|
||||
import { Type, Copy, RotateCcw, Globe, Download, AlertCircle, CheckCircle, Clock, X } from 'lucide-react';
|
||||
import ToolLayout from '../components/ToolLayout';
|
||||
import CopyButton from '../components/CopyButton';
|
||||
import { extractContentFromUrl, CONTENT_TYPE_INFO } from '../utils/contentExtractor';
|
||||
|
||||
const TextLengthTool = () => {
|
||||
const [text, setText] = useState('');
|
||||
const [url, setUrl] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [urlResult, setUrlResult] = useState(null);
|
||||
const [error, setError] = useState('');
|
||||
const [useArticleOnly, setUseArticleOnly] = useState(true);
|
||||
const [stats, setStats] = useState({
|
||||
characters: 0,
|
||||
charactersNoSpaces: 0,
|
||||
@@ -65,8 +71,52 @@ const TextLengthTool = () => {
|
||||
calculateStats();
|
||||
}, [text]);
|
||||
|
||||
// Handle URL fetching
|
||||
const fetchUrlContent = async () => {
|
||||
if (!url.trim()) {
|
||||
setError('Please enter a valid URL');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
setUrlResult(null);
|
||||
|
||||
try {
|
||||
const result = await extractContentFromUrl(url.trim());
|
||||
|
||||
if (result.success) {
|
||||
setUrlResult(result);
|
||||
// Set text based on user preference
|
||||
const textToAnalyze = useArticleOnly ? result.articleText : result.allText;
|
||||
setText(textToAnalyze);
|
||||
setError('');
|
||||
} else {
|
||||
setError(result.error);
|
||||
setUrlResult(null);
|
||||
}
|
||||
} catch (err) {
|
||||
let errorMessage = err.message;
|
||||
if (errorMessage.includes('Failed to fetch')) {
|
||||
errorMessage = 'Unable to fetch content due to CORS restrictions or network issues';
|
||||
}
|
||||
setError(errorMessage);
|
||||
setUrlResult(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const clearText = () => {
|
||||
setText('');
|
||||
setUrlResult(null);
|
||||
setError('');
|
||||
};
|
||||
|
||||
const clearUrl = () => {
|
||||
setUrl('');
|
||||
setUrlResult(null);
|
||||
setError('');
|
||||
};
|
||||
|
||||
const loadSample = () => {
|
||||
@@ -101,6 +151,121 @@ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru
|
||||
description="Analyze text length, word count, and other text statistics"
|
||||
icon={Type}
|
||||
>
|
||||
{/* URL Input Section */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 mb-6">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Globe className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Analyze Content from URL</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-2">
|
||||
<div className="relative flex-1">
|
||||
<input
|
||||
type="url"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
placeholder="https://example.com/article"
|
||||
className="tool-input w-full pr-10"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{url && !isLoading && (
|
||||
<button
|
||||
onClick={clearUrl}
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={fetchUrlContent}
|
||||
disabled={isLoading || !url.trim()}
|
||||
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium px-4 py-2 rounded-md transition-colors flex items-center whitespace-nowrap"
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Clock className="h-4 w-4 mr-2 animate-spin" />
|
||||
Fetching...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="h-4 w-4 mr-2" />
|
||||
Fetch Content
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* URL Result Status */}
|
||||
{urlResult && (
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-lg">{CONTENT_TYPE_INFO[urlResult.contentType].emoji}</span>
|
||||
<span className={`font-medium ${CONTENT_TYPE_INFO[urlResult.contentType].color}`}>
|
||||
{CONTENT_TYPE_INFO[urlResult.contentType].label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||
{CONTENT_TYPE_INFO[urlResult.contentType].description}
|
||||
</div>
|
||||
{urlResult.title && (
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100 mb-1">
|
||||
{urlResult.title}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Article: {urlResult.metrics.articleWordCount} words •
|
||||
Total: {urlResult.metrics.totalWordCount} words •
|
||||
Ratio: {Math.round(urlResult.metrics.contentRatio * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="flex items-center text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={useArticleOnly}
|
||||
onChange={(e) => {
|
||||
setUseArticleOnly(e.target.checked);
|
||||
const textToAnalyze = e.target.checked ? urlResult.articleText : urlResult.allText;
|
||||
setText(textToAnalyze);
|
||||
}}
|
||||
className="mr-2"
|
||||
/>
|
||||
Article Only
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="h-4 w-4 text-red-600 dark:text-red-400 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<div className="text-sm text-red-700 dark:text-red-300 mb-2">{error}</div>
|
||||
{error.includes('fetch') && (
|
||||
<div className="text-xs text-red-600 dark:text-red-400">
|
||||
<p className="mb-1"><strong>Common solutions:</strong></p>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
<li>Some websites block cross-origin requests for security</li>
|
||||
<li>Try copying the article text manually and pasting it below</li>
|
||||
<li>The site might require JavaScript to load content</li>
|
||||
<li>Check if the URL is accessible and returns HTML content</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex flex-wrap gap-3 mb-6">
|
||||
<button onClick={loadSample} className="tool-button-secondary">
|
||||
@@ -138,9 +303,32 @@ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru
|
||||
|
||||
{/* Statistics */}
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Text Statistics
|
||||
</h3>
|
||||
{(stats.characters > 0 || stats.words > 0) && (
|
||||
<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-3 py-1.5 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>
|
||||
|
||||
{/* Main Stats Grid */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
@@ -220,30 +408,6 @@ Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deseru
|
||||
</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>
|
||||
|
||||
@@ -251,11 +415,13 @@ Typing time: ${getTypingTime()}` : ''}`;
|
||||
<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>
|
||||
<li>• <strong>URL Analysis:</strong> Fetch and analyze content from any web page or article</li>
|
||||
<li>• <strong>Smart Content Detection:</strong> Automatically detects articles vs general web content</li>
|
||||
<li>• <strong>Article vs Full Page:</strong> Choose to analyze just the main article or entire page content</li>
|
||||
<li>• <strong>Real-time Counting:</strong> Updates as you type or paste text</li>
|
||||
<li>• <strong>Reading Time:</strong> Estimates based on average reading speed (225 WPM)</li>
|
||||
<li>• <strong>Content Quality:</strong> Shows content-to-noise ratio for web pages</li>
|
||||
<li>• Perfect for checking character limits for social media, essays, or articles</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ToolLayout>
|
||||
|
||||
147
src/utils/browserCompat.js
Normal file
147
src/utils/browserCompat.js
Normal file
@@ -0,0 +1,147 @@
|
||||
// Browser compatibility utilities for handling different browser environments
|
||||
|
||||
/**
|
||||
* Detect if the app is running in Telegram's built-in browser
|
||||
*/
|
||||
export const isTelegramBrowser = () => {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
return userAgent.includes('telegram') ||
|
||||
userAgent.includes('tgios') ||
|
||||
userAgent.includes('tgandroid') ||
|
||||
// Check for Telegram-specific window properties
|
||||
(window.TelegramWebviewProxy !== undefined) ||
|
||||
// Check for common Telegram browser characteristics
|
||||
(userAgent.includes('mobile') && userAgent.includes('webkit') && !userAgent.includes('chrome'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect if the app is running in any mobile in-app browser
|
||||
*/
|
||||
export const isInAppBrowser = () => {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
return userAgent.includes('wv') || // WebView
|
||||
userAgent.includes('telegram') ||
|
||||
userAgent.includes('fbav') || // Facebook
|
||||
userAgent.includes('fban') || // Facebook
|
||||
userAgent.includes('instagram') ||
|
||||
userAgent.includes('twitter') ||
|
||||
userAgent.includes('line') ||
|
||||
userAgent.includes('whatsapp');
|
||||
};
|
||||
|
||||
/**
|
||||
* Get browser information
|
||||
*/
|
||||
export const getBrowserInfo = () => {
|
||||
const userAgent = navigator.userAgent;
|
||||
return {
|
||||
userAgent,
|
||||
isTelegram: isTelegramBrowser(),
|
||||
isInApp: isInAppBrowser(),
|
||||
isMobile: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent),
|
||||
isIOS: /iPad|iPhone|iPod/.test(userAgent),
|
||||
isAndroid: /Android/.test(userAgent)
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Add polyfills and compatibility fixes for problematic browsers
|
||||
*/
|
||||
export const addCompatibilityFixes = () => {
|
||||
// Fix for missing or problematic console methods in some browsers
|
||||
if (!window.console) {
|
||||
window.console = {
|
||||
log: () => {},
|
||||
error: () => {},
|
||||
warn: () => {},
|
||||
info: () => {},
|
||||
debug: () => {}
|
||||
};
|
||||
}
|
||||
|
||||
// Ensure console methods exist and are functions
|
||||
['log', 'error', 'warn', 'info', 'debug'].forEach(method => {
|
||||
if (typeof console[method] !== 'function') {
|
||||
console[method] = () => {};
|
||||
}
|
||||
});
|
||||
|
||||
// Add requestAnimationFrame polyfill if missing
|
||||
if (!window.requestAnimationFrame) {
|
||||
window.requestAnimationFrame = (callback) => {
|
||||
return setTimeout(callback, 1000 / 60);
|
||||
};
|
||||
}
|
||||
|
||||
// Add cancelAnimationFrame polyfill if missing
|
||||
if (!window.cancelAnimationFrame) {
|
||||
window.cancelAnimationFrame = (id) => {
|
||||
clearTimeout(id);
|
||||
};
|
||||
}
|
||||
|
||||
// Fix for missing or problematic localStorage in some browsers
|
||||
try {
|
||||
localStorage.setItem('test', 'test');
|
||||
localStorage.removeItem('test');
|
||||
} catch (e) {
|
||||
window.localStorage = {
|
||||
getItem: () => null,
|
||||
setItem: () => {},
|
||||
removeItem: () => {},
|
||||
clear: () => {},
|
||||
length: 0,
|
||||
key: () => null
|
||||
};
|
||||
}
|
||||
|
||||
// Fix for missing or problematic sessionStorage
|
||||
try {
|
||||
sessionStorage.setItem('test', 'test');
|
||||
sessionStorage.removeItem('test');
|
||||
} catch (e) {
|
||||
window.sessionStorage = {
|
||||
getItem: () => null,
|
||||
setItem: () => {},
|
||||
removeItem: () => {},
|
||||
clear: () => {},
|
||||
length: 0,
|
||||
key: () => null
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize compatibility fixes
|
||||
*/
|
||||
export const initBrowserCompat = () => {
|
||||
const browserInfo = getBrowserInfo();
|
||||
|
||||
// Log browser info for debugging
|
||||
console.log('Browser Info:', browserInfo);
|
||||
|
||||
// Add compatibility fixes
|
||||
addCompatibilityFixes();
|
||||
|
||||
// Add specific fixes for Telegram browser
|
||||
if (browserInfo.isTelegram) {
|
||||
console.log('Telegram browser detected - applying compatibility fixes');
|
||||
|
||||
// Add Telegram-specific error handling
|
||||
window.addEventListener('error', (event) => {
|
||||
console.log('Global error caught in Telegram browser:', event.error);
|
||||
// Prevent the error from bubbling up and showing the error overlay
|
||||
event.preventDefault();
|
||||
return true;
|
||||
});
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
console.log('Unhandled promise rejection in Telegram browser:', event.reason);
|
||||
// Prevent the error from bubbling up
|
||||
event.preventDefault();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return browserInfo;
|
||||
};
|
||||
371
src/utils/contentExtractor.js
Normal file
371
src/utils/contentExtractor.js
Normal file
@@ -0,0 +1,371 @@
|
||||
// Content extraction and article detection utilities
|
||||
|
||||
/**
|
||||
* Content classification types
|
||||
*/
|
||||
export const CONTENT_TYPES = {
|
||||
RICH_ARTICLE: 'rich_article',
|
||||
GENERAL_CONTENT: 'general_content',
|
||||
LIMITED_CONTENT: 'limited_content',
|
||||
NO_CONTENT: 'no_content'
|
||||
};
|
||||
|
||||
/**
|
||||
* Content type display information
|
||||
*/
|
||||
export const CONTENT_TYPE_INFO = {
|
||||
[CONTENT_TYPES.RICH_ARTICLE]: {
|
||||
label: 'Rich Article Content',
|
||||
emoji: '🟢',
|
||||
description: 'Clear article structure with headings and paragraphs',
|
||||
color: 'text-green-600 dark:text-green-400'
|
||||
},
|
||||
[CONTENT_TYPES.GENERAL_CONTENT]: {
|
||||
label: 'General Web Content',
|
||||
emoji: '🟡',
|
||||
description: 'Readable text mixed with navigation and UI elements',
|
||||
color: 'text-yellow-600 dark:text-yellow-400'
|
||||
},
|
||||
[CONTENT_TYPES.LIMITED_CONTENT]: {
|
||||
label: 'Limited Text Content',
|
||||
emoji: '🟠',
|
||||
description: 'Mostly UI/navigation with minimal readable text',
|
||||
color: 'text-orange-600 dark:text-orange-400'
|
||||
},
|
||||
[CONTENT_TYPES.NO_CONTENT]: {
|
||||
label: 'No Readable Content',
|
||||
emoji: '🔴',
|
||||
description: 'Images, videos, or heavily JavaScript-dependent content',
|
||||
color: 'text-red-600 dark:text-red-400'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* CORS proxy services for fetching external content
|
||||
*/
|
||||
const CORS_PROXIES = [
|
||||
'https://api.allorigins.win/get?url=',
|
||||
'https://corsproxy.io/?',
|
||||
'https://cors-anywhere.herokuapp.com/',
|
||||
'https://thingproxy.freeboard.io/fetch/'
|
||||
];
|
||||
|
||||
/**
|
||||
* Fetch and parse HTML content from URL with CORS proxy fallback
|
||||
*/
|
||||
export const fetchUrlContent = async (url) => {
|
||||
try {
|
||||
// Validate URL
|
||||
const urlObj = new URL(url);
|
||||
if (!['http:', 'https:'].includes(urlObj.protocol)) {
|
||||
throw new Error('Only HTTP and HTTPS URLs are supported');
|
||||
}
|
||||
|
||||
// First try direct fetch (works for same-origin or CORS-enabled sites)
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; TextAnalyzer/1.0)'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
if (contentType.includes('text/html')) {
|
||||
const html = await response.text();
|
||||
return { html, url: response.url, contentType };
|
||||
}
|
||||
}
|
||||
} catch (directError) {
|
||||
console.log('Direct fetch failed, trying CORS proxy:', directError.message);
|
||||
}
|
||||
|
||||
// Try CORS proxies
|
||||
let lastError = null;
|
||||
|
||||
for (const proxy of CORS_PROXIES) {
|
||||
try {
|
||||
let proxyUrl;
|
||||
let response;
|
||||
|
||||
if (proxy.includes('allorigins.win')) {
|
||||
// AllOrigins returns JSON with contents
|
||||
proxyUrl = `${proxy}${encodeURIComponent(url)}`;
|
||||
response = await fetch(proxyUrl);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.contents) {
|
||||
return {
|
||||
html: data.contents,
|
||||
url: data.status.url || url,
|
||||
contentType: 'text/html'
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Other proxies return HTML directly
|
||||
proxyUrl = `${proxy}${url}`;
|
||||
response = await fetch(proxyUrl, {
|
||||
headers: {
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const contentType = response.headers.get('content-type') || 'text/html';
|
||||
if (contentType.includes('text/html') || contentType.includes('text/plain')) {
|
||||
const html = await response.text();
|
||||
return { html, url, contentType };
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (proxyError) {
|
||||
lastError = proxyError;
|
||||
console.log(`Proxy ${proxy} failed:`, proxyError.message);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`All fetch methods failed. Last error: ${lastError?.message || 'Unknown error'}`);
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to fetch content: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse HTML and create DOM
|
||||
*/
|
||||
export const parseHtml = (html) => {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
return doc;
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect article elements and structure
|
||||
*/
|
||||
export const detectArticleStructure = (doc) => {
|
||||
const structure = {
|
||||
hasArticleTag: false,
|
||||
hasMainTag: false,
|
||||
headingCount: 0,
|
||||
paragraphCount: 0,
|
||||
hasMetaArticle: false,
|
||||
hasJsonLd: false,
|
||||
wordCount: 0,
|
||||
linkDensity: 0
|
||||
};
|
||||
|
||||
// Check for semantic HTML5 tags
|
||||
structure.hasArticleTag = doc.querySelector('article') !== null;
|
||||
structure.hasMainTag = doc.querySelector('main') !== null;
|
||||
|
||||
// Count headings
|
||||
structure.headingCount = doc.querySelectorAll('h1, h2, h3, h4, h5, h6').length;
|
||||
|
||||
// Count paragraphs
|
||||
structure.paragraphCount = doc.querySelectorAll('p').length;
|
||||
|
||||
// Check meta tags for articles
|
||||
const metaTags = doc.querySelectorAll('meta[property^="og:"], meta[name^="article:"]');
|
||||
structure.hasMetaArticle = Array.from(metaTags).some(meta =>
|
||||
meta.getAttribute('property') === 'og:type' && meta.getAttribute('content') === 'article' ||
|
||||
meta.getAttribute('name')?.startsWith('article:')
|
||||
);
|
||||
|
||||
// Check for JSON-LD structured data
|
||||
const jsonLdScripts = doc.querySelectorAll('script[type="application/ld+json"]');
|
||||
structure.hasJsonLd = Array.from(jsonLdScripts).some(script => {
|
||||
try {
|
||||
const data = JSON.parse(script.textContent);
|
||||
const type = data['@type'] || (Array.isArray(data) ? data[0]['@type'] : null);
|
||||
return type && ['Article', 'NewsArticle', 'BlogPosting'].includes(type);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return structure;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract clean text from article elements
|
||||
*/
|
||||
export const extractArticleText = (doc) => {
|
||||
const articleSelectors = [
|
||||
'article',
|
||||
'main article',
|
||||
'[role="main"] article',
|
||||
'.article-content',
|
||||
'.post-content',
|
||||
'.entry-content',
|
||||
'.content-body'
|
||||
];
|
||||
|
||||
// Try to find article container
|
||||
let articleContainer = null;
|
||||
for (const selector of articleSelectors) {
|
||||
articleContainer = doc.querySelector(selector);
|
||||
if (articleContainer) break;
|
||||
}
|
||||
|
||||
// If no article container, try main content area
|
||||
if (!articleContainer) {
|
||||
const mainSelectors = ['main', '[role="main"]', '#main', '#content', '.main-content'];
|
||||
for (const selector of mainSelectors) {
|
||||
articleContainer = doc.querySelector(selector);
|
||||
if (articleContainer) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract text from container or full document
|
||||
const container = articleContainer || doc.body;
|
||||
|
||||
if (!container) return { text: '', elements: [] };
|
||||
|
||||
// Remove unwanted elements
|
||||
const unwantedSelectors = [
|
||||
'script', 'style', 'nav', 'header', 'footer', 'aside',
|
||||
'.navigation', '.nav', '.menu', '.sidebar', '.ads', '.advertisement',
|
||||
'.social-share', '.comments', '.related-posts', '.author-bio'
|
||||
];
|
||||
|
||||
const clone = container.cloneNode(true);
|
||||
unwantedSelectors.forEach(selector => {
|
||||
clone.querySelectorAll(selector).forEach(el => el.remove());
|
||||
});
|
||||
|
||||
// Extract text from meaningful elements
|
||||
const meaningfulElements = clone.querySelectorAll('h1, h2, h3, h4, h5, h6, p, li, blockquote, pre');
|
||||
const elements = Array.from(meaningfulElements).map(el => ({
|
||||
tag: el.tagName.toLowerCase(),
|
||||
text: el.textContent.trim(),
|
||||
length: el.textContent.trim().length
|
||||
})).filter(el => el.length > 0);
|
||||
|
||||
const text = elements.map(el => el.text).join('\n\n');
|
||||
|
||||
return { text, elements };
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract all visible text from page
|
||||
*/
|
||||
export const extractAllText = (doc) => {
|
||||
const clone = doc.body.cloneNode(true);
|
||||
|
||||
// Remove unwanted elements
|
||||
const unwantedSelectors = ['script', 'style', 'noscript'];
|
||||
unwantedSelectors.forEach(selector => {
|
||||
clone.querySelectorAll(selector).forEach(el => el.remove());
|
||||
});
|
||||
|
||||
const text = clone.textContent || clone.innerText || '';
|
||||
return text.replace(/\s+/g, ' ').trim();
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate content quality metrics
|
||||
*/
|
||||
export const calculateContentMetrics = (doc, articleText, allText) => {
|
||||
const metrics = {
|
||||
articleWordCount: articleText.split(/\s+/).filter(w => w.length > 0).length,
|
||||
totalWordCount: allText.split(/\s+/).filter(w => w.length > 0).length,
|
||||
contentRatio: 0,
|
||||
linkCount: doc.querySelectorAll('a[href]').length,
|
||||
imageCount: doc.querySelectorAll('img').length,
|
||||
headingCount: doc.querySelectorAll('h1, h2, h3, h4, h5, h6').length,
|
||||
paragraphCount: doc.querySelectorAll('p').length,
|
||||
linkDensity: 0
|
||||
};
|
||||
|
||||
if (metrics.totalWordCount > 0) {
|
||||
metrics.contentRatio = metrics.articleWordCount / metrics.totalWordCount;
|
||||
metrics.linkDensity = metrics.linkCount / metrics.totalWordCount;
|
||||
}
|
||||
|
||||
return metrics;
|
||||
};
|
||||
|
||||
/**
|
||||
* Classify content type based on structure and metrics
|
||||
*/
|
||||
export const classifyContent = (structure, metrics, articleText) => {
|
||||
const wordCount = metrics.articleWordCount;
|
||||
const contentRatio = metrics.contentRatio;
|
||||
const hasStructure = structure.hasArticleTag || structure.hasMainTag || structure.hasMetaArticle;
|
||||
const hasGoodStructure = structure.headingCount >= 2 && structure.paragraphCount >= 3;
|
||||
|
||||
// Rich Article Content
|
||||
if ((hasStructure || hasGoodStructure) && wordCount >= 300 && contentRatio > 0.6) {
|
||||
return CONTENT_TYPES.RICH_ARTICLE;
|
||||
}
|
||||
|
||||
// General Web Content
|
||||
if (wordCount >= 100 && contentRatio > 0.3) {
|
||||
return CONTENT_TYPES.GENERAL_CONTENT;
|
||||
}
|
||||
|
||||
// Limited Content
|
||||
if (wordCount >= 20) {
|
||||
return CONTENT_TYPES.LIMITED_CONTENT;
|
||||
}
|
||||
|
||||
// No readable content
|
||||
return CONTENT_TYPES.NO_CONTENT;
|
||||
};
|
||||
|
||||
/**
|
||||
* Main function to extract and analyze content from URL
|
||||
*/
|
||||
export const extractContentFromUrl = async (url) => {
|
||||
try {
|
||||
// Fetch content
|
||||
const { html, url: finalUrl, contentType } = await fetchUrlContent(url);
|
||||
|
||||
// Parse HTML
|
||||
const doc = parseHtml(html);
|
||||
|
||||
// Detect article structure
|
||||
const structure = detectArticleStructure(doc);
|
||||
|
||||
// Extract text content
|
||||
const { text: articleText, elements } = extractArticleText(doc);
|
||||
const allText = extractAllText(doc);
|
||||
|
||||
// Calculate metrics
|
||||
const metrics = calculateContentMetrics(doc, articleText, allText);
|
||||
|
||||
// Classify content
|
||||
const contentClassification = classifyContent(structure, metrics, articleText);
|
||||
|
||||
// Get page metadata
|
||||
const title = doc.querySelector('title')?.textContent?.trim() || '';
|
||||
const description = doc.querySelector('meta[name="description"]')?.getAttribute('content') || '';
|
||||
|
||||
return {
|
||||
success: true,
|
||||
url: finalUrl,
|
||||
title,
|
||||
description,
|
||||
contentType: contentClassification,
|
||||
structure,
|
||||
metrics,
|
||||
articleText,
|
||||
allText,
|
||||
elements,
|
||||
extractedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
url
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -22,6 +22,11 @@ module.exports = {
|
||||
},
|
||||
fontFamily: {
|
||||
mono: ['JetBrains Mono', 'Monaco', 'Cascadia Code', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 'Fira Code', 'Droid Sans Mono', 'Courier New', 'monospace'],
|
||||
},
|
||||
maxWidth: {
|
||||
'1/4': '25%',
|
||||
'1/2': '50%',
|
||||
'3/4': '75%',
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
27
temp_export_tabs.js
Normal file
27
temp_export_tabs.js
Normal file
@@ -0,0 +1,27 @@
|
||||
{/* Export Tabs */}
|
||||
<div className="flex border-b border-gray-200 dark:border-gray-700 overflow-x-auto scrollbar-hide">
|
||||
<div className="flex min-w-max">
|
||||
<button
|
||||
onClick={() => setActiveExportTab('json')}
|
||||
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
|
||||
activeExportTab === 'json'
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
|
||||
}`}
|
||||
>
|
||||
<Braces className="h-4 w-4" />
|
||||
JSON
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveExportTab('php')}
|
||||
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium transition-colors ${
|
||||
activeExportTab === 'php'
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 border-b-2 border-blue-500'
|
||||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
|
||||
}`}
|
||||
>
|
||||
<Code className="h-4 w-4" />
|
||||
PHP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user