refactor: migrate to modular tool-based architecture

- Implement tool registry system with individual tool modules
- Reorganize transformers into categorized source modules
- Remove emojiLibrary.js, consolidate into EmojiUtils and emojiData
- Fix mobile close button and tooltip functionality
- Add build system for transforms and emoji data
- Migrate from Python backend to pure JavaScript
- Add comprehensive documentation and testing
- Improve code organization and maintainability
- Ignore generated files (transforms-bundle.js, emojiData.js)
This commit is contained in:
Dustin Farley
2025-12-02 20:26:32 -08:00
parent 105084437a
commit dc10a90851
146 changed files with 12712 additions and 8171 deletions
+96
View File
@@ -0,0 +1,96 @@
/**
* Decode Tool - Universal decoder tool
*/
class DecodeTool extends Tool {
constructor() {
super({
id: 'decoder',
name: 'Decoder',
icon: 'fa-key',
title: 'Universal Decoder (D)',
order: 2
});
}
getVueData() {
return {
decoderInput: '',
decoderOutput: '',
decoderResult: null,
selectedDecoder: 'auto'
};
}
getVueMethods() {
return {
getAllTransformsWithReverse: function() {
return this.transforms.filter(t => t && typeof t.reverse === 'function');
},
runUniversalDecode: function() {
const input = this.decoderInput;
if (!input) {
this.decoderOutput = '';
this.decoderResult = null;
return;
}
let result = null;
if (this.selectedDecoder !== 'auto') {
const selectedTransform = this.transforms.find(t => t.name === this.selectedDecoder);
if (selectedTransform && selectedTransform.reverse) {
try {
const decoded = selectedTransform.reverse(input);
if (decoded && decoded !== input) {
result = {
text: decoded,
method: selectedTransform.name,
alternatives: []
};
}
} catch (e) {
console.error(`Error using manual decoder ${this.selectedDecoder}:`, e);
}
}
} else {
result = window.universalDecode(input, {
activeTab: this.activeTab,
activeTransform: this.activeTransform
});
}
this.decoderResult = result;
this.decoderOutput = result ? result.text : '';
},
useAlternative: function(alternative) {
if (alternative && alternative.text) {
this.decoderOutput = alternative.text;
this.decoderResult = {
method: alternative.method,
text: alternative.text,
alternatives: this.decoderResult.alternatives.filter(a => a.method !== alternative.method)
};
}
}
};
}
getVueWatchers() {
return {
decoderInput() {
this.runUniversalDecode();
}
};
}
}
// Export
if (typeof module !== 'undefined' && module.exports) {
module.exports = DecodeTool;
} else {
window.DecodeTool = DecodeTool;
}
+398
View File
@@ -0,0 +1,398 @@
/**
* Emoji Tool - Steganography/Emoji encoding tool
*/
class EmojiTool extends Tool {
constructor() {
super({
id: 'steganography',
name: 'Emoji',
icon: 'fa-smile',
title: 'Hide text in emojis (H)',
order: 3
});
}
getVueData() {
const allEmojis = window.EmojiUtils ? window.EmojiUtils.getAllEmojis() : [];
return {
emojiMessage: '',
encodedMessage: '',
decodeInput: '',
decodedMessage: '',
selectedCarrier: null,
activeSteg: null,
carriers: window.steganography.carriers,
filteredEmojis: [...allEmojis],
selectedEmoji: null,
carrierEmojiList: [...allEmojis],
compatibleEmojis: [],
quickCarrierEmojis: ['🐍','🐉','🐲','🔥','💥','🗿','⚓','⭐','✨','🚀','💀','🪨','🍃','🪶','🔮','🐢','🐊','🦎']
};
}
getVueMethods() {
const self = this;
return {
async initializeEmojiList() {
if (!window.EmojiUtils) {
console.warn('EmojiUtils not available');
return;
}
this.showNotification('Checking emoji compatibility...', 'info', 'fas fa-spinner fa-spin');
const progressCallback = (tested, total, compatible) => {
if (tested % 500 === 0 || tested === total) {
const percent = ((tested / total) * 100).toFixed(0);
console.log(`Emoji compatibility: ${percent}% (${compatible} compatible so far)`);
}
};
const compatible = await window.EmojiUtils.getCompatibleEmojis(progressCallback);
this.compatibleEmojis = compatible;
this.filteredEmojis = [...compatible];
this.carrierEmojiList = [...compatible];
this.emojiListInitialized = true;
this.showNotification(`${compatible.length} compatible emojis loaded`, 'success', 'fas fa-check');
if (this.activeTab === 'steganography') {
this.$nextTick(() => {
this.renderEmojiGrid();
});
}
},
selectCarrier: function(carrier) {
if (this.selectedCarrier === carrier) {
this.selectedCarrier = null;
this.encodedMessage = '';
} else {
this.selectedCarrier = carrier;
this.activeSteg = 'emoji';
this.autoEncode();
}
},
setStegMode: function(mode) {
if (mode === 'invisible') {
this.activeSteg = mode;
this.selectedCarrier = null;
this.autoEncode();
if (this.encodedMessage) {
this.$nextTick(() => {
this.forceCopyToClipboard(this.encodedMessage);
this.showNotification('Invisible text created and copied!', 'success', 'fas fa-check');
});
}
} else {
if (this.activeSteg === mode) {
this.activeSteg = null;
this.encodedMessage = '';
} else {
this.activeSteg = mode;
this.autoEncode();
}
}
},
autoEncode: function() {
if (!this.emojiMessage || this.activeTab !== 'steganography') {
this.encodedMessage = '';
return;
}
if (this.activeSteg === 'invisible') {
this.encodedMessage = window.steganography.encodeInvisible(this.emojiMessage);
} else if (this.selectedCarrier) {
this.encodedMessage = window.steganography.encodeEmoji(
this.selectedCarrier.emoji,
this.emojiMessage
);
}
},
selectEmoji: function(emoji) {
const emojiStr = String(emoji);
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(emojiStr)
.then(() => {
this.showNotification('Emoji copied!', 'success', 'fas fa-check');
this.addToCopyHistory('Emoji', emojiStr);
})
.catch(err => {
console.warn('Emoji clipboard API failed:', err);
this.forceCopyToClipboard(emojiStr);
this.showNotification('Emoji copied!', 'success', 'fas fa-check');
});
} else {
this.forceCopyToClipboard(emojiStr);
this.showNotification('Emoji copied!', 'success', 'fas fa-check');
}
if (this.activeTab === 'steganography') {
this.selectedEmoji = emoji;
const tempCarrier = {
name: `${emoji} Carrier`,
emoji: emoji,
encode: (text) => this.steganography.encode(text, emoji),
decode: (text) => this.steganography.decode(text),
preview: (text) => `${emoji}${text}${emoji}`
};
this.selectedCarrier = tempCarrier;
this.activeSteg = 'emoji';
if (this.emojiMessage) {
this.autoEncode();
this.$nextTick(() => {
if (this.encodedMessage) {
const encodedStr = String(this.encodedMessage);
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(encodedStr)
.then(() => {
this.showNotification(`Hidden message copied with ${emoji}`, 'success', 'fas fa-check');
this.addToCopyHistory(`Hidden Message with ${emoji}`, encodedStr);
})
.catch(err => {
console.warn('Encoded emoji clipboard API failed:', err);
this.forceCopyToClipboard(encodedStr);
this.showNotification(`Hidden message copied with ${emoji}`, 'success', 'fas fa-check');
});
} else {
this.forceCopyToClipboard(encodedStr);
this.showNotification(`Hidden message copied with ${emoji}`, 'success', 'fas fa-check');
}
}
});
}
}
},
renderEmojiGrid: function() {
const container = document.getElementById('emoji-grid-container');
if (!container) {
console.error('emoji-grid-container not found!');
return;
}
container.style.cssText = 'display: block !important; visibility: visible !important; min-height: 300px;';
const emojiLibrary = document.querySelector('.emoji-library');
if (emojiLibrary) {
emojiLibrary.style.cssText = 'display: block !important; visibility: visible !important;';
}
while (container.firstChild) {
container.removeChild(container.firstChild);
}
this._renderEmojiGridInternal('emoji-grid-container', this.selectEmoji.bind(this), this.filteredEmojis);
},
_renderEmojiGridInternal: function(containerId, onEmojiSelect, filteredList) {
const container = document.getElementById(containerId);
if (!container) {
console.error('Container not found:', containerId);
return;
}
const categories = window.emojiData && window.emojiData.categories ? window.emojiData.categories : [];
const emojiHeader = document.createElement('div');
emojiHeader.className = 'emoji-header';
const headerTitle = document.createElement('h3');
const icon = document.createElement('i');
icon.className = 'fas fa-icons';
headerTitle.appendChild(icon);
headerTitle.appendChild(document.createTextNode(' Choose an Emoji'));
const subtitle = document.createElement('p');
subtitle.className = 'emoji-subtitle';
const magicIcon = document.createElement('i');
magicIcon.className = 'fas fa-magic';
subtitle.appendChild(magicIcon);
subtitle.appendChild(document.createTextNode(' Click any emoji to copy your hidden message'));
emojiHeader.appendChild(headerTitle);
emojiHeader.appendChild(subtitle);
container.appendChild(emojiHeader);
const categoryTabs = document.createElement('div');
categoryTabs.className = 'emoji-category-tabs';
categories.forEach(category => {
const tab = document.createElement('button');
tab.className = 'emoji-category-tab';
if (category.id === 'all') {
tab.classList.add('active');
}
tab.setAttribute('data-category', category.id);
tab.textContent = `${category.icon} ${category.name}`;
categoryTabs.appendChild(tab);
});
container.appendChild(categoryTabs);
const gridContainer = document.createElement('div');
gridContainer.className = 'emoji-grid';
let activeCategory = 'all';
const activeCategoryTab = container.querySelector('.emoji-category-tab.active');
if (activeCategoryTab) {
activeCategory = activeCategoryTab.getAttribute('data-category');
}
let emojisToShow = [];
if (filteredList && filteredList.length > 0) {
emojisToShow = filteredList;
} else if (window.emojiData && typeof window.emojiData.getByCategory === 'function') {
emojisToShow = window.emojiData.getByCategory(activeCategory, false);
}
const emojisToRender = emojisToShow.filter(emoji => {
if (this.compatibleEmojis && this.compatibleEmojis.length > 0) {
return this.compatibleEmojis.includes(emoji);
}
if (window.emojiCompatibility && typeof window.emojiCompatibility.shouldShowInPicker === 'function') {
return window.emojiCompatibility.shouldShowInPicker(emoji);
}
return true;
});
emojisToRender.forEach(emoji => {
const emojiButton = document.createElement('button');
emojiButton.className = 'emoji-button';
emojiButton.textContent = emoji;
emojiButton.title = 'Click to encode with this emoji';
emojiButton.addEventListener('click', () => {
if (typeof onEmojiSelect === 'function') {
onEmojiSelect(emoji);
emojiButton.style.backgroundColor = '#e6f7ff';
setTimeout(() => {
emojiButton.style.backgroundColor = '';
}, 300);
}
});
gridContainer.appendChild(emojiButton);
});
container.appendChild(gridContainer);
const categoryTabButtons = container.querySelectorAll('.emoji-category-tab');
categoryTabButtons.forEach(tab => {
tab.addEventListener('click', () => {
categoryTabButtons.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
const selectedCategory = tab.getAttribute('data-category');
let emojisToShow = [];
if (window.emojiData && typeof window.emojiData.getByCategory === 'function') {
emojisToShow = window.emojiData.getByCategory(selectedCategory, false);
}
while (gridContainer.firstChild) {
gridContainer.removeChild(gridContainer.firstChild);
}
const emojisToRender = emojisToShow.filter(emoji => {
if (this.compatibleEmojis && this.compatibleEmojis.length > 0) {
return this.compatibleEmojis.includes(emoji);
}
if (window.emojiCompatibility && typeof window.emojiCompatibility.shouldShowInPicker === 'function') {
return window.emojiCompatibility.shouldShowInPicker(emoji);
}
return true;
});
emojisToRender.forEach(emoji => {
const emojiButton = document.createElement('button');
emojiButton.className = 'emoji-button';
emojiButton.textContent = emoji;
emojiButton.title = 'Click to encode with this emoji';
emojiButton.addEventListener('click', () => {
if (typeof onEmojiSelect === 'function') {
onEmojiSelect(emoji);
emojiButton.style.backgroundColor = '#e6f7ff';
setTimeout(() => {
emojiButton.style.backgroundColor = '';
}, 300);
}
});
gridContainer.appendChild(emojiButton);
});
const countDisplay = container.querySelector('.emoji-count');
if (countDisplay) {
countDisplay.textContent = `${emojisToShow.length} emojis available`;
}
});
});
const countDisplay = document.createElement('div');
countDisplay.className = 'emoji-count';
countDisplay.textContent = `${emojisToShow.length} emojis available`;
container.appendChild(countDisplay);
},
filterEmojis: function() {
const allEmojis = window.EmojiUtils ? window.EmojiUtils.getAllEmojis() : [];
this.filteredEmojis = this.compatibleEmojis.length > 0 ? [...this.compatibleEmojis] : [...allEmojis];
this.renderEmojiGrid();
}
};
}
getVueLifecycle() {
return {
mounted() {
this.initializeEmojiList();
this.$nextTick(() => {
const allEmojis = window.EmojiUtils ? window.EmojiUtils.getAllEmojis() : [];
this.filteredEmojis = this.compatibleEmojis.length > 0 ? [...this.compatibleEmojis] : [...allEmojis];
const initializeEmojiGrid = () => {
if (this.activeTab !== 'steganography') {
return;
}
const emojiGridContainer = document.getElementById('emoji-grid-container');
if (emojiGridContainer) {
emojiGridContainer.setAttribute('style', 'display: block !important; visibility: visible !important; min-height: 300px; padding: 10px;');
const emojiLibrary = document.querySelector('.emoji-library');
if (emojiLibrary) {
emojiLibrary.setAttribute('style', 'display: block !important; visibility: visible !important; margin-top: 20px; overflow: visible;');
}
this.renderEmojiGrid();
clearInterval(emojiGridInitializer);
}
};
const emojiGridInitializer = setInterval(initializeEmojiGrid, 500);
});
}
};
}
onActivate(vueInstance) {
vueInstance.$nextTick(() => {
const emojiGridContainer = document.getElementById('emoji-grid-container');
if (emojiGridContainer) {
emojiGridContainer.setAttribute('style', 'display: block !important; visibility: visible !important; min-height: 300px; padding: 10px;');
vueInstance.renderEmojiGrid();
}
});
}
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = EmojiTool;
} else {
window.EmojiTool = EmojiTool;
}
+236
View File
@@ -0,0 +1,236 @@
/**
* Gibberish Tool - Generate gibberish dictionary and random/specific character removal
*/
class GibberishTool extends Tool {
constructor() {
super({
id: 'gibberish',
name: 'Gibberish',
icon: 'fa-comments',
title: 'Gibberish Generator',
order: 8
});
}
getVueData() {
return {
// Gibberish Dictionary
gibberishInput: '',
gibberishOutput: '',
gibberishSeed: '',
gibberishDictionary: '',
gibberishChars: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
gibberishMode: 'random',
// Removal mode properties
removalSubMode: 'random',
removalInput: '',
removalVariations: 10,
removalMinLetters: 1,
removalMaxLetters: 3,
removalSeed: '',
removalOutputs: [],
removalSpecificInput: '',
removalCharsToRemove: '',
removalSpecificOutput: ''
};
}
getVueMethods() {
return {
// Gibberish Logic - Seeded random number generator
seededRandom(seed) {
const x = Math.sin(seed) * 10000;
return x - Math.floor(x);
},
/**
* Generate gibberish from input sentence while maintaining structure
* Creates a consistent dictionary mapping for words
*/
sentenceToGibberish() {
function generateGibberish(word, seed) {
const length = Math.max(4, word.length);
let gibberish = "";
const chars = this.gibberishChars;
for (let i = 0; i < length; i++) {
const randomValue = this.seededRandom(seed + i * 0.1);
gibberish += chars[Math.floor(randomValue * chars.length)];
}
return gibberish;
}
const src = String(this.gibberishInput || '');
if (!src) {
this.gibberishOutput = '';
return;
}
const words = this.gibberishInput.match(/\b\w+\b/g) || [];
const dictionary = {};
let gibberishSentence = "";
let wordIndex = 0;
words.forEach((word) => {
const lowerWord = word.toLowerCase();
const seed =
this.gibberishSeed === ""
? Math.random() * 100
: Number(this.gibberishSeed);
if (!dictionary[lowerWord]) {
const wordSeed = seed + wordIndex * 100;
dictionary[lowerWord] = generateGibberish.call(this, word, wordSeed);
wordIndex++;
}
});
let charIndex = 0;
for (let i = 0; i < this.gibberishInput.length; i++) {
const char = this.gibberishInput[i];
if (/\w/.test(char)) {
let j = i;
while (
j < this.gibberishInput.length &&
/\w/.test(this.gibberishInput[j])
) {
j++;
}
const word = this.gibberishInput.substring(i, j).toLowerCase();
gibberishSentence += dictionary[word];
i = j - 1;
} else {
gibberishSentence += char;
}
}
const dictionaryString = Object.entries(dictionary)
.map(([plain, gib]) => `"${plain}": "${gib}"`)
.join(", ");
this.gibberishOutput = gibberishSentence;
this.gibberishDictionary = '{' + dictionaryString + '}';
},
/**
* Factory for creating seeded random number generators
* @param {string} seedStr - Seed string for RNG
* @returns {Function} Random number generator function
*/
seededRandomFactory(seedStr) {
if (!seedStr) return Math.random;
let h = 1779033703 ^ seedStr.length;
for (let i=0;i<seedStr.length;i++) {
h = Math.imul(h ^ seedStr.charCodeAt(i), 3432918353);
h = (h << 13) | (h >>> 19);
}
return function() {
h = Math.imul(h ^ (h >>> 16), 2246822507);
h = Math.imul(h ^ (h >>> 13), 3266489909);
return ((h ^= h >>> 16) >>> 0) / 4294967296;
};
},
/**
* Generate random character removals from input text
* Creates multiple variations with different random removals
*/
generateRandomRemovals() {
if (!this.removalInput.trim()) {
this.showNotification('Please enter text to process', 'error');
return;
}
const seed = this.removalSeed ? String(this.removalSeed) : String(Date.now());
let rng = this.seededRandomFactory(seed);
this.removalOutputs = [];
const words = this.removalInput.split(/\s+/);
for (let v = 0; v < this.removalVariations; v++) {
const modifiedWords = words.map(word => {
// Skip very short words or non-alphabetic
if (word.length <= 1 || !/[a-zA-Z]/.test(word)) {
return word;
}
// Determine how many letters to remove for this word
const minRemove = Math.max(0, this.removalMinLetters);
const maxRemove = Math.min(word.length - 1, this.removalMaxLetters);
const numToRemove = minRemove + Math.floor(rng() * (maxRemove - minRemove + 1));
if (numToRemove === 0) {
return word;
}
// Get letter positions
const letters = word.split('').map((c, i) => ({ char: c, index: i }))
.filter(item => /[a-zA-Z]/.test(item.char));
// Randomly select positions to remove
const toRemoveIndices = new Set();
const maxAttempts = numToRemove * 3;
let attempts = 0;
while (toRemoveIndices.size < Math.min(numToRemove, letters.length) && attempts < maxAttempts) {
const randIdx = Math.floor(rng() * letters.length);
toRemoveIndices.add(letters[randIdx].index);
attempts++;
}
// Build result by skipping removed indices
return word.split('').filter((_, i) => !toRemoveIndices.has(i)).join('');
});
this.removalOutputs.push(modifiedWords.join(' '));
}
this.showNotification(`Generated ${this.removalOutputs.length} variations`, 'success');
},
/**
* Remove specific characters from input text
*/
generateSpecificRemoval() {
if (!this.removalSpecificInput.trim()) {
this.showNotification('Please enter text to process', 'error');
return;
}
if (!this.removalCharsToRemove) {
this.showNotification('Please specify characters to remove', 'error');
return;
}
const charsToRemove = new Set(this.removalCharsToRemove.split(''));
this.removalSpecificOutput = this.removalSpecificInput
.split('')
.filter(char => !charsToRemove.has(char))
.join('');
this.showNotification('Characters removed', 'success');
},
/**
* Copy all removal outputs to clipboard (one per line)
*/
copyAllRemovals() {
if (this.removalOutputs.length === 0) return;
const allOutputs = this.removalOutputs.join('\n');
this.copyToClipboard(allOutputs);
}
};
}
}
// Export
if (typeof module !== 'undefined' && module.exports) {
module.exports = GibberishTool;
} else {
window.GibberishTool = GibberishTool;
}
+119
View File
@@ -0,0 +1,119 @@
/**
* Mutation Tool - Fuzzer/Mutation Lab tool
*/
class MutationTool extends Tool {
constructor() {
super({
id: 'fuzzer',
name: 'Mutation Lab',
icon: 'fa-bug',
title: 'Generate many mutated payloads for testing',
order: 5
});
}
getVueData() {
return {
fuzzerInput: '',
fuzzerCount: 20,
fuzzerSeed: '',
fuzzUseRandomMix: true,
fuzzZeroWidth: true,
fuzzUnicodeNoise: true,
fuzzZalgo: false,
fuzzWhitespace: true,
fuzzCasing: true,
fuzzEncodeShuffle: false,
fuzzerOutputs: []
};
}
getVueMethods() {
return {
seededRandomFactory: function(seedStr) {
if (!seedStr) return Math.random;
let h = 1779033703 ^ seedStr.length;
for (let i=0;i<seedStr.length;i++) {
h = Math.imul(h ^ seedStr.charCodeAt(i), 3432918353);
h = (h << 13) | (h >>> 19);
}
return function() {
h ^= h >>> 16; h = Math.imul(h, 2246822507); h ^= h >>> 13; h = Math.imul(h, 3266489909); h ^= h >>> 16;
return (h >>> 0) / 4294967296;
};
},
pick: function(arr, rnd) { return arr[Math.floor(rnd()*arr.length)]; },
injectZeroWidth: function(text, rnd) {
const zw = ['\u200B','\u200C','\u200D','\u2060'];
return [...text].map(ch => (rnd()<0.2 ? ch+this.pick(zw,rnd) : ch)).join('');
},
injectUnicodeNoise: function(text, rnd) {
const marks = ['\u0301','\u0300','\u0302','\u0303','\u0308','\u0307','\u0304'];
return [...text].map(ch => (rnd()<0.15 ? ch+this.pick(marks,rnd) : ch)).join('');
},
whitespaceChaos: function(text, rnd) {
return text.replace(/\s/g, (m)=> (rnd()<0.5? m : (rnd()<0.5?'\t':'\u00A0')));
},
casingChaos: function(text, rnd) {
return [...text].map(c => /[a-z]/i.test(c)? (rnd()<0.5? c.toUpperCase():c.toLowerCase()) : c).join('');
},
encodeShuffle: function(text, rnd) {
const map = {
'A':'Α','B':'Β','C':'Ϲ','E':'Ε','H':'Η','I':'Ι','K':'Κ','M':'Μ','N':'Ν','O':'Ο','P':'Ρ','T':'Τ','X':'Χ','Y':'Υ',
'a':'а','c':'с','e':'е','i':'і','j':'ј','o':'о','p':'р','s':'ѕ','x':'х','y':'у'
};
return [...text].map(ch => {
if (map[ch] && rnd() < 0.25) return map[ch];
return ch;
}).join('');
},
generateFuzzCases: function() {
const src = String(this.fuzzerInput || '');
if (!src) { this.fuzzerOutputs = []; return; }
const rnd = this.seededRandomFactory(String(this.fuzzerSeed||''));
const out = [];
for (let i=0;i<Math.max(1,Math.min(500,Number(this.fuzzerCount)||1)); i++) {
let s = src;
if (this.fuzzUseRandomMix) {
try { s = window.transforms.randomizer.func(s, { minTransforms:2, maxTransforms:4 }); } catch(_) {}
}
if (this.fuzzZeroWidth) s = this.injectZeroWidth(s, rnd);
if (this.fuzzUnicodeNoise) s = this.injectUnicodeNoise(s, rnd);
if (this.fuzzWhitespace) s = this.whitespaceChaos(s, rnd);
if (this.fuzzCasing) s = this.casingChaos(s, rnd);
if (this.fuzzZalgo) { try { s = window.transforms.zalgo.func(s); } catch(_) {} }
if (this.fuzzEncodeShuffle) s = this.encodeShuffle(s, rnd);
out.push(s);
}
this.fuzzerOutputs = out;
},
copyAllFuzz: function() { this.copyToClipboard(this.fuzzerOutputs.join('\n')); },
downloadFuzz: function() {
const lines = this.fuzzerOutputs.map((s, i) => `#${i+1}\t${s}`).join('\n');
const header = `# Parseltongue Fuzzer Output\n# count=${this.fuzzerOutputs.length}\n# seed=${this.fuzzerSeed || ''}\n# strategies=${[
this.fuzzUseRandomMix?'randomMix':null,
this.fuzzZeroWidth?'zeroWidth':null,
this.fuzzUnicodeNoise?'unicodeNoise':null,
this.fuzzWhitespace?'whitespace':null,
this.fuzzCasing?'casing':null,
this.fuzzZalgo?'zalgo':null,
this.fuzzEncodeShuffle?'encodeShuffle':null
].filter(Boolean).join(',')}\n`;
const blob = new Blob([header + lines + '\n'], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a'); a.href = url; a.download = 'fuzz_cases.txt'; a.click();
setTimeout(()=>URL.revokeObjectURL(url), 200);
}
};
}
}
// Export
if (typeof module !== 'undefined' && module.exports) {
module.exports = MutationTool;
} else {
window.MutationTool = MutationTool;
}
+267
View File
@@ -0,0 +1,267 @@
/**
* Splitter Tool - Split text into multiple copyable messages
*/
class SplitterTool extends Tool {
constructor() {
super({
id: 'splitter',
name: 'Splitter',
icon: 'fa-grip-lines',
title: 'Split text into multiple copyable messages',
order: 7
});
}
getVueData() {
return {
// Message Splitter Tab
splitterInput: '',
splitterMode: 'word', // 'chunk' or 'word' - default to word
splitterChunkSize: 6,
splitterWordSplitSide: 'left', // 'left' or 'right' for even-length words
splitterWordSkip: 0, // number of words to skip between splits
splitterMinWordLength: 2, // minimum word length to consider for splitting (skip shorter words)
splitterSplitFirstWord: true, // whether to split the first word (true) or keep it whole (false)
splitterCopyAsSingleLine: false, // copy as single line (true) or multiline (false)
splitterTransforms: [''], // array of transform names to apply in sequence (start with one empty slot)
splitterStartWrap: '',
splitterEndWrap: '',
splitMessages: []
};
}
getVueMethods() {
return {
/**
* Set encapsulation start and end strings
* @param {string} start - The start string
* @param {string} end - The end string
*/
setEncapsulation(start, end) {
this.splitterStartWrap = start;
this.splitterEndWrap = end;
},
/**
* Handle transform change - auto-add next dropdown or collapse consecutive Nones
* @param {number} index - The index of the transformation that changed
*/
handleTransformChange(index) {
const value = this.splitterTransforms[index];
if (value && value !== '') {
// Transform was selected - add next dropdown if it doesn't exist
if (index === this.splitterTransforms.length - 1) {
this.splitterTransforms.push('');
}
} else {
// Transform was set to None
// Check if previous dropdown is also None - if so, remove current one and collapse from previous position
if (index > 0) {
const prev = this.splitterTransforms[index - 1];
if (!prev || prev === '') {
// Collapse: remove this dropdown
this.splitterTransforms.splice(index, 1);
}
} else if (index === 0 && this.splitterTransforms.length === 1) {
// Only one dropdown and it's set to None - keep it as the starting dropdown
// Do nothing
} else if (index === 0 && this.splitterTransforms.length > 1) {
// First dropdown set to None, check if next is also None
const next = this.splitterTransforms[1];
if (!next || next === '') {
// Remove the first one
this.splitterTransforms.splice(0, 1);
}
}
}
// Ensure there's always at least one dropdown
if (this.splitterTransforms.length === 0) {
this.splitterTransforms = [''];
}
// Force Vue to update
this.$forceUpdate();
},
/**
* Generate split messages from input text
* Supports two modes: character chunks or split words in half
*/
generateSplitMessages() {
// Clear previous output at the start
this.splitMessages = [];
const input = this.splitterInput;
if (!input) {
return;
}
let chunks = [];
if (this.splitterMode === 'chunk') {
// Character chunk mode
const chunkSize = Math.max(1, Math.min(500, this.splitterChunkSize || 6));
for (let i = 0; i < input.length; i += chunkSize) {
chunks.push(input.slice(i, i + chunkSize));
}
} else if (this.splitterMode === 'word') {
// Word split mode - creates messages with pattern: secondHalf + wholeWords + firstHalf
// IMPORTANT: ALL words must be included in output, never filtered out
const words = input.match(/\S+/g) || [];
if (words.length === 0) return;
const skipCount = Math.max(0, Math.min(20, this.splitterWordSkip || 0));
const minLength = Math.max(1, this.splitterMinWordLength || 2);
// Process all words - only split words that meet minimum length
// Short words are kept whole but still included in the pattern
let wordsToProcess = words;
let prependToFirst = [];
// Handle "Split First Word" option
if (!this.splitterSplitFirstWord && words.length > 0) {
prependToFirst = [words[0]];
wordsToProcess = words.slice(1);
}
// Build word processing array - track which words can be split vs kept whole
const wordData = wordsToProcess.map((word, idx) => {
const canSplit = word.length >= minLength && word.length > 1;
return {
word: word,
canSplit: canSplit,
index: idx
};
});
// Determine which words to split (only words that can be split)
const splittableWords = wordData.filter(w => w.canSplit);
if (splittableWords.length === 0) {
// No words can be split, output everything as one message
chunks.push([...prependToFirst, ...wordsToProcess].join(' '));
return;
}
// Determine split pattern based on splittable words only
const splitIndexes = new Set();
for (let i = 0; i < splittableWords.length; i++) {
if ((i % (skipCount + 1)) === 0) {
splitIndexes.add(splittableWords[i].index);
}
}
// Process all words and build split structure
const processedWords = wordData.map((wd, idx) => {
if (splitIndexes.has(idx) && wd.canSplit) {
// Split this word
let splitPos;
if (wd.word.length % 2 === 0) {
splitPos = wd.word.length / 2;
} else {
splitPos = this.splitterWordSplitSide === 'left'
? Math.ceil(wd.word.length / 2)
: Math.floor(wd.word.length / 2);
}
return {
firstHalf: wd.word.slice(0, splitPos),
secondHalf: wd.word.slice(splitPos),
split: true
};
}
// Keep whole (either too short or skipped)
return { whole: wd.word, split: false };
});
// Build output messages
let currentMessage = [...prependToFirst];
let messageStarted = false;
for (let i = 0; i < processedWords.length; i++) {
const item = processedWords[i];
if (item.split) {
if (!messageStarted) {
// First split word - add first half to current message
currentMessage.push(item.firstHalf);
chunks.push(currentMessage.join(' '));
currentMessage = [item.secondHalf];
messageStarted = true;
} else {
// Add first half to current message, then start new message with second half
currentMessage.push(item.firstHalf);
chunks.push(currentMessage.join(' '));
currentMessage = [item.secondHalf];
}
} else {
// Whole word - add to current message (ALL words included)
currentMessage.push(item.whole);
}
}
// Add any remaining message
if (currentMessage.length > 0) {
chunks.push(currentMessage.join(' '));
}
}
// Apply transformations in sequence (chaining)
let processedChunks = chunks;
if (this.splitterTransforms && this.splitterTransforms.length > 0) {
// Filter out empty transforms
const activeTransforms = this.splitterTransforms.filter(t => t && t !== '');
if (activeTransforms.length > 0) {
// Apply each transformation in sequence
for (const transformName of activeTransforms) {
const selectedTransform = this.transforms.find(t => t.name === transformName);
if (selectedTransform && selectedTransform.func) {
processedChunks = processedChunks.map(chunk => {
try {
return selectedTransform.func(chunk);
} catch (e) {
console.error('Transform error:', e);
return chunk;
}
});
}
}
}
}
// Apply encapsulation
const start = this.splitterStartWrap || '';
const end = this.splitterEndWrap || '';
this.splitMessages = processedChunks.map(chunk => `${start}${chunk}${end}`);
},
/**
* Copy all split messages to clipboard
* Single line: merges messages into one continuous string (keeps encapsulation/transformations)
* Multiline: copies messages separated by newlines
*/
copyAllSplitMessages() {
if (this.splitMessages.length === 0) return;
if (this.splitterCopyAsSingleLine) {
// Merge all messages back together, keeping encapsulation and transformations
// Just join without newlines - all encapsulation/transformations are already in splitMessages
const merged = this.splitMessages.join('');
this.copyToClipboard(merged);
} else {
// Copy all messages separated by newlines
const allMessages = this.splitMessages.join('\n');
this.copyToClipboard(allMessages);
}
}
};
}
}
// Export
if (typeof module !== 'undefined' && module.exports) {
module.exports = SplitterTool;
} else {
window.SplitterTool = SplitterTool;
}
+245
View File
@@ -0,0 +1,245 @@
/**
* Tokenade Tool - Token bomb generator tool
* Note: This is a complex tool, so we'll include the key methods
*/
class TokenadeTool extends Tool {
constructor() {
super({
id: 'tokenade',
name: 'Tokenade',
icon: 'fa-bomb',
title: 'Tokenade Generator',
order: 4
});
}
getVueData() {
return {
tbDepth: 3,
tbBreadth: 4,
tbRepeats: 5,
tbSeparator: 'zwnj',
tbIncludeVS: true,
tbIncludeNoise: true,
tbRandomizeEmojis: true,
tbAutoCopy: true,
tbSingleCarrier: true,
tbCarrier: '',
tbPayloadEmojis: [],
tokenBombOutput: '',
tpBase: '',
tpRepeat: 100,
tpCombining: true,
tpZW: false,
textPayload: '',
dangerThresholdTokens: 25_000_000,
quickCarrierEmojis: ['🐍','🐉','🐲','🔥','💥','🗿','⚓','⭐','✨','🚀','💀','🪨','🍃','🪶','🔮','🐢','🐊','🦎'],
tbCarrierManual: '',
carrierEmojiList: [...(window.EmojiUtils ? window.EmojiUtils.getAllEmojis() : [])]
};
}
getVueMethods() {
return {
generateTokenBomb: function() {
const depth = Math.max(1, Math.min(8, Number(this.tbDepth) || 1));
const breadth = Math.max(1, Math.min(10, Number(this.tbBreadth) || 1));
const repeats = Math.max(1, Math.min(50, Number(this.tbRepeats) || 1));
const sep = this.tbSeparator === 'zwj' ? '\u200D' : this.tbSeparator === 'zwnj' ? '\u200C' : this.tbSeparator === 'zwsp' ? '\u200B' : '';
const includeVS = !!this.tbIncludeVS;
const includeNoise = !!this.tbIncludeNoise;
const randomize = !!this.tbRandomizeEmojis;
const emojiList = (this.carrierEmojiList && this.carrierEmojiList.length) ? this.carrierEmojiList :
(window.EmojiUtils ? window.EmojiUtils.getAllEmojis() : this.quickCarrierEmojis);
function pickEmojis(count) {
const out = [];
for (let i = 0; i < count; i++) {
const idx = randomize ? Math.floor(Math.random() * emojiList.length) : (i % emojiList.length);
out.push(String(emojiList[idx]));
}
return out;
}
function addVS(str) {
if (!includeVS) return str;
// Alternate VS16/VS15 to maximize tokenization churn
const vs16 = '\uFE0F';
const vs15 = '\uFE0E';
let out = '';
for (let i = 0; i < str.length; i++) {
const ch = str[i];
out += ch + (i % 2 === 0 ? vs16 : vs15);
}
return out;
}
function noise() {
if (!includeNoise) return '';
const parts = ['\u200B','\u200C','\u200D','\u2060','\u2062','\u2063'];
let s = '';
const n = 1 + Math.floor(Math.random() * 3);
for (let i = 0; i < n; i++) s += parts[Math.floor(Math.random() * parts.length)];
return s;
}
function buildLevel(level) {
if (level === 0) {
const base = pickEmojis(breadth).join('');
return addVS(base);
}
const items = [];
for (let i = 0; i < breadth; i++) {
const inner = buildLevel(level - 1);
items.push(inner + noise());
}
return items.join(sep);
}
if (this.tbSingleCarrier) {
const manual = (this.tbCarrierManual || '').trim();
const carrier = manual || (this.tbCarrier && String(this.tbCarrier)) || (this.selectedEmoji ? String(this.selectedEmoji) : '💥');
function countUnits(level) {
if (level === 0) return breadth;
return breadth * countUnits(level - 1);
}
const unitsPerBlock = countUnits(depth - 1);
const totalUnits = Math.max(1, repeats * unitsPerBlock);
let payload = [];
payload = pickEmojis(totalUnits);
function toTagSeqForEmojiChar(ch) {
const cp = ch.codePointAt(0);
const hex = cp.toString(16);
let seq = '';
for (const d of hex) {
if (d >= '0' && d <= '9') {
const base = 0xE0030 + (d.charCodeAt(0) - '0'.charCodeAt(0));
seq += String.fromCodePoint(base);
} else {
const base = 0xE0061 + (d.charCodeAt(0) - 'a'.charCodeAt(0));
seq += String.fromCodePoint(base);
}
}
seq += String.fromCodePoint(0xE007F);
return seq;
}
const vs16 = includeVS ? '\uFE0F' : '';
let out = carrier + vs16;
for (let i = 0; i < payload.length; i++) {
out += sep + toTagSeqForEmojiChar(payload[i]) + noise();
}
this.tokenBombOutput = out;
} else {
let block = buildLevel(depth - 1);
// Repeat the block to increase token length
const blocks = [];
for (let i = 0; i < repeats; i++) {
blocks.push(block + noise());
}
this.tokenBombOutput = blocks.join(sep);
}
// Auto-copy if enabled
if (this.tbAutoCopy && this.tokenBombOutput) {
this.$nextTick(() => {
this.forceCopyToClipboard(this.tokenBombOutput);
this.showNotification('Tokenade generated and copied!', 'success', 'fas fa-bomb');
});
} else {
this.showNotification('Tokenade generated!', 'success', 'fas fa-bomb');
}
},
applyTokenadePreset: function(preset) {
if (preset === 'feather') {
this.tbDepth = 1; this.tbBreadth = 3; this.tbRepeats = 2; this.tbSeparator = 'zwnj';
this.tbIncludeVS = false; this.tbIncludeNoise = false; this.tbRandomizeEmojis = true;
} else if (preset === 'light') {
this.tbDepth = 2; this.tbBreadth = 3; this.tbRepeats = 3; this.tbSeparator = 'zwnj';
this.tbIncludeVS = false; this.tbIncludeNoise = true; this.tbRandomizeEmojis = true;
} else if (preset === 'middle') {
this.tbDepth = 3; this.tbBreadth = 4; this.tbRepeats = 6; this.tbSeparator = 'zwnj';
this.tbIncludeVS = true; this.tbIncludeNoise = true; this.tbRandomizeEmojis = true;
} else if (preset === 'heavy') {
this.tbDepth = 4; this.tbBreadth = 6; this.tbRepeats = 12; this.tbSeparator = 'zwnj';
this.tbIncludeVS = true; this.tbIncludeNoise = true; this.tbRandomizeEmojis = true;
} else if (preset === 'super') {
this.tbDepth = 5; this.tbBreadth = 8; this.tbRepeats = 18; this.tbSeparator = 'zwnj';
this.tbIncludeVS = true; this.tbIncludeNoise = true; this.tbRandomizeEmojis = true;
}
this.showNotification('Preset applied', 'success', 'fas fa-sliders-h');
},
estimateTokenadeLength: function() {
const depth = Math.max(1, Math.min(8, Number(this.tbDepth) || 1));
const breadth = Math.max(1, Math.min(10, Number(this.tbBreadth) || 1));
const repeats = Math.max(1, Math.min(50, Number(this.tbRepeats) || 1));
const sepLen = this.tbSeparator === 'none' ? 0 : 1;
const vsPerEmoji = this.tbIncludeVS ? 1 : 0;
const noiseAvg = this.tbIncludeNoise ? 2 : 0;
function lenLevel(level) {
if (level === 0) {
return breadth * (1 + vsPerEmoji);
}
const inner = lenLevel(level - 1);
return breadth * (inner + noiseAvg) + Math.max(0, breadth - 1) * sepLen;
}
if (this.tbSingleCarrier) {
function countUnits(level) { return level === 0 ? breadth : breadth * countUnits(level - 1); }
const unitsPerBlock = countUnits(depth - 1);
const totalUnits = Math.max(1, repeats * unitsPerBlock);
const avgDigits = 5;
const perUnit = avgDigits + 1 + sepLen + (this.tbIncludeNoise ? 2 : 0);
const carrierLen = 1 + (this.tbIncludeVS ? 1 : 0);
return carrierLen + totalUnits * perUnit;
} else {
const blockLen = lenLevel(depth - 1);
return repeats * (blockLen + noiseAvg) + Math.max(0, repeats - 1) * sepLen;
}
},
estimateTokenadeTokens: function() {
return Math.max(0, this.estimateTokenadeLength());
},
setCarrierFromSelected: function() {
if (this.selectedEmoji) this.tbCarrier = String(this.selectedEmoji);
},
generateTextPayload: function() {
const base = String(this.tpBase || 'A');
const count = Math.max(1, Math.min(10000, Number(this.tpRepeat) || 1));
const combining = this.tpCombining;
const addZW = this.tpZW;
const marks = ['\u0301','\u0300','\u0302','\u0303','\u0308','\u0307','\u0304'];
const zw = ['\u200B','\u200C','\u200D','\u2060'];
let out = '';
for (let i=0;i<count;i++) {
let token = base;
if (combining) {
const m = marks[i % marks.length];
token += m;
}
if (addZW) {
const z = zw[i % zw.length];
token += z;
}
out += token;
}
this.textPayload = out;
this.showNotification('Text payload generated', 'success', 'fas fa-bomb');
}
};
}
}
// Export
if (typeof module !== 'undefined' && module.exports) {
module.exports = TokenadeTool;
} else {
window.TokenadeTool = TokenadeTool;
}
+95
View File
@@ -0,0 +1,95 @@
/**
* Tokenizer Tool - Tokenizer visualization tool
*/
class TokenizerTool extends Tool {
constructor() {
super({
id: 'tokenizer',
name: 'Tokenizer',
icon: 'fa-layer-group',
title: 'Tokenizer visualization',
order: 6
});
}
getVueData() {
return {
tokenizerInput: '',
tokenizerEngine: 'byte',
tokenizerTokens: [],
tokenizerCharCount: 0,
tokenizerWordCount: 0
};
}
getVueMethods() {
return {
runTokenizer: async function() {
const text = this.tokenizerInput || '';
const engine = this.tokenizerEngine;
const tokens = [];
if (!text) { this.tokenizerTokens = []; this.tokenizerCharCount = 0; this.tokenizerWordCount = 0; return; }
if (engine === 'byte') {
const encoder = new TextEncoder();
const bytes = encoder.encode(text);
for (let i=0;i<bytes.length;i++) {
tokens.push({ id: bytes[i], text: `0x${bytes[i].toString(16).padStart(2,'0')}` });
}
} else if (engine === 'word') {
const parts = text.split(/(\s+|[\.,!?:;()\[\]{}])/);
for (const p of parts) { if (p) tokens.push({ text: p }); }
} else if (['cl100k','o200k','p50k','r50k'].includes(engine)) {
try {
if (!window.gptTok) {
window.gptTok = await import('https://cdn.jsdelivr.net/npm/gpt-tokenizer@2/+esm');
}
const map = { cl100k: 'cl100k_base', o200k: 'o200k_base', p50k: 'p50k_base', r50k: 'r50k_base' };
const enc = map[engine];
const ids = window.gptTok.encode(text, enc);
for (const id of ids) {
const piece = window.gptTok.decode([id], enc);
tokens.push({ id, text: piece });
}
} catch (e) {
console.warn('Failed to load/use gpt-tokenizer; falling back to bytes', e);
this.tokenizerEngine = 'byte';
return this.runTokenizer();
}
} else {
const encoder = new TextEncoder();
const bytes = encoder.encode(text);
for (let i=0;i<bytes.length;i++) tokens.push({ id: bytes[i], text: `0x${bytes[i].toString(16).padStart(2,'0')}` });
}
this.tokenizerTokens = tokens;
this.tokenizerCharCount = Array.from(text).length;
const wordMatches = text.trim().match(/[^\s]+/g) || [];
this.tokenizerWordCount = wordMatches.length;
}
};
}
getVueWatchers() {
return {
tokenizerInput() {
this.runTokenizer();
},
tokenizerEngine() {
this.runTokenizer();
}
};
}
onActivate(vueInstance) {
vueInstance.$nextTick(() => vueInstance.runTokenizer());
}
}
// Export
if (typeof module !== 'undefined' && module.exports) {
module.exports = TokenizerTool;
} else {
window.TokenizerTool = TokenizerTool;
}
+97
View File
@@ -0,0 +1,97 @@
/**
* Base Tool Class
* All tools should inherit from this class and implement required methods
*/
class Tool {
constructor(config) {
// Required properties
this.id = config.id; // Unique identifier (e.g., 'transforms', 'decoder')
this.name = config.name; // Display name (e.g., 'Transform', 'Decoder')
this.icon = config.icon || 'fa-circle'; // Font Awesome icon class
this.title = config.title || this.name; // Tooltip/title text
// Optional properties
this.order = config.order || 999; // Order in tab bar (lower = earlier)
this.enabled = config.enabled !== false; // Whether tool is enabled
}
/**
* Get Vue data properties needed for this tool
* Should return an object that will be merged into Vue's data
* @returns {Object}
*/
getVueData() {
return {};
}
/**
* Get Vue methods needed for this tool
* Should return an object with method definitions
* @returns {Object}
*/
getVueMethods() {
return {};
}
/**
* Get Vue watchers needed for this tool
* Should return an object with watcher definitions
* @returns {Object}
*/
getVueWatchers() {
return {};
}
/**
* Get Vue lifecycle hooks
* Should return an object with lifecycle methods (mounted, created, etc.)
* @returns {Object}
*/
getVueLifecycle() {
return {};
}
/**
* Get HTML template for the tab button
* @returns {String} HTML string for the tab button
*/
getTabButtonHTML() {
return `
<button
:class="{ active: activeTab === '${this.id}' }"
@click="switchToTab('${this.id}')"
title="${this.title}"
>
<i class="fas ${this.icon}"></i> ${this.name}
</button>
`;
}
/**
* Initialize tool-specific functionality
* Called when the tool's tab is activated
* @param {Vue} vueInstance - The Vue app instance
*/
onActivate(vueInstance) {
// Override in subclasses
}
/**
* Cleanup tool-specific functionality
* Called when switching away from this tool's tab
* @param {Vue} vueInstance - The Vue app instance
*/
onDeactivate(vueInstance) {
// Override in subclasses
}
}
// Export for use in other files
if (typeof module !== 'undefined' && module.exports) {
module.exports = Tool;
} else {
window.Tool = Tool;
}
+193
View File
@@ -0,0 +1,193 @@
/**
* Transform Tool - Text transformation tool
*/
class TransformTool extends Tool {
constructor() {
super({
id: 'transforms',
name: 'Transform',
icon: 'fa-font',
title: 'Transform text (T)',
order: 1
});
}
getVueData() {
const transforms = (window.transforms && Object.keys(window.transforms).length > 0)
? Object.entries(window.transforms).map(([key, transform]) => ({
name: transform.name,
func: transform.func.bind(transform),
preview: transform.preview.bind(transform),
reverse: transform.reverse ? transform.reverse.bind(transform) : null,
category: transform.category || 'special'
}))
: [];
const categorySet = new Set();
transforms.forEach(transform => {
if (transform.category) {
categorySet.add(transform.category);
}
});
// Sort categories, but always put randomizer last
const sortedCategories = Array.from(categorySet).sort((a, b) => {
if (a === 'randomizer') return 1;
if (b === 'randomizer') return -1;
return a.localeCompare(b);
});
return {
transformInput: '',
transformOutput: '',
activeTransform: null,
transforms: transforms,
categories: sortedCategories
};
}
getVueMethods() {
return {
getDisplayCategory: function(transformName) {
// Find transform by name and return its category property
const transform = this.transforms.find(t => t.name === transformName);
return transform ? transform.category : 'special';
},
getTransformsByCategory: function(category) {
return this.transforms.filter(transform => transform.category === category);
},
isSpecialCategory: function(category) {
return category === 'randomizer';
},
applyTransform: function(transform, event) {
event && event.preventDefault();
event && event.stopPropagation();
if (transform && transform.name === 'Random Mix') {
this.triggerRandomizerChaos();
}
if (this.transformInput) {
this.activeTransform = transform;
if (transform.name === 'Random Mix') {
this.transformOutput = window.transforms.randomizer.func(this.transformInput);
const transformInfo = window.transforms.randomizer.getLastTransformInfo();
if (transformInfo.length > 0) {
const transformsList = transformInfo.map(t => t.transformName).join(', ');
this.showNotification(`Mixed with: ${transformsList}`, 'success', 'fas fa-random');
}
} else {
this.transformOutput = transform.func(this.transformInput);
}
this.isTransformCopy = true;
this.forceCopyToClipboard(this.transformOutput);
if (transform.name !== 'Random Mix') {
this.showNotification(`${transform.name} applied and copied!`, 'success', 'fas fa-check');
}
document.querySelectorAll('.transform-button').forEach(button => {
button.classList.remove('active');
});
const inputBox = document.querySelector('#transform-input');
if (inputBox) {
this.focusWithoutScroll(inputBox);
const len = inputBox.value.length;
try { inputBox.setSelectionRange(len, len); } catch (_) {}
}
this.isTransformCopy = false;
this.ignoreKeyboardEvents = false;
}
},
autoTransform: function() {
if (this.transformInput && this.activeTransform && this.activeTab === 'transforms') {
const segments = window.EmojiUtils.splitEmojis(this.transformInput);
const transformedSegments = segments.map(segment => {
if (segment.length > 1 || /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}]/u.test(segment)) {
return segment;
}
return this.activeTransform.func(segment);
});
this.transformOutput = window.EmojiUtils.joinEmojis(transformedSegments);
}
},
initializeCategoryNavigation: function() {
this.$nextTick(() => {
const legendItems = document.querySelectorAll('.transform-category-legend .legend-item');
legendItems.forEach(item => {
const newItem = item.cloneNode(true);
item.parentNode.replaceChild(newItem, item);
});
document.querySelectorAll('.transform-category-legend .legend-item').forEach(item => {
item.addEventListener('click', () => {
const targetId = item.getAttribute('data-target');
if (targetId) {
const targetElement = document.getElementById(targetId);
if (targetElement) {
document.querySelectorAll('.transform-category-legend .legend-item').forEach(li => {
li.classList.remove('active-category');
});
item.classList.add('active-category');
const inputSection = document.querySelector('.input-section');
const inputSectionHeight = inputSection.offsetHeight;
const elementPosition = targetElement.getBoundingClientRect().top + window.pageYOffset;
const offsetPosition = elementPosition - inputSectionHeight - 10;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
targetElement.classList.add('highlight-section');
setTimeout(() => {
targetElement.classList.remove('highlight-section');
}, 1000);
}
}
});
});
});
}
};
}
getVueWatchers() {
return {
transformInput() {
if (this.activeTransform && this.activeTab === 'transforms') {
this.transformOutput = this.activeTransform.func(this.transformInput);
}
}
};
}
getVueLifecycle() {
return {
mounted() {
this.initializeCategoryNavigation();
}
};
}
onActivate(vueInstance) {
vueInstance.$nextTick(() => {
vueInstance.initializeCategoryNavigation();
});
}
}
// Export
if (typeof module !== 'undefined' && module.exports) {
module.exports = TransformTool;
} else {
window.TransformTool = TransformTool;
}