Initial commit: Text Corruptor PWA

Features:
- Zalgo text generator with adjustable intensity (1-10)
- Real-time text corruption as you type
- Click-to-copy functionality with visual feedback
- Progressive Web App with offline support
- Responsive design for mobile and desktop
- Dark theme with glitch-inspired aesthetics

Technical implementation:
- Pure JavaScript implementation (no frameworks)
- Service Worker for offline functionality
- PWA manifest for installability
- Python development server
- Comprehensive linting setup (ESLint, Prettier, Black, Pylint)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2025-08-18 20:00:58 +02:00
commit 44a2ac4cbd
23 changed files with 1672 additions and 0 deletions

20
.eslintrc.json Normal file
View file

@ -0,0 +1,20 @@
{
"env": {
"browser": true,
"es2021": true,
"serviceworker": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
"indent": ["error", 4],
"linebreak-style": ["error", "unix"],
"quotes": ["error", "single"],
"semi": ["error", "always"],
"no-unused-vars": ["warn"],
"no-console": ["warn", { "allow": ["warn", "error", "log"] }]
}
}

13
.gitignore vendored Normal file
View file

@ -0,0 +1,13 @@
node_modules/
*.pyc
__pycache__/
.DS_Store
*.log
.env
.vscode/
.idea/
*.swp
*.swo
*~
dist/
build/

10
.prettierrc Normal file
View file

@ -0,0 +1,10 @@
{
"printWidth": 100,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "avoid"
}

114
CLAUDE.md Normal file
View file

@ -0,0 +1,114 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Text Corruptor is a Progressive Web App (PWA) that generates zalgo text (corrupted/glitched text using Unicode combining characters). The app features real-time text corruption with adjustable intensity and click-to-copy functionality.
## Project Structure
```
zalgo/
├── app/ # Main application directory (served as document root)
│ ├── index.html # Main HTML page
│ ├── zalgo.js # Zalgo text generation logic
│ ├── app.js # Main application logic and PWA registration
│ ├── styles.css # Application styles
│ ├── sw.js # Service worker for offline functionality
│ ├── manifest.json # PWA manifest
│ ├── icons/ # PWA icons (SVG placeholders)
│ ├── create-icons.js # Node.js script to generate PNG icons
│ └── generate-icons.html # Browser-based icon generator
├── server.py # Development server (serves app/ as root)
├── package.json # Node dependencies and scripts
├── bun.lockb # Bun lockfile
├── .eslintrc.json # ESLint configuration
└── .prettierrc # Prettier configuration
```
## Development Commands
### Running the Application
```bash
# Start development server (serves on http://localhost:8000/)
python3 server.py
# Or using the npm script
bun run serve
```
### Code Quality and Linting
```bash
# Lint JavaScript files
bun run lint:js
# Lint and auto-fix JavaScript
bun run lint:js:fix
# Format all code with Prettier
bun run format
# Check formatting without changes
bun run check
# Lint Python code
black server.py
pylint server.py
```
### Generating Icons
```bash
# Install canvas package first (optional, for PNG generation)
bun add canvas
# Generate PNG icons programmatically
node app/create-icons.js
# Or open app/generate-icons.html in browser and manually save icons
```
## Architecture and Key Components
### Zalgo Text Generation (zalgo.js)
- **ZalgoGenerator class**: Core text corruption engine
- Uses Unicode combining diacritical marks (above, middle, below)
- Intensity scale 1-10 controls corruption amount
- `generate()`: Creates zalgo text from input
- `clean()`: Removes corruption from text
### Application Logic (app.js)
- Real-time text corruption as user types
- Click-to-copy functionality with visual feedback
- Keyboard shortcuts (Ctrl+K to clear)
- PWA installation handling
- Service worker registration
### PWA Implementation
- **Service Worker (sw.js)**: Enables offline functionality with cache-first strategy
- **Manifest (manifest.json)**: PWA configuration for installability
- Caches all essential files for offline use
- Automatic cache cleanup on update
### Styling (styles.css)
- Dark theme with glitch-inspired aesthetics
- Responsive design for mobile and desktop
- CSS animations for glitch effects
- Print-friendly styles
## Deployment Notes
- The `app/` directory should be served as the document root
- All paths in the app are relative to assume `app/` is the root
- Service worker requires HTTPS in production (except localhost)
- Icons are currently SVG placeholders; generate PNGs for production
## Testing Checklist
When making changes, ensure:
1. Text corruption works with various Unicode characters
2. Copy functionality works on both desktop and mobile
3. PWA installs correctly
4. Offline mode functions properly
5. Responsive design works on all screen sizes
6. All linting passes (JavaScript, Python, HTML validation)

