bug fixes, more cleanup, updated Docs/Readme

This commit is contained in:
Dustin Farley
2026-03-21 02:44:01 -07:00
parent 1ceed15aa0
commit 57102797de
15 changed files with 1802 additions and 341 deletions
+1
View File
@@ -134,6 +134,7 @@ Higher priority = more specific pattern (used for decoder result ordering):
3. Test in webapp
4. Add `detector` function if format has distinctive patterns
5. Optionally add test cases to `tests/test_universal.js`
6. Add a one-line description for the transforms `name` in `DESCRIPTIONS` inside `build/readme-transform-section.js`, then run `node build/readme-transform-section.js` and merge the printed block into the **Text Transformations** section of the root `README.md` (the script exits with an error if a transform is missing from `DESCRIPTIONS`)
## Testing
+58 -41
View File
@@ -3,49 +3,66 @@ import BaseTransformer from '../BaseTransformer.js';
export default new BaseTransformer({
name: 'Alternating Case',
priority: 150, // Higher priority to detect before Base64
func: function(text) {
let upper = true;
return [...text].map(c => {
if (/[a-zA-Z]/.test(c)) {
const out = upper ? c.toUpperCase() : c.toLowerCase();
upper = !upper;
return out;
name: 'Alternating Case',
priority: 150, // Higher priority to detect before Base64
startWith: 'upper', // 'upper' | 'lower' — first alphabetic letter (fallback when options omitted)
configurableOptions: [
{
id: 'startWith',
label: 'First alphabetic letter',
type: 'select',
default: 'upper',
options: [
{ value: 'upper', label: 'Uppercase' },
{ value: 'lower', label: 'Lowercase' }
]
}
],
func: function(text, options) {
options = options || {};
const sw = options.startWith !== undefined && options.startWith !== ''
? options.startWith
: this.startWith;
let upper = sw === 'lower' ? false : true;
return [...text].map(c => {
if (/[a-zA-Z]/.test(c)) {
const out = upper ? c.toUpperCase() : c.toLowerCase();
upper = !upper;
return out;
}
return c;
}).join('');
},
preview: function(text, options) {
if (!text) return '[alt case]';
return this.func(text.slice(0, 6), options) + (text.length > 6 ? '...' : '');
},
reverse: function(text) {
// Reverse by lowercasing (loses original case pattern)
return text.toLowerCase();
},
detector: function(text) {
const cleaned = text.trim();
if (cleaned.length < 4) return false;
// Check for alternating pattern in letters only
let lastWasUpper = null;
let alternations = 0;
let letterCount = 0;
for (const char of cleaned) {
if (/[a-zA-Z]/.test(char)) {
const isUpper = char === char.toUpperCase();
if (lastWasUpper !== null && isUpper !== lastWasUpper) {
alternations++;
}
return c;
}).join('');
},
preview: function(text) {
if (!text) return '[alt case]';
return this.func(text.slice(0, 6)) + (text.length > 6 ? '...' : '');
},
reverse: function(text) {
// Reverse by lowercasing (loses original case pattern)
return text.toLowerCase();
},
detector: function(text) {
const cleaned = text.trim();
if (cleaned.length < 4) return false;
// Check for alternating pattern in letters only
let lastWasUpper = null;
let alternations = 0;
let letterCount = 0;
for (const char of cleaned) {
if (/[a-zA-Z]/.test(char)) {
const isUpper = char === char.toUpperCase();
if (lastWasUpper !== null && isUpper !== lastWasUpper) {
alternations++;
}
lastWasUpper = isUpper;
letterCount++;
}
lastWasUpper = isUpper;
letterCount++;
}
// Must have at least 3 alternations and at least 70% alternation rate
return letterCount >= 4 && alternations >= 3 && alternations >= letterCount * 0.7;
}
// Must have at least 3 alternations and at least 70% alternation rate
return letterCount >= 4 && alternations >= 3 && alternations >= letterCount * 0.7;
}
});
+52 -22
View File
@@ -1,39 +1,69 @@
// bitwise NOT transform
// Encode: UTF-8 bytes → NOT each byte → lossless lowercase hex (invalid UTF-8 after NOT is common).
// Decode: hex → bytes → NOT each byte → UTF-8 decode (exact inverse of encode).
// Helpers must live inside the default export: build-transforms.js concatenates the file body
// into transforms[name] = … so multiple top-level declarations would assign the wrong value.
import BaseTransformer from '../BaseTransformer.js';
export default new BaseTransformer({
export default (function () {
function utf8BytesBitwiseNot(bytes) {
const out = new Uint8Array(bytes.length);
for (let i = 0; i < bytes.length; i++) {
out[i] = ~bytes[i] & 0xFF;
}
return out;
}
function bytesToHex(bytes) {
return Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('');
}
function hexToBytes(hex) {
const cleaned = hex.replace(/\s+/g, '').replace(/0x/gi, '');
if (cleaned.length % 2 !== 0) {
return null;
}
if (!/^[0-9a-fA-F]*$/.test(cleaned)) {
return null;
}
const out = new Uint8Array(cleaned.length / 2);
for (let i = 0; i < out.length; i++) {
out[i] = parseInt(cleaned.slice(i * 2, i * 2 + 2), 16);
}
return out;
}
return new BaseTransformer({
name: 'Bitwise NOT',
priority: 100,
category: 'format',
func: function(text) {
// Invert all bits in each byte
const bytes = new TextEncoder().encode(text);
const result = new Uint8Array(bytes.length);
for (let i = 0; i < bytes.length; i++) {
result[i] = ~bytes[i] & 0xFF; // NOT operation, mask to 8 bits
}
try {
return new TextDecoder().decode(result);
} catch (e) {
// If decoding fails, return as hex
return Array.from(result).map(b => b.toString(16).padStart(2, '0')).join('');
}
const inverted = utf8BytesBitwiseNot(bytes);
return bytesToHex(inverted);
},
reverse: function(text) {
// Bitwise NOT is self-reciprocal (NOT NOT = original)
return this.func(text);
const inverted = hexToBytes(text);
if (!inverted) {
return '[invalid hex - paste the hex from encode; spaces allowed]';
}
const bytes = utf8BytesBitwiseNot(inverted);
try {
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
} catch (e) {
return '[invalid UTF-8 after inverse NOT]';
}
},
preview: function(text) {
if (!text) return '[bitwise-not]';
return this.func(text.slice(0, 5));
const h = this.func(text.slice(0, 5));
return h.length > 24 ? `${h.slice(0, 24)}` : h;
},
detector: function(text) {
// Bitwise NOT produces scrambled text, hard to detect
// Check for non-printable characters or unusual patterns
const hasNonPrintable = /[\x00-\x1F\x7F-\x9F]/.test(text);
return hasNonPrintable && text.length >= 5;
const t = text.trim().replace(/\s+/g, '');
if (t.length < 8 || t.length % 2 !== 0) return false;
return /^[0-9a-fA-F]+$/.test(t);
}
});
});
})();
+23 -6
View File
@@ -5,10 +5,27 @@ export default new BaseTransformer({
name: 'Indent',
priority: 100,
category: 'format',
spaces: 4, // Default indent spaces
func: function(text) {
const spaces = parseInt(this.spaces) || 4;
const indent = ' '.repeat(spaces);
spaces: 4, // Default indent spaces (fallback when options omitted)
configurableOptions: [
{
id: 'spaces',
label: 'Spaces per indent',
type: 'number',
default: 4,
min: 1,
max: 32,
step: 1
}
],
func: function(text, options) {
options = options || {};
let s = options.spaces !== undefined && options.spaces !== ''
? parseInt(options.spaces, 10)
: parseInt(this.spaces, 10) || 4;
if (Number.isNaN(s) || s < 1) {
s = 4;
}
const indent = ' '.repeat(s);
return text.split('\n').map(line => indent + line).join('\n');
},
@@ -16,9 +33,9 @@ export default new BaseTransformer({
// Remove leading spaces from each line
return text.split('\n').map(line => line.replace(/^\s+/, '')).join('\n');
},
preview: function(text) {
preview: function(text, options) {
if (!text) return '[indent]';
return this.func(text.slice(0, 20));
return this.func(text.slice(0, 20), options);
},
detector: function(text) {
// Check if all lines start with same amount of whitespace
+39 -6
View File
@@ -5,15 +5,48 @@ export default new BaseTransformer({
name: 'Line Numbers',
priority: 100,
category: 'format',
start: 1, // Starting line number
func: function(text) {
const start = parseInt(this.start) || 1;
start: 1, // Starting line number (fallback when options omitted)
gutterWidth: 4, // padStart width for the number column
configurableOptions: [
{
id: 'start',
label: 'Starting line number',
type: 'number',
default: 1,
min: 0,
max: 9999999,
step: 1
},
{
id: 'gutterWidth',
label: 'Number column width',
type: 'number',
default: 4,
min: 1,
max: 12,
step: 1
}
],
func: function(text, options) {
options = options || {};
let start = options.start !== undefined && options.start !== ''
? parseInt(options.start, 10)
: parseInt(this.start, 10) || 1;
if (Number.isNaN(start)) {
start = 1;
}
let gutterWidth = options.gutterWidth !== undefined && options.gutterWidth !== ''
? parseInt(options.gutterWidth, 10)
: parseInt(this.gutterWidth, 10) || 4;
if (Number.isNaN(gutterWidth) || gutterWidth < 1) {
gutterWidth = 4;
}
const lines = text.split('\n');
let result = '';
for (let i = 0; i < lines.length; i++) {
const lineNum = start + i;
result += lineNum.toString().padStart(4, ' ') + ': ' + lines[i] + '\n';
result += lineNum.toString().padStart(gutterWidth, ' ') + ': ' + lines[i] + '\n';
}
return result.trimEnd();
@@ -24,9 +57,9 @@ export default new BaseTransformer({
return line.replace(/^\s*\d+\s*:\s*/, '');
}).join('\n');
},
preview: function(text) {
preview: function(text, options) {
if (!text) return '[line-numbers]';
return this.func(text.slice(0, 30));
return this.func(text.slice(0, 30), options);
},
detector: function(text) {
// Check for line number pattern at start of lines
+37 -6
View File
@@ -5,11 +5,42 @@ export default new BaseTransformer({
name: 'Text Justify',
priority: 100,
category: 'format',
width: 80, // Default width
width: 80, // Default width (fallback when options omitted)
align: 'left', // left, right, center
func: function(text) {
const width = parseInt(this.width) || 80;
const align = this.align || 'left';
configurableOptions: [
{
id: 'width',
label: 'Line width (characters)',
type: 'number',
default: 80,
min: 8,
max: 200,
step: 1
},
{
id: 'align',
label: 'Alignment',
type: 'select',
default: 'left',
options: [
{ value: 'left', label: 'Left (pad on the right)' },
{ value: 'right', label: 'Right (pad on the left)' },
{ value: 'center', label: 'Center' }
]
}
],
func: function(text, options) {
options = options || {};
let w = options.width !== undefined && options.width !== ''
? parseInt(options.width, 10)
: parseInt(this.width, 10) || 80;
if (Number.isNaN(w) || w < 1) {
w = 80;
}
const width = w;
const align = options.align !== undefined && options.align !== ''
? options.align
: (this.align || 'left');
const lines = text.split('\n');
let result = '';
@@ -47,9 +78,9 @@ export default new BaseTransformer({
// Remove padding spaces
return text.split('\n').map(line => line.trim()).join('\n');
},
preview: function(text) {
preview: function(text, options) {
if (!text) return '[text-justify]';
return this.func(text.slice(0, 20));
return this.func(text.slice(0, 20), options);
},
detector: function(text) {
// Check for consistent line lengths with padding
+24 -7
View File
@@ -5,11 +5,28 @@ export default new BaseTransformer({
name: 'Word Wrap',
priority: 100,
category: 'format',
width: 80, // Default wrap width
func: function(text) {
const width = parseInt(this.width) || 80;
if (width < 1) return text;
width: 80, // Default wrap width (fallback when options omitted)
configurableOptions: [
{
id: 'width',
label: 'Maximum line width (characters)',
type: 'number',
default: 80,
min: 8,
max: 200,
step: 1
}
],
func: function(text, options) {
options = options || {};
let w = options.width !== undefined && options.width !== ''
? parseInt(options.width, 10)
: parseInt(this.width, 10) || 80;
if (Number.isNaN(w) || w < 1) {
w = 80;
}
const width = w;
const lines = text.split('\n');
let result = '';
@@ -45,9 +62,9 @@ export default new BaseTransformer({
// Remove line breaks (simple approach - may not be perfect)
return text.replace(/\n/g, ' ');
},
preview: function(text) {
preview: function(text, options) {
if (!text) return '[word-wrap]';
return this.func(text.slice(0, 50));
return this.func(text.slice(0, 50), options);
},
detector: function(text) {
// Check if text has consistent line lengths