Encode and decode
Two methods
import { create, resolveCipher } from "@agntn/ciphers";
const caesar = create("caesar");
caesar.encode("Hello, World!", { shift: 7 }).text; // "Olssv, Dvysk!"
caesar.decode("Olssv, Dvysk!", { shift: 7 }).text; // "Hello, World!"
resolveCipher("Rail Fence").encode("WEAREDISCOVEREDRUNATONCE", { rails: 3 }).text;
// "WECRUOERDSOEERNTNEAIVDAC"
create wants the exact registry name. resolveCipher lowercases and turns runs of whitespace into a hyphen, so "Rail Fence" and "rail-fence" are the same thing - and then it also wants an exact match. Both cache one instance per name.
Options every cipher shares
| Option | Default | Effect |
|---|---|---|
preserveCase | true | Lowercase in, lowercase out. false uppercases the output. |
stripNonAlpha | false | Drop everything that is not an ASCII letter before the cipher runs. |
With the defaults a Caesar leaves spaces, digits and punctuation exactly where they were and shifts the letters around them. A puzzle that wants one solid block gets stripNonAlpha: true:
create("vigenere").encode("ATTACK AT DAWN", { key: "LEMON", stripNonAlpha: true }).text;
// "LXFOPVEFRNHR"
options on the result echoes what was applied, both flags included when you set them. One log line and you can rerun the call.
Ciphers that reshape the input
Some ciphers can't leave the input alone, so the round trip comes back reshaped:
- Playfair works on letter pairs. It strips non-letters, folds J into I, splits doubled letters with X and pads an odd length with X.
- ADFGVX keeps letters and digits and drops the rest.
- Bifid and Polybius fold J into I, and Bifid drops non-letters.
- Morse, Bacon and tap code change the alphabet entirely. You get uppercase back and, for Morse, single spaces.
For everything else decode(encode(x)) === x holds for any input. Emoji, Polish letters, whatever - they pass through untouched.
Self-inverse ciphers
ROT-13, ROT-47, Atbash and Enigma are their own inverse. encode and decode do the same thing and info().selfInverse says so. Both methods still exist on every cipher, so you can write decode and mean it.
const rot13 = create("rot13");
rot13.decode(rot13.encode("HELLO").text).text === "HELLO"; // true
rot13.encode("URYYB").text; // "HELLO", same thing
Options per cipher
Each cipher validates its own options and lists them in info().options with a type, whether it is required, a default and a description. The Playground form and ciphers info are built from that list, nothing is typed twice:
create("affine").info().options;
// [
// { name: "a", type: "number", required: false, default: 5, description: "Multiplier (must be coprime with 26: ...)" },
// { name: "b", type: "number", required: false, default: 8, description: "Additive shift (0-25)" },
// ]
In your own cipher read one with getOpt(options, "shift", 3). Fallback when the key is absent, the value as typed otherwise.
What a wrong option does
create("caesar").encode("X", { shift: 0 }); // InvalidOptionError, 0 is the identity
create("affine").encode("X", { a: 13 }); // InvalidOptionError, gcd(13, 26) is 13
create("vigenere").encode("X"); // MissingOptionError: key
create("enigma").encode("X", { plugboard: "AB AC" }); // InvalidOptionError, a letter in two pairs
All of them thrown before the first character is touched. A cipher that quietly accepts shift 0 and hands your text back unchanged is worse than one that throws.