122
app/app.js Normal file
View file

@ -0,0 +1,122 @@
/**
* Main application logic for Text Corruptor
* Handles UI interactions and PWA registration
*/
/* global ZalgoGenerator */
// Initialize zalgo generator
const zalgo = new ZalgoGenerator();
// DOM elements
const inputText = document.getElementById('inputText');
const outputText = document.getElementById('outputText');
const intensitySlider = document.getElementById('intensity');
const intensityValue = document.getElementById('intensityValue');
const clearBtn = document.getElementById('clearBtn');
const copyNotification = document.getElementById('copyNotification');
/**
* Update the corrupted text output
*/
function updateOutput() {
const text = inputText.value;
const intensity = parseInt(intensitySlider.value);
if (text) {
outputText.value = zalgo.generate(text, intensity);
} else {
outputText.value = '';
}
}
/**
* Copy text to clipboard and show notification
*/
async function copyToClipboard() {
if (!outputText.value) return;
try {
await navigator.clipboard.writeText(outputText.value);
// Show copy notification
copyNotification.classList.add('show');
setTimeout(() => {
copyNotification.classList.remove('show');
}, 2000);
} catch (err) {
// Fallback for older browsers
outputText.select();
document.execCommand('copy');
// Show copy notification
copyNotification.classList.add('show');
setTimeout(() => {
copyNotification.classList.remove('show');
}, 2000);
}
}
/**
* Clear all text fields
*/
function clearAll() {
inputText.value = '';
outputText.value = '';
inputText.focus();
}
// Event listeners
inputText.addEventListener('input', updateOutput);
intensitySlider.addEventListener('input', () => {
intensityValue.textContent = intensitySlider.value;
updateOutput();
});
outputText.addEventListener('click', copyToClipboard);
clearBtn.addEventListener('click', clearAll);
// Handle keyboard shortcuts
document.addEventListener('keydown', e => {
// Ctrl/Cmd + K to clear
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
clearAll();
}
// Ctrl/Cmd + C when output is focused to copy
if ((e.ctrlKey || e.metaKey) && e.key === 'c' && document.activeElement === outputText) {
copyToClipboard();
}
});
// Register service worker for PWA functionality
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('sw.js')
.then(registration => {
console.log('Service Worker registered successfully:', registration.scope);
})
.catch(error => {
console.log('Service Worker registration failed:', error);
});
});
}
// Handle install prompt for PWA
window.addEventListener('beforeinstallprompt', e => {
// Prevent the mini-infobar from appearing on mobile
e.preventDefault();
// Store the event so it can be triggered later if needed
// Currently not used but kept for potential future install button
window.deferredPrompt = e;
console.log('Install prompt ready');
});
// Focus input on load
window.addEventListener('load', () => {
inputText.focus();
});

96
app/create-icons.js Normal file
View file

@ -0,0 +1,96 @@
/**
* Node.js script to generate PWA icons programmatically
* Run with: node create-icons.js
* Requires: npm install canvas
*/
/* eslint-env node */
/* eslint-disable no-undef */
const fs = require('fs');
const path = require('path');
// Check if we're in a browser or Node.js environment
const isBrowser = typeof window !== 'undefined';
if (!isBrowser) {
// Node.js environment - use canvas package
try {
const { createCanvas } = require('canvas');
// Icon sizes needed for PWA
const sizes = [72, 96, 128, 144, 152, 192, 384, 512];
// Create icons directory if it doesn't exist
const iconsDir = path.join(__dirname, 'icons');
if (!fs.existsSync(iconsDir)) {
fs.mkdirSync(iconsDir, { recursive: true });
}
const createIcon = function (size) {
const canvas = createCanvas(size, size);
const ctx = canvas.getContext('2d');
// Background gradient (solid color fallback for canvas)
ctx.fillStyle = '#1a1a2e';
ctx.fillRect(0, 0, size, size);
// Center circle
const centerX = size / 2;
const centerY = size / 2;
const radius = size * 0.35;
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
ctx.fillStyle = '#00ff88';
ctx.fill();
// Letter "T"
ctx.font = `bold ${size * 0.4}px Arial`;
ctx.fillStyle = '#0f0f14';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('T', centerX, centerY);
return canvas;
};
// Generate all icons
sizes.forEach(size => {
const canvas = createIcon(size);
const buffer = canvas.toBuffer('image/png');
const filename = path.join(iconsDir, `icon-${size}.png`);
fs.writeFileSync(filename, buffer);
console.log(`Created ${filename}`);
});
console.log('All icons created successfully!');
} catch (error) {
console.log('Canvas package not installed. Creating placeholder icons instead...');
// Fallback: Create placeholder SVG icons
const sizes = [72, 96, 128, 144, 152, 192, 384, 512];
const iconsDir = path.join(__dirname, 'icons');
if (!fs.existsSync(iconsDir)) {
fs.mkdirSync(iconsDir, { recursive: true });
}
sizes.forEach(size => {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
<rect width="${size}" height="${size}" fill="#1a1a2e"/>
<circle cx="${size / 2}" cy="${size / 2}" r="${size * 0.35}" fill="#00ff88"/>
<text x="${size / 2}" y="${size / 2}" font-family="Arial" font-size="${size * 0.4}" font-weight="bold" text-anchor="middle" dominant-baseline="middle" fill="#0f0f14">T</text>
</svg>`;
const filename = path.join(iconsDir, `icon-${size}.svg`);
fs.writeFileSync(filename, svg);
console.log(`Created ${filename} (SVG placeholder)`);
});
console.log('\nNote: SVG placeholders created. For PNG icons, install canvas package:');
console.log('npm install canvas');
}
} else {
console.log('This script should be run in Node.js, not in a browser.');
}

156
app/generate-icons.html Normal file
View file

@ -0,0 +1,156 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Generate PWA Icons</title>
</head>
<body>
<h1>Icon Generator for Text Corruptor</h1>
<p>Right-click each canvas and save as PNG to the icons folder</p>
<div id="canvases"></div>
<script>
// Icon sizes needed for PWA
const sizes = [72, 96, 128, 144, 152, 192, 384, 512];
function createIcon(size) {
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
// Background gradient
const gradient = ctx.createLinearGradient(0, 0, size, size);
gradient.addColorStop(0, '#1a1a2e');
gradient.addColorStop(1, '#0f0f14');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, size, size);
// Add glitch lines effect
ctx.strokeStyle = 'rgba(0, 255, 136, 0.1)';
ctx.lineWidth = 1;
for (let i = 0; i < size; i += 4) {
ctx.beginPath();
ctx.moveTo(0, i);
ctx.lineTo(size, i);
ctx.stroke();
}
// Center circle background
const centerX = size / 2;
const centerY = size / 2;
const radius = size * 0.35;
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
const circleGradient = ctx.createRadialGradient(
centerX,
centerY,
0,
centerX,
centerY,
radius
);
circleGradient.addColorStop(0, '#00ff88');
circleGradient.addColorStop(1, '#00cc6a');
ctx.fillStyle = circleGradient;
ctx.fill();
// Add corrupted "T" letter with zalgo effect simulation
ctx.font = `bold ${size * 0.4}px Arial`;
ctx.fillStyle = '#0f0f14';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Main letter
ctx.fillText('T', centerX, centerY);
// Simulated zalgo corruption marks
ctx.fillStyle = 'rgba(15, 15, 20, 0.5)';
const corruption = ['̴', '̸', '̶', '̷', '̵'];
const offsetX = size * 0.02;
const offsetY = size * 0.02;
// Add small corruption marks around the letter
for (let i = 0; i < 3; i++) {
const x = centerX + (Math.random() - 0.5) * offsetX * 2;
const y = centerY + (Math.random() - 0.5) * offsetY * 2;
ctx.font = `${size * 0.1}px Arial`;
ctx.fillText('░', x, y - size * 0.15);
ctx.fillText('▒', x + offsetX, y + size * 0.15);
}
// Add glitch effect
ctx.strokeStyle = '#ff3366';
ctx.lineWidth = 1;
ctx.globalAlpha = 0.3;
ctx.strokeText('T', centerX - 2, centerY);
ctx.strokeStyle = '#00ffff';
ctx.strokeText('T', centerX + 2, centerY);
ctx.globalAlpha = 1;
// Add label
const label = document.createElement('div');
label.textContent = `icon-${size}.png`;
label.style.marginTop = '10px';
label.style.marginBottom = '20px';
const container = document.createElement('div');
container.appendChild(canvas);
container.appendChild(label);
return container;
}
// Generate all icons
const container = document.getElementById('canvases');
sizes.forEach(size => {
container.appendChild(createIcon(size));
});
// Auto-download function (optional - uncomment to use)
/*
function downloadCanvas(canvas, filename) {
canvas.toBlob(blob => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
});
}
// Auto-download all icons
setTimeout(() => {
const canvases = document.querySelectorAll('canvas');
canvases.forEach((canvas, index) => {
setTimeout(() => {
downloadCanvas(canvas, `icon-${sizes[index]}.png`);
}, index * 500);
});
}, 1000);
*/
</script>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
background: #f0f0f0;
}
canvas {
border: 1px solid #ccc;
display: block;
background: white;
}
#canvases > div {
display: inline-block;
margin: 10px;
text-align: center;
}
</style>
</body>
</html>

5
app/icons/icon-128.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
<rect width="128" height="128" fill="#1a1a2e"/>
<circle cx="64" cy="64" r="44.8" fill="#00ff88"/>
<text x="64" y="64" font-family="Arial" font-size="51.2" font-weight="bold" text-anchor="middle" dominant-baseline="middle" fill="#0f0f14">T</text>
</svg>

After

Width:  |  Height:  |  Size: 400 B

5
app/icons/icon-144.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="144" height="144" viewBox="0 0 144 144">
<rect width="144" height="144" fill="#1a1a2e"/>
<circle cx="72" cy="72" r="50.4" fill="#00ff88"/>
<text x="72" y="72" font-family="Arial" font-size="57.6" font-weight="bold" text-anchor="middle" dominant-baseline="middle" fill="#0f0f14">T</text>
</svg>

After

Width:  |  Height:  |  Size: 400 B

5
app/icons/icon-152.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="152" height="152" viewBox="0 0 152 152">
<rect width="152" height="152" fill="#1a1a2e"/>
<circle cx="76" cy="76" r="53.199999999999996" fill="#00ff88"/>
<text x="76" y="76" font-family="Arial" font-size="60.800000000000004" font-weight="bold" text-anchor="middle" dominant-baseline="middle" fill="#0f0f14">T</text>
</svg>

After

Width:  |  Height:  |  Size: 428 B

5
app/icons/icon-192.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 192 192">
<rect width="192" height="192" fill="#1a1a2e"/>
<circle cx="96" cy="96" r="67.19999999999999" fill="#00ff88"/>
<text x="96" y="96" font-family="Arial" font-size="76.80000000000001" font-weight="bold" text-anchor="middle" dominant-baseline="middle" fill="#0f0f14">T</text>
</svg>

After

Width:  |  Height:  |  Size: 426 B

5
app/icons/icon-384.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="384" height="384" viewBox="0 0 384 384">
<rect width="384" height="384" fill="#1a1a2e"/>
<circle cx="192" cy="192" r="134.39999999999998" fill="#00ff88"/>
<text x="192" y="192" font-family="Arial" font-size="153.60000000000002" font-weight="bold" text-anchor="middle" dominant-baseline="middle" fill="#0f0f14">T</text>
</svg>

After

Width:  |  Height:  |  Size: 432 B

5
app/icons/icon-512.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<rect width="512" height="512" fill="#1a1a2e"/>
<circle cx="256" cy="256" r="179.2" fill="#00ff88"/>
<text x="256" y="256" font-family="Arial" font-size="204.8" font-weight="bold" text-anchor="middle" dominant-baseline="middle" fill="#0f0f14">T</text>
</svg>

After

Width:  |  Height:  |  Size: 406 B

5
app/icons/icon-72.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" viewBox="0 0 72 72">
<rect width="72" height="72" fill="#1a1a2e"/>
<circle cx="36" cy="36" r="25.2" fill="#00ff88"/>
<text x="36" y="36" font-family="Arial" font-size="28.8" font-weight="bold" text-anchor="middle" dominant-baseline="middle" fill="#0f0f14">T</text>
</svg>

After

Width:  |  Height:  |  Size: 394 B

5
app/icons/icon-96.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">
<rect width="96" height="96" fill="#1a1a2e"/>
<circle cx="48" cy="48" r="33.599999999999994" fill="#00ff88"/>
<text x="48" y="48" font-family="Arial" font-size="38.400000000000006" font-weight="bold" text-anchor="middle" dominant-baseline="middle" fill="#0f0f14">T</text>
</svg>

After

Width:  |  Height:  |  Size: 422 B

66
app/index.html Normal file
View file

@ -0,0 +1,66 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Transform your text into corrupted zalgo text with one click. Perfect for creating glitchy, distorted text effects."
/>
<meta name="theme-color" content="#1a1a2e" />
<title>Text Corruptor - Zalgo Text Generator</title>
<link rel="manifest" href="manifest.json" />
<link rel="icon" type="image/png" sizes="192x192" href="icons/icon-192.png" />
<link rel="icon" type="image/png" sizes="512x512" href="icons/icon-512.png" />
<link rel="apple-touch-icon" href="icons/icon-192.png" />
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="container">
<header>
<h1>Text Corruptor</h1>
<p class="subtitle">Transform your text into corrupted chaos</p>
</header>
<main>
<div class="controls">
<label for="intensity">Corruption Intensity:</label>
<input type="range" id="intensity" min="1" max="10" value="5" />
<span id="intensityValue">5</span>
</div>
<div class="text-area-container">
<label for="inputText">Original Text</label>
<textarea
id="inputText"
placeholder="Enter your text here..."
rows="6"
autocomplete="off"
spellcheck="false"
></textarea>
</div>
<div class="text-area-container">
<label for="outputText">Corrupted Text</label>
<textarea
id="outputText"
placeholder="Your corrupted text will appear here..."
rows="6"
readonly
title="Click to copy"
></textarea>
<div id="copyNotification" class="copy-notification">Copied!</div>
</div>
<button id="clearBtn" class="clear-btn">Clear All</button>
</main>
<footer>
<p>Click the corrupted text to copy it to your clipboard</p>
</footer>
</div>
<script src="zalgo.js"></script>
<script src="app.js"></script>
</body>
</html>

62
app/manifest.json Normal file
View file

@ -0,0 +1,62 @@
{
"name": "Text Corruptor - Zalgo Text Generator",
"short_name": "Text Corruptor",
"description": "Transform your text into corrupted zalgo text with adjustable intensity",
"start_url": "/",
"display": "standalone",
"background_color": "#0f0f14",
"theme_color": "#1a1a2e",
"orientation": "portrait-primary",
"icons": [
{
"src": "icons/icon-72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "icons/icon-96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "icons/icon-128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "icons/icon-144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "icons/icon-152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "icons/icon-384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"categories": ["utilities", "productivity"],
"screenshots": [
{
"src": "screenshots/screenshot1.png",
"type": "image/png",
"sizes": "1280x720"
}
]
}

344
app/styles.css Normal file
View file

@ -0,0 +1,344 @@
/**
* Styles for Text Corruptor
* Dark theme with glitch-inspired aesthetics
*/
:root {
--bg-primary: #0f0f14;
--bg-secondary: #1a1a2e;
--bg-tertiary: #16213e;
--text-primary: #eaeaea;
--text-secondary: #a8a8b3;
--accent: #00ff88;
--accent-hover: #00cc6a;
--error: #ff3366;
--border: #2a2a3e;
--shadow: rgba(0, 255, 136, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background-image: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
rgba(0, 255, 136, 0.03) 2px,
rgba(0, 255, 136, 0.03) 4px
);
}
.container {
width: 100%;
max-width: 800px;
background: var(--bg-secondary);
border-radius: 16px;
padding: 40px;
box-shadow:
0 10px 40px rgba(0, 0, 0, 0.5),
0 0 100px var(--shadow);
border: 1px solid var(--border);
}
header {
text-align: center;
margin-bottom: 40px;
}
h1 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 10px;
background: linear-gradient(135deg, var(--accent), #00ffff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 40px var(--shadow);
animation: glitch 3s infinite;
}
@keyframes glitch {
0%,
100% {
text-shadow: 0 0 40px var(--shadow);
}
25% {
text-shadow:
-2px 0 var(--error),
2px 0 var(--accent);
}
50% {
text-shadow:
2px 0 var(--error),
-2px 0 var(--accent);
}
75% {
text-shadow: 0 0 40px var(--shadow);
}
}
.subtitle {
color: var(--text-secondary);
font-size: 1.1rem;
}
.controls {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 30px;
padding: 20px;
background: var(--bg-tertiary);
border-radius: 12px;
border: 1px solid var(--border);
}
.controls label {
color: var(--text-secondary);
font-size: 0.95rem;
}
#intensity {
flex: 1;
-webkit-appearance: none;
appearance: none;
height: 6px;
background: var(--border);
border-radius: 3px;
outline: none;
transition: all 0.3s ease;
}
#intensity::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
background: var(--accent);
border-radius: 50%;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 0 10px var(--shadow);
}
#intensity::-moz-range-thumb {
width: 20px;
height: 20px;
background: var(--accent);
border-radius: 50%;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 0 10px var(--shadow);
}
#intensity:hover::-webkit-slider-thumb {
transform: scale(1.2);
box-shadow: 0 0 20px var(--shadow);
}
#intensity:hover::-moz-range-thumb {
transform: scale(1.2);
box-shadow: 0 0 20px var(--shadow);
}
#intensityValue {
min-width: 30px;
text-align: center;
font-weight: 600;
color: var(--accent);
font-size: 1.1rem;
}
.text-area-container {
margin-bottom: 25px;
position: relative;
}
.text-area-container label {
display: block;
margin-bottom: 10px;
color: var(--text-secondary);
font-size: 0.95rem;
text-transform: uppercase;
letter-spacing: 1px;
}
textarea {
width: 100%;
min-height: 150px;
padding: 15px;
background: var(--bg-tertiary);
border: 2px solid var(--border);
border-radius: 12px;
color: var(--text-primary);
font-size: 1rem;
font-family: 'Courier New', monospace;
resize: vertical;
outline: none;
transition: all 0.3s ease;
}
textarea:focus {
border-color: var(--accent);
box-shadow: 0 0 20px var(--shadow);
}
#outputText {
cursor: pointer;
position: relative;
}
#outputText:hover {
background: rgba(0, 255, 136, 0.05);
}
.copy-notification {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--accent);
color: var(--bg-primary);
padding: 12px 24px;
border-radius: 8px;
font-weight: 600;
opacity: 0;
pointer-events: none;
transition: all 0.3s ease;
z-index: 10;
}
.copy-notification.show {
opacity: 1;
animation: pulse 0.5s ease;
}
@keyframes pulse {
0% {
transform: translate(-50%, -50%) scale(0.8);
}
50% {
transform: translate(-50%, -50%) scale(1.1);
}
100% {
transform: translate(-50%, -50%) scale(1);
}
}
.clear-btn {
width: 100%;
padding: 15px;
background: transparent;
border: 2px solid var(--accent);
border-radius: 12px;
color: var(--accent);
font-size: 1rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 20px;
}
.clear-btn:hover {
background: var(--accent);
color: var(--bg-primary);
box-shadow: 0 0 30px var(--shadow);
transform: translateY(-2px);
}
.clear-btn:active {
transform: translateY(0);
}
footer {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid var(--border);
}
footer p {
color: var(--text-secondary);
font-size: 0.9rem;
}
/* Mobile responsiveness */
@media (max-width: 600px) {
.container {
padding: 25px;
}
h1 {
font-size: 2rem;
}
.controls {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
textarea {
min-height: 120px;
font-size: 0.95rem;
}
}
/* Glitch effect for corrupted text */
#outputText {
animation: subtle-glitch 5s infinite;
}
@keyframes subtle-glitch {
0%,
100% {
filter: none;
}
92% {
filter: none;
}
93% {
filter: drop-shadow(-2px 0 var(--error)) drop-shadow(2px 0 var(--accent));
}
94% {
filter: none;
}
}
/* Loading state */
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Print styles */
@media print {
body {
background: white;
color: black;
}
.container {
box-shadow: none;
border: 1px solid #ccc;
}
.clear-btn {
display: none;
}
}

90
app/sw.js Normal file
View file

@ -0,0 +1,90 @@
/**
* Service Worker for Text Corruptor PWA
* Enables offline functionality and caching
*/
const CACHE_NAME = 'text-corruptor-v1';
const urlsToCache = ['/', '/index.html', '/styles.css', '/zalgo.js', '/app.js', '/manifest.json'];
// Install event - cache essential files
self.addEventListener('install', event => {
event.waitUntil(
caches
.open(CACHE_NAME)
.then(cache => {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
.then(() => {
// Force the service worker to become active immediately
return self.skipWaiting();
})
);
});
// Fetch event - serve from cache when offline
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
// Cache hit - return response
if (response) {
return response;
}
// Clone the request because it's a stream
const fetchRequest = event.request.clone();
return fetch(fetchRequest)
.then(response => {
// Check if we received a valid response
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Clone the response because it's a stream
const responseToCache = response.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, responseToCache);
});
return response;
})
.catch(() => {
// Network request failed, serve offline fallback if available
return caches.match('/index.html');
});
})
);
});
// Activate event - clean up old caches
self.addEventListener('activate', event => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches
.keys()
.then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
.then(() => {
// Ensure the service worker takes control immediately
return self.clients.claim();
})
);
});
// Handle messages from the client
self.addEventListener('message', event => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});

267
app/zalgo.js Normal file
View file

@ -0,0 +1,267 @@
/**
* Zalgo Text Generator
* Generates corrupted/glitched text using Unicode combining characters
*/
/* exported ZalgoGenerator */
class ZalgoGenerator {
constructor() {
// Combining diacritical marks that appear above characters
this.above = [
'\u0300',
'\u0301',
'\u0302',
'\u0303',
'\u0304',
'\u0305',
'\u0306',
'\u0307',
'\u0308',
'\u0309',
'\u030A',
'\u030B',
'\u030C',
'\u030D',
'\u030E',
'\u030F',
'\u0310',
'\u0311',
'\u0312',
'\u0313',
'\u0314',
'\u0315',
'\u0316',
'\u0317',
'\u0318',
'\u0319',
'\u031A',
'\u031B',
'\u031C',
'\u031D',
'\u031E',
'\u031F',
'\u0320',
'\u0321',
'\u0322',
'\u0323',
'\u0324',
'\u0325',
'\u0326',
'\u0327',
'\u0328',
'\u0329',
'\u032A',
'\u032B',
'\u032C',
'\u032D',
'\u032E',
'\u032F',
'\u0330',
'\u0331',
'\u0332',
'\u0333',
'\u0334',
'\u0335',
'\u0336',
'\u0337',
'\u0338',
'\u0339',
'\u033A',
'\u033B',
'\u033C',
'\u033D',
'\u033E',
'\u033F',
'\u0340',
'\u0341',
'\u0342',
'\u0343',
'\u0344',
'\u0345',
'\u0346',
'\u0347',
'\u0348',
'\u0349',
'\u034A',
'\u034B',
'\u034C',
'\u034D',
'\u034E',
'\u034F',
'\u0350',
'\u0351',
'\u0352',
'\u0353',
'\u0354',
'\u0355',
'\u0356',
'\u0357',
'\u0358',
'\u0359',
'\u035A',
'\u035B',
'\u035C',
'\u035D',
'\u035E',
'\u035F',
'\u0360',
'\u0361',
'\u0362',
'\u0363',
'\u0364',
'\u0365',
'\u0366',
'\u0367',
'\u0368',
'\u0369',
'\u036A',
'\u036B',
'\u036C',
'\u036D',
'\u036E',
'\u036F',
];
// Combining diacritical marks that appear in the middle
this.middle = [
'\u0315',
'\u031B',
'\u0340',
'\u0341',
'\u0358',
'\u0321',
'\u0322',
'\u0327',
'\u0328',
'\u0334',
'\u0335',
'\u0336',
'\u034F',
'\u035C',
'\u035D',
'\u035E',
'\u035F',
'\u0360',
'\u0362',
'\u0338',
'\u0337',
'\u0361',
'\u0489',
];
// Combining diacritical marks that appear below characters
this.below = [
'\u0316',
'\u0317',
'\u0318',
'\u0319',
'\u031C',
'\u031D',
'\u031E',
'\u031F',
'\u0320',
'\u0324',
'\u0325',
'\u0326',
'\u0329',
'\u032A',
'\u032B',
'\u032C',
'\u032D',
'\u032E',
'\u032F',
'\u0330',
'\u0331',
'\u0332',
'\u0333',
'\u0339',
'\u033A',
'\u033B',
'\u033C',
'\u0345',
'\u0347',
'\u0348',
'\u0349',
'\u034D',
'\u034E',
'\u0353',
'\u0354',
'\u0355',
'\u0356',
'\u0359',
'\u035A',
'\u0323',
];
}
/**
* Get a random character from an array
*/
randomChar(array) {
return array[Math.floor(Math.random() * array.length)];
}
/**
* Generate a specified number of random characters from an array
*/
randomChars(array, count) {
let result = '';
for (let i = 0; i < count; i++) {
result += this.randomChar(array);
}
return result;
}
/**
* Generate zalgo text with specified intensity
* @param {string} text - The input text to corrupt
* @param {number} intensity - Corruption intensity (1-10)
* @returns {string} - The corrupted zalgo text
*/
generate(text, intensity = 5) {
if (!text) return '';
// Normalize intensity to 1-10 range
intensity = Math.max(1, Math.min(10, intensity));
let result = '';
for (let char of text) {
result += char;
// Skip whitespace and newlines
if (/\s/.test(char)) {
continue;
}
// Calculate how many combining characters to add based on intensity
// Higher intensity = more corruption
const factor = intensity / 10;
// Add characters above (0-3 based on intensity)
const aboveCount = Math.floor(Math.random() * (4 * factor));
result += this.randomChars(this.above, aboveCount);
// Add characters in middle (0-2 based on intensity)
const middleCount = Math.floor(Math.random() * (3 * factor));
result += this.randomChars(this.middle, middleCount);
// Add characters below (0-3 based on intensity)
const belowCount = Math.floor(Math.random() * (4 * factor));
result += this.randomChars(this.below, belowCount);
}
return result;
}
/**
* Remove zalgo corruption from text
* @param {string} text - The corrupted text
* @returns {string} - Clean text without combining characters
*/
clean(text) {
// Remove all combining characters (Unicode category Mn)
return text.replace(/[\u0300-\u036f\u0489]/g, '');
}
}

216
bun.lock Normal file
View file

@ -0,0 +1,216 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "text-corruptor",
"devDependencies": {
"eslint": "^8.57.0",
"prettier": "^3.2.5",
"vnu-jar": "^24.10.17",
},
},
},
"packages": {
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
"@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="],
"@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="],
"@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="],
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
"@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
"doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="],
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="],
"eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="],
"eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="],
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
"file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="],
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
"flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="],
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="],
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"vnu-jar": ["vnu-jar@24.10.17", "", {}, "sha512-YT7gNrRY5PiJrI1GavlWRHWIwqq2o52COc6J9QeXPfoldKRiZ9BeGP4shNLLaVfi0naA+/LMksdYWkKCr4pnVg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
}
}

17
package.json Normal file
View file

@ -0,0 +1,17 @@
{
"name": "text-corruptor",
"version": "1.0.0",
"description": "Zalgo text generator PWA",
"scripts": {
"serve": "python3 server.py",
"lint:js": "eslint app/*.js",
"lint:js:fix": "eslint app/*.js --fix",
"format": "prettier --write app/*.{js,html,css}",
"check": "prettier --check app/*.{js,html,css}"
},
"devDependencies": {
"eslint": "^8.57.0",
"prettier": "^3.2.5",
"vnu-jar": "^24.10.17"
}
}

39
server.py Normal file
View file

@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""
Simple HTTP server for testing the Text Corruptor PWA
Run with: python3 server.py
Then open http://localhost:8000/ in your browser
"""
import http.server
import socketserver
import os
PORT = 8000
# Serve from the app directory
DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "app")
class MyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
"""Custom HTTP request handler for serving the Text Corruptor PWA."""
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=DIRECTORY, **kwargs)
def end_headers(self):
# Add headers for PWA and CORS
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
self.send_header("Service-Worker-Allowed", "/")
self.send_header("X-Content-Type-Options", "nosniff")
super().end_headers()
if __name__ == "__main__":
with socketserver.TCPServer(("", PORT), MyHTTPRequestHandler) as httpd:
print(f"Server running at http://localhost:{PORT}/")
print(f"Serving files from: {DIRECTORY}")
print("Press Ctrl+C to stop the server")
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\nServer stopped.")