@sectui/operations 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -2
  3. package/dist/args.d.ts +8 -0
  4. package/dist/args.js +47 -0
  5. package/dist/args.js.map +1 -0
  6. package/dist/cli-flags.d.ts +7 -0
  7. package/dist/cli-flags.js +28 -0
  8. package/dist/cli-flags.js.map +1 -0
  9. package/dist/cli.d.ts +2 -0
  10. package/dist/cli.js +185 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/completions.d.ts +3 -0
  13. package/dist/completions.js +113 -0
  14. package/dist/completions.js.map +1 -0
  15. package/dist/display.d.ts +4 -0
  16. package/dist/display.js +29 -0
  17. package/dist/display.js.map +1 -0
  18. package/dist/errors.d.ts +6 -0
  19. package/dist/errors.js +7 -0
  20. package/dist/errors.js.map +1 -0
  21. package/dist/index.d.ts +4 -0
  22. package/dist/index.js +5 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/operation.d.ts +64 -0
  25. package/dist/operation.js +5 -0
  26. package/dist/operation.js.map +1 -0
  27. package/dist/operations/base/alphabets.d.ts +28 -0
  28. package/dist/operations/base/alphabets.js +27 -0
  29. package/dist/operations/base/alphabets.js.map +1 -0
  30. package/dist/operations/base/base32.d.ts +33 -0
  31. package/dist/operations/base/base32.js +85 -0
  32. package/dist/operations/base/base32.js.map +1 -0
  33. package/dist/operations/base/base58.d.ts +34 -0
  34. package/dist/operations/base/base58.js +43 -0
  35. package/dist/operations/base/base58.js.map +1 -0
  36. package/dist/operations/base/base62.d.ts +28 -0
  37. package/dist/operations/base/base62.js +43 -0
  38. package/dist/operations/base/base62.js.map +1 -0
  39. package/dist/operations/base/base64.d.ts +19 -0
  40. package/dist/operations/base/base64.js +72 -0
  41. package/dist/operations/base/base64.js.map +1 -0
  42. package/dist/operations/base/base64url.d.ts +15 -0
  43. package/dist/operations/base/base64url.js +44 -0
  44. package/dist/operations/base/base64url.js.map +1 -0
  45. package/dist/operations/base/big-number.d.ts +6 -0
  46. package/dist/operations/base/big-number.js +54 -0
  47. package/dist/operations/base/big-number.js.map +1 -0
  48. package/dist/operations/color/hex-to-rgb.d.ts +14 -0
  49. package/dist/operations/color/hex-to-rgb.js +41 -0
  50. package/dist/operations/color/hex-to-rgb.js.map +1 -0
  51. package/dist/operations/data/json-format.d.ts +20 -0
  52. package/dist/operations/data/json-format.js +33 -0
  53. package/dist/operations/data/json-format.js.map +1 -0
  54. package/dist/operations/data/json.d.ts +20 -0
  55. package/dist/operations/data/json.js +59 -0
  56. package/dist/operations/data/json.js.map +1 -0
  57. package/dist/operations/data/yaml.d.ts +21 -0
  58. package/dist/operations/data/yaml.js +54 -0
  59. package/dist/operations/data/yaml.js.map +1 -0
  60. package/dist/operations/extract/extract.d.ts +55 -0
  61. package/dist/operations/extract/extract.js +129 -0
  62. package/dist/operations/extract/extract.js.map +1 -0
  63. package/dist/operations/hash/bcrypt.d.ts +16 -0
  64. package/dist/operations/hash/bcrypt.js +45 -0
  65. package/dist/operations/hash/bcrypt.js.map +1 -0
  66. package/dist/operations/hash/hashes.d.ts +10 -0
  67. package/dist/operations/hash/hashes.js +29 -0
  68. package/dist/operations/hash/hashes.js.map +1 -0
  69. package/dist/operations/hex/from-hex.d.ts +1 -0
  70. package/dist/operations/hex/from-hex.js +31 -0
  71. package/dist/operations/hex/from-hex.js.map +1 -0
  72. package/dist/operations/hex/to-hex.d.ts +31 -0
  73. package/dist/operations/hex/to-hex.js +44 -0
  74. package/dist/operations/hex/to-hex.js.map +1 -0
  75. package/dist/operations/html/entities.d.ts +2 -0
  76. package/dist/operations/html/entities.js +69 -0
  77. package/dist/operations/html/entities.js.map +1 -0
  78. package/dist/operations/html/html-decode.d.ts +1 -0
  79. package/dist/operations/html/html-decode.js +36 -0
  80. package/dist/operations/html/html-decode.js.map +1 -0
  81. package/dist/operations/html/html-encode.d.ts +8 -0
  82. package/dist/operations/html/html-encode.js +30 -0
  83. package/dist/operations/html/html-encode.js.map +1 -0
  84. package/dist/operations/text/case.d.ts +14 -0
  85. package/dist/operations/text/case.js +85 -0
  86. package/dist/operations/text/case.js.map +1 -0
  87. package/dist/operations/text/count.d.ts +19 -0
  88. package/dist/operations/text/count.js +59 -0
  89. package/dist/operations/text/count.js.map +1 -0
  90. package/dist/operations/text/line-operations.d.ts +48 -0
  91. package/dist/operations/text/line-operations.js +134 -0
  92. package/dist/operations/text/line-operations.js.map +1 -0
  93. package/dist/operations/text/lines.d.ts +13 -0
  94. package/dist/operations/text/lines.js +22 -0
  95. package/dist/operations/text/lines.js.map +1 -0
  96. package/dist/operations/text/text-operations.d.ts +37 -0
  97. package/dist/operations/text/text-operations.js +79 -0
  98. package/dist/operations/text/text-operations.js.map +1 -0
  99. package/dist/operations/url/url-decode.d.ts +8 -0
  100. package/dist/operations/url/url-decode.js +56 -0
  101. package/dist/operations/url/url-decode.js.map +1 -0
  102. package/dist/operations/url/url-encode.d.ts +8 -0
  103. package/dist/operations/url/url-encode.js +37 -0
  104. package/dist/operations/url/url-encode.js.map +1 -0
  105. package/dist/registry.d.ts +395 -0
  106. package/dist/registry.js +131 -0
  107. package/dist/registry.js.map +1 -0
  108. package/dist/template.d.ts +23 -0
  109. package/dist/template.js +198 -0
  110. package/dist/template.js.map +1 -0
  111. package/package.json +67 -4
  112. package/src/args.ts +47 -0
  113. package/src/cli-flags.ts +29 -0
  114. package/src/cli.ts +189 -0
  115. package/src/completions.ts +129 -0
  116. package/src/display.ts +29 -0
  117. package/src/errors.ts +6 -0
  118. package/src/index.ts +4 -0
  119. package/src/operation.ts +62 -0
  120. package/src/operations/base/alphabets.ts +33 -0
  121. package/src/operations/base/base32.ts +84 -0
  122. package/src/operations/base/base58.ts +45 -0
  123. package/src/operations/base/base62.ts +44 -0
  124. package/src/operations/base/base64.ts +76 -0
  125. package/src/operations/base/base64url.ts +45 -0
  126. package/src/operations/base/big-number.ts +54 -0
  127. package/src/operations/color/hex-to-rgb.ts +42 -0
  128. package/src/operations/data/json-format.ts +33 -0
  129. package/src/operations/data/json.ts +53 -0
  130. package/src/operations/data/yaml.ts +56 -0
  131. package/src/operations/extract/extract.ts +135 -0
  132. package/src/operations/hash/bcrypt.ts +44 -0
  133. package/src/operations/hash/hashes.ts +64 -0
  134. package/src/operations/hex/from-hex.ts +31 -0
  135. package/src/operations/hex/to-hex.ts +46 -0
  136. package/src/operations/html/entities.ts +71 -0
  137. package/src/operations/html/html-decode.ts +36 -0
  138. package/src/operations/html/html-encode.ts +31 -0
  139. package/src/operations/text/case.ts +140 -0
  140. package/src/operations/text/count.ts +61 -0
  141. package/src/operations/text/line-operations.ts +144 -0
  142. package/src/operations/text/lines.ts +22 -0
  143. package/src/operations/text/text-operations.ts +85 -0
  144. package/src/operations/url/url-decode.ts +55 -0
  145. package/src/operations/url/url-encode.ts +38 -0
  146. package/src/registry.ts +152 -0
  147. package/src/template.ts +214 -0
@@ -0,0 +1,129 @@
1
+ import { cliFlag } from "./cli-flags.ts";
2
+ import type { Operation } from "./operation.ts";
3
+ import { operations } from "./registry.ts";
4
+
5
+ export const SHELLS = ["bash", "zsh", "fish"] as const;
6
+ export type Shell = (typeof SHELLS)[number];
7
+
8
+ const COMMANDS = [
9
+ ["list", "List available operations"],
10
+ ["help", "Show help"],
11
+ ["render", "Replace operation{...} blocks in text"],
12
+ ["completions", "Print a shell completion script"],
13
+ ] as const;
14
+
15
+ const GLOBAL_FLAGS = [
16
+ ["--raw", "Write raw bytes even to a terminal"],
17
+ ["--help", "Show help"],
18
+ ] as const;
19
+
20
+ const ops = operations as readonly Operation[];
21
+
22
+ /** Wraps a string in single quotes for any POSIX-like shell. */
23
+ const sq = (s: string) => `'${s.replaceAll("'", `'\\''`)}'`;
24
+
25
+ interface Flag {
26
+ flag: string;
27
+ description: string;
28
+ values?: string[];
29
+ }
30
+
31
+ function flagsOf(op: Operation): Flag[] {
32
+ return Object.entries(op.args).map(([key, def]) => ({
33
+ flag: cliFlag(key, def),
34
+ description: def.name,
35
+ values: def.type === "select" ? def.options.map((o) => o.value) : def.type === "boolean" ? undefined : [],
36
+ }));
37
+ }
38
+
39
+ function bash(): string {
40
+ const ids = ops.map((op) => op.id).join(" ");
41
+ const cases = ops.map((op) => {
42
+ const flags = flagsOf(op);
43
+ const valueCases = flags
44
+ .filter((f) => f.values)
45
+ .map((f) => ` ${f.flag}) COMPREPLY=($(compgen -W ${sq(f.values!.join(" "))} -- "$cur")); return ;;`);
46
+ const all = [...flags.map((f) => f.flag), ...GLOBAL_FLAGS.map(([f]) => f)].join(" ");
47
+ return [
48
+ ` ${op.id})`,
49
+ ...(valueCases.length ? [' case "$prev" in', ...valueCases, " esac"] : []),
50
+ ` [[ $cur == -* ]] && COMPREPLY=($(compgen -W ${sq(all)} -- "$cur"))`,
51
+ " ;;",
52
+ ].join("\n");
53
+ });
54
+ return `# bash completion for ops
55
+ _ops() {
56
+ local cur="\${COMP_WORDS[COMP_CWORD]}" prev="\${COMP_WORDS[COMP_CWORD-1]}"
57
+ if (( COMP_CWORD == 1 )); then
58
+ COMPREPLY=($(compgen -W ${sq(`${COMMANDS.map(([c]) => c).join(" ")} ${ids}`)} -- "$cur"))
59
+ return
60
+ fi
61
+ case "\${COMP_WORDS[1]}" in
62
+ help) (( COMP_CWORD == 2 )) && COMPREPLY=($(compgen -W ${sq(ids)} -- "$cur")) ;;
63
+ completions) (( COMP_CWORD == 2 )) && COMPREPLY=($(compgen -W ${sq(SHELLS.join(" "))} -- "$cur")) ;;
64
+ render) [[ $cur == -* ]] && COMPREPLY=($(compgen -W ${sq(GLOBAL_FLAGS.map(([f]) => f).join(" "))} -- "$cur")) ;;
65
+ ${cases.join("\n")}
66
+ esac
67
+ }
68
+ complete -F _ops ops
69
+ `;
70
+ }
71
+
72
+ function zsh(): string {
73
+ // Escapes characters that are special in _describe entries and _arguments specs.
74
+ const esc = (s: string) => s.replace(/([:[\]\\])/g, "\\$1");
75
+ const describe = (items: (readonly [string, string])[]) => items.map(([n, d]) => sq(`${esc(n)}:${esc(d)}`)).join(" ");
76
+ const globals = GLOBAL_FLAGS.map(([f, d]) => sq(`${f}[${esc(d)}]`)).join(" ");
77
+ const cases = ops.map((op) => {
78
+ const specs = flagsOf(op).map((f) => {
79
+ const action = f.values === undefined ? "" : f.values.length ? `:value:(${f.values.join(" ")})` : ":value:";
80
+ return sq(`${f.flag}[${esc(f.description)}]${action}`);
81
+ });
82
+ return ` ${op.id}) _arguments ${[...specs, globals, sq("*:input:")].join(" ")} ;;`;
83
+ });
84
+ return `#compdef ops
85
+ _ops() {
86
+ if (( CURRENT == 2 )); then
87
+ local -a commands=(${describe([...COMMANDS, ...ops.map((op) => [op.id, op.name] as const)])})
88
+ _describe command commands
89
+ return
90
+ fi
91
+ local cmd=$words[2]
92
+ shift words
93
+ (( CURRENT-- ))
94
+ case $cmd in
95
+ help) (( CURRENT == 2 )) && { local -a ids=(${describe(ops.map((op) => [op.id, op.name] as const))}); _describe operation ids } ;;
96
+ completions) (( CURRENT == 2 )) && _values shell ${SHELLS.join(" ")} ;;
97
+ render) _arguments ${globals} ${sq("*:text:")} ;;
98
+ ${cases.join("\n")}
99
+ esac
100
+ }
101
+ if [[ $funcstack[1] == _ops ]]; then _ops "$@"; else compdef _ops ops; fi
102
+ `;
103
+ }
104
+
105
+ function fish(): string {
106
+ const lines = ["# fish completion for ops", "complete -c ops -f"];
107
+ for (const [command, description] of COMMANDS) {
108
+ lines.push(`complete -c ops -n __fish_use_subcommand -a ${command} -d ${sq(description)}`);
109
+ }
110
+ for (const op of ops) lines.push(`complete -c ops -n __fish_use_subcommand -a ${op.id} -d ${sq(op.name)}`);
111
+ const ids = sq(ops.map((op) => op.id).join(" "));
112
+ lines.push(`complete -c ops -n '__fish_seen_subcommand_from help' -a ${ids}`);
113
+ lines.push(`complete -c ops -n '__fish_seen_subcommand_from completions' -a ${sq(SHELLS.join(" "))}`);
114
+ const withGlobals = ["render", ...ops.map((op) => op.id)].join(" ");
115
+ for (const [flag, description] of GLOBAL_FLAGS) {
116
+ lines.push(`complete -c ops -n ${sq(`__fish_seen_subcommand_from ${withGlobals}`)} -l ${flag.slice(2)} -d ${sq(description)}`);
117
+ }
118
+ for (const op of ops) {
119
+ for (const f of flagsOf(op)) {
120
+ const values = f.values === undefined ? "" : f.values.length ? ` -x -a ${sq(f.values.join(" "))}` : " -x";
121
+ lines.push(`complete -c ops -n ${sq(`__fish_seen_subcommand_from ${op.id}`)} -l ${f.flag.slice(2)}${values} -d ${sq(f.description)}`);
122
+ }
123
+ }
124
+ return `${lines.join("\n")}\n`;
125
+ }
126
+
127
+ export function completions(shell: Shell): string {
128
+ return { bash, zsh, fish }[shell]();
129
+ }
package/src/display.ts ADDED
@@ -0,0 +1,29 @@
1
+ // C0 controls except tab/LF/CR, DEL, C1 controls (e.g. U+009B is a CSI on some terminals),
2
+ // and bidi overrides that can make text display differently from its content.
3
+ const UNSAFE_CHARS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f‪-‮⁦-⁩]/;
4
+
5
+ /** True if the bytes are valid UTF-8 text without characters that can control a terminal. */
6
+ export function isPrintable(bytes: Uint8Array): boolean {
7
+ let text: string;
8
+ try {
9
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
10
+ } catch {
11
+ return false;
12
+ }
13
+ return !UNSAFE_CHARS.test(text);
14
+ }
15
+
16
+ /** Canonical hex+ASCII dump, like `hexdump -C`. */
17
+ export function hexdump(bytes: Uint8Array): string {
18
+ const lines: string[] = [];
19
+ for (let offset = 0; offset < bytes.length; offset += 16) {
20
+ const row = bytes.subarray(offset, offset + 16);
21
+ const hex = [...row].map((b) => b.toString(16).padStart(2, "0"));
22
+ const left = hex.slice(0, 8).join(" ");
23
+ const right = hex.slice(8).join(" ");
24
+ const ascii = [...row].map((b) => (b >= 0x20 && b < 0x7f ? String.fromCharCode(b) : ".")).join("");
25
+ lines.push(`${offset.toString(16).padStart(8, "0")} ${left.padEnd(23)} ${right.padEnd(23)} |${ascii}|`);
26
+ }
27
+ lines.push(bytes.length.toString(16).padStart(8, "0"));
28
+ return lines.join("\n");
29
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,6 @@
1
+ /** Error raised when an operation cannot process its input or arguments. */
2
+ export class OperationError extends Error {
3
+ override name = "OperationError";
4
+ /** Id of the failing operation, set by `run`. */
5
+ operation?: string;
6
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./display.ts";
2
+ export * from "./errors.ts";
3
+ export * from "./operation.ts";
4
+ export * from "./registry.ts";
@@ -0,0 +1,62 @@
1
+ export type Category = "Encoding" | "Encryption" | "Hashing" | "Compression" | "Data format" | "Text" | "Extractors" | "Utils";
2
+
3
+ interface BaseArg<T> {
4
+ name: string;
5
+ description?: string;
6
+ default: T;
7
+ }
8
+
9
+ export type ArgDef =
10
+ | ({ type: "boolean" } & BaseArg<boolean>)
11
+ | ({ type: "string" } & BaseArg<string>)
12
+ | ({ type: "number"; min?: number; max?: number } & BaseArg<number>)
13
+ | ({ type: "select"; options: readonly { value: string; label: string }[] } & BaseArg<string>);
14
+
15
+ export type ArgDefs = Record<string, ArgDef>;
16
+
17
+ /** A select arg resolves to the union of its option values. */
18
+ export type ArgValue<D extends ArgDef> = D extends { type: "select"; options: readonly { value: infer V }[] }
19
+ ? V
20
+ : D extends { type: "boolean" }
21
+ ? boolean
22
+ : D extends { type: "number" }
23
+ ? number
24
+ : string;
25
+
26
+ export type ArgValues<A extends ArgDefs> = {
27
+ [K in keyof A]: ArgValue<A[K]>;
28
+ };
29
+
30
+ export interface Example<A extends ArgDefs = ArgDefs> {
31
+ input: string;
32
+ args?: Partial<ArgValues<A>>;
33
+ /** Expected output, decoded as UTF-8. */
34
+ output: string;
35
+ }
36
+
37
+ export interface Operation<Id extends string = string, A extends ArgDefs = ArgDefs, Inverse extends string = string> {
38
+ /** Stable kebab-case id, used in templates and recipes. */
39
+ id: Id;
40
+ name: string;
41
+ description: string;
42
+ category: Category;
43
+ /** `binary` when the output may contain arbitrary bytes, so callers know not to print it as text. */
44
+ output: "text" | "binary";
45
+ /** Extra search terms. */
46
+ aliases?: readonly string[];
47
+ /** Id of the operation that undoes this one. */
48
+ inverse?: Inverse;
49
+ infoUrl?: string;
50
+ /** Keyed by arg id, in display order. */
51
+ args: A;
52
+ /** Shown in the docs and the CLI help, and checked by the test suite. */
53
+ examples: readonly [Example<A>, ...Example<A>[]];
54
+ run(input: Uint8Array, args: ArgValues<A>): Uint8Array;
55
+ }
56
+
57
+ /** Identity function that preserves the literal types of the id, args and inverse. */
58
+ export function defineOperation<const Id extends string, const A extends ArgDefs, const Inverse extends string = never>(
59
+ op: Operation<Id, A, Inverse>,
60
+ ): Operation<Id, A, Inverse> {
61
+ return op;
62
+ }
@@ -0,0 +1,33 @@
1
+ export const BASE32_ALPHABETS = [
2
+ { value: "base32", label: "Standard (RFC 4648): A-Z2-7=" },
3
+ { value: "base32hex", label: "Extended hex (RFC 4648 §7): 0-9A-V=" },
4
+ ] as const;
5
+
6
+ export type Base32Alphabet = (typeof BASE32_ALPHABETS)[number]["value"];
7
+
8
+ export const BASE32_CHARS: Record<Base32Alphabet, string> = {
9
+ base32: "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",
10
+ base32hex: "0123456789ABCDEFGHIJKLMNOPQRSTUV",
11
+ };
12
+
13
+ export const BASE58_ALPHABETS = [
14
+ { value: "bitcoin", label: "Bitcoin: 1-9A-HJ-NP-Za-km-z" },
15
+ { value: "flickr", label: "Flickr: 1-9a-km-zA-HJ-NP-Z" },
16
+ { value: "ripple", label: "Ripple: rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz" },
17
+ ] as const;
18
+
19
+ export const BASE58_CHARS: Record<(typeof BASE58_ALPHABETS)[number]["value"], string> = {
20
+ bitcoin: "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz",
21
+ flickr: "123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ",
22
+ ripple: "rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz",
23
+ };
24
+
25
+ export const BASE62_ALPHABETS = [
26
+ { value: "0-9A-Za-z", label: "Digits, uppercase, lowercase (GMP)" },
27
+ { value: "0-9a-zA-Z", label: "Digits, lowercase, uppercase" },
28
+ ] as const;
29
+
30
+ export const BASE62_CHARS: Record<(typeof BASE62_ALPHABETS)[number]["value"], string> = {
31
+ "0-9A-Za-z": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
32
+ "0-9a-zA-Z": "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
33
+ };
@@ -0,0 +1,84 @@
1
+ import { OperationError } from "../../errors.ts";
2
+ import { defineOperation } from "../../operation.ts";
3
+ import { BASE32_ALPHABETS, BASE32_CHARS } from "./alphabets.ts";
4
+
5
+ export const toBase32 = defineOperation({
6
+ id: "to-base32",
7
+ name: "To Base32",
8
+ description: 'Encodes raw bytes as Base32 text. e.g. "hello" becomes "NBSWY3DP".',
9
+ category: "Encoding",
10
+ output: "text",
11
+ aliases: ["b32", "base32 encode"],
12
+ inverse: "from-base32",
13
+ infoUrl: "https://wikipedia.org/wiki/Base32",
14
+ args: {
15
+ alphabet: { type: "select", name: "Alphabet", options: BASE32_ALPHABETS, default: "base32" },
16
+ padding: { type: "boolean", name: "Padding (=)", default: true },
17
+ },
18
+ examples: [
19
+ { input: "hello", output: "NBSWY3DP" },
20
+ { input: "hi", output: "NBUQ====" },
21
+ { input: "hi", args: { alphabet: "base32hex", padding: false }, output: "D1KG" },
22
+ ],
23
+ run(input, { alphabet, padding }) {
24
+ const chars = BASE32_CHARS[alphabet];
25
+ let out = "";
26
+ let buffer = 0;
27
+ let bits = 0;
28
+ for (const byte of input) {
29
+ buffer = ((buffer << 8) | byte) & 0xfff;
30
+ bits += 8;
31
+ while (bits >= 5) {
32
+ bits -= 5;
33
+ out += chars[(buffer >> bits) & 31];
34
+ }
35
+ }
36
+ if (bits > 0) out += chars[(buffer << (5 - bits)) & 31];
37
+ if (padding) out = out.padEnd(Math.ceil(out.length / 8) * 8, "=");
38
+ return new TextEncoder().encode(out);
39
+ },
40
+ });
41
+
42
+ // Number of characters in a final group that can encode whole bytes (2 -> 1 byte, 4 -> 2, 5 -> 3, 7 -> 4).
43
+ const VALID_TAIL = new Set([0, 2, 4, 5, 7]);
44
+
45
+ export const fromBase32 = defineOperation({
46
+ id: "from-base32",
47
+ name: "From Base32",
48
+ description: 'Decodes Base32 text into raw bytes, ignoring case, whitespace and padding. e.g. "NBSWY3DP" becomes "hello".',
49
+ category: "Encoding",
50
+ output: "binary",
51
+ aliases: ["b32", "base32 decode"],
52
+ inverse: "to-base32",
53
+ infoUrl: "https://wikipedia.org/wiki/Base32",
54
+ args: {
55
+ alphabet: { type: "select", name: "Alphabet", options: BASE32_ALPHABETS, default: "base32" },
56
+ },
57
+ examples: [
58
+ { input: "NBSWY3DP", output: "hello" },
59
+ { input: "nbuq====", output: "hi" },
60
+ { input: "D1KG", args: { alphabet: "base32hex" }, output: "hi" },
61
+ ],
62
+ run(input, { alphabet }) {
63
+ const chars = BASE32_CHARS[alphabet];
64
+ const text = new TextDecoder().decode(input).replace(/\s+/g, "").replace(/=+$/, "").toUpperCase();
65
+ if (!VALID_TAIL.has(text.length % 8)) {
66
+ throw new OperationError("Invalid Base32: truncated input (the last group has an impossible length)");
67
+ }
68
+ const out = new Uint8Array(Math.floor((text.length * 5) / 8));
69
+ let n = 0;
70
+ let buffer = 0;
71
+ let bits = 0;
72
+ for (let i = 0; i < text.length; i++) {
73
+ const value = chars.indexOf(text[i]!);
74
+ if (value < 0) throw new OperationError(`Invalid Base32: unexpected character "${text[i]}"`);
75
+ buffer = ((buffer << 5) | value) & 0xfff;
76
+ bits += 5;
77
+ if (bits >= 8) {
78
+ bits -= 8;
79
+ out[n++] = (buffer >> bits) & 0xff;
80
+ }
81
+ }
82
+ return out;
83
+ },
84
+ });
@@ -0,0 +1,45 @@
1
+ import { defineOperation } from "../../operation.ts";
2
+ import { BASE58_ALPHABETS, BASE58_CHARS } from "./alphabets.ts";
3
+ import { decodeBaseX, encodeBaseX } from "./big-number.ts";
4
+
5
+ export const toBase58 = defineOperation({
6
+ id: "to-base58",
7
+ name: "To Base58",
8
+ description:
9
+ 'Encodes raw bytes as Base58 text, as used by Bitcoin addresses. Leading zero bytes become "1". e.g. "hello" becomes "Cn8eVZg".',
10
+ category: "Encoding",
11
+ output: "text",
12
+ aliases: ["b58", "base58 encode", "bitcoin"],
13
+ inverse: "from-base58",
14
+ infoUrl: "https://wikipedia.org/wiki/Binary-to-text_encoding#Base58",
15
+ args: {
16
+ alphabet: { type: "select", name: "Alphabet", options: BASE58_ALPHABETS, default: "bitcoin" },
17
+ },
18
+ examples: [
19
+ { input: "hello", output: "Cn8eVZg" },
20
+ { input: "Hello World!", output: "2NEpo7TZRRrLZSi2U" },
21
+ ],
22
+ run: (input, { alphabet }) => new TextEncoder().encode(encodeBaseX(input, BASE58_CHARS[alphabet])),
23
+ });
24
+
25
+ export const fromBase58 = defineOperation({
26
+ id: "from-base58",
27
+ name: "From Base58",
28
+ description: 'Decodes Base58 text into raw bytes, ignoring whitespace. e.g. "Cn8eVZg" becomes "hello".',
29
+ category: "Encoding",
30
+ output: "binary",
31
+ aliases: ["b58", "base58 decode", "bitcoin"],
32
+ inverse: "to-base58",
33
+ infoUrl: "https://wikipedia.org/wiki/Binary-to-text_encoding#Base58",
34
+ args: {
35
+ alphabet: { type: "select", name: "Alphabet", options: BASE58_ALPHABETS, default: "bitcoin" },
36
+ },
37
+ examples: [
38
+ { input: "Cn8eVZg", output: "hello" },
39
+ { input: "2NEpo7TZRRrLZSi2U", output: "Hello World!" },
40
+ ],
41
+ run(input, { alphabet }) {
42
+ const text = new TextDecoder().decode(input).replace(/\s+/g, "");
43
+ return decodeBaseX(text, BASE58_CHARS[alphabet], "Base58");
44
+ },
45
+ });
@@ -0,0 +1,44 @@
1
+ import { defineOperation } from "../../operation.ts";
2
+ import { BASE62_ALPHABETS, BASE62_CHARS } from "./alphabets.ts";
3
+ import { decodeBaseX, encodeBaseX } from "./big-number.ts";
4
+
5
+ export const toBase62 = defineOperation({
6
+ id: "to-base62",
7
+ name: "To Base62",
8
+ description: 'Encodes raw bytes as Base62 text (letters and digits only). Leading zero bytes become "0". e.g. "hello" becomes "7tQLFHz".',
9
+ category: "Encoding",
10
+ output: "text",
11
+ aliases: ["b62", "base62 encode"],
12
+ inverse: "from-base62",
13
+ infoUrl: "https://wikipedia.org/wiki/Base62",
14
+ args: {
15
+ alphabet: { type: "select", name: "Alphabet", options: BASE62_ALPHABETS, default: "0-9A-Za-z" },
16
+ },
17
+ examples: [
18
+ { input: "hello", output: "7tQLFHz" },
19
+ { input: "hello", args: { alphabet: "0-9a-zA-Z" }, output: "7TqlfhZ" },
20
+ ],
21
+ run: (input, { alphabet }) => new TextEncoder().encode(encodeBaseX(input, BASE62_CHARS[alphabet])),
22
+ });
23
+
24
+ export const fromBase62 = defineOperation({
25
+ id: "from-base62",
26
+ name: "From Base62",
27
+ description: 'Decodes Base62 text into raw bytes, ignoring whitespace. e.g. "7tQLFHz" becomes "hello".',
28
+ category: "Encoding",
29
+ output: "binary",
30
+ aliases: ["b62", "base62 decode"],
31
+ inverse: "to-base62",
32
+ infoUrl: "https://wikipedia.org/wiki/Base62",
33
+ args: {
34
+ alphabet: { type: "select", name: "Alphabet", options: BASE62_ALPHABETS, default: "0-9A-Za-z" },
35
+ },
36
+ examples: [
37
+ { input: "7tQLFHz", output: "hello" },
38
+ { input: "7TqlfhZ", args: { alphabet: "0-9a-zA-Z" }, output: "hello" },
39
+ ],
40
+ run(input, { alphabet }) {
41
+ const text = new TextDecoder().decode(input).replace(/\s+/g, "");
42
+ return decodeBaseX(text, BASE62_CHARS[alphabet], "Base62");
43
+ },
44
+ });
@@ -0,0 +1,76 @@
1
+ import { OperationError } from "../../errors.ts";
2
+ import { defineOperation } from "../../operation.ts";
3
+
4
+ type Base64Alphabet = "base64" | "base64url";
5
+
6
+ const NON_ALPHABET: Record<Base64Alphabet, RegExp> = {
7
+ base64: /[^A-Za-z0-9+/]/g,
8
+ base64url: /[^A-Za-z0-9\-_]/g,
9
+ };
10
+
11
+ function invalidInput(text: string, alphabet: Base64Alphabet, strict: boolean): OperationError {
12
+ if (strict) {
13
+ const unpadded = text.replace(/=+$/, "");
14
+ const bad = new RegExp(NON_ALPHABET[alphabet].source).exec(unpadded);
15
+ if (bad) return new OperationError(`Invalid Base64: unexpected character "${bad[0]}" at offset ${bad.index}`);
16
+ return new OperationError("Invalid Base64: missing or incorrect padding");
17
+ }
18
+ return new OperationError("Invalid Base64: truncated input (the last group has a single character)");
19
+ }
20
+
21
+ export function encodeBase64(input: Uint8Array, alphabet: Base64Alphabet, padding: boolean): Uint8Array {
22
+ return new TextEncoder().encode(input.toBase64({ alphabet, omitPadding: !padding }));
23
+ }
24
+
25
+ export function decodeBase64(input: Uint8Array, alphabet: Base64Alphabet, strict: boolean): Uint8Array {
26
+ let text = new TextDecoder().decode(input);
27
+ if (!strict) text = text.replace(NON_ALPHABET[alphabet], "");
28
+ try {
29
+ return Uint8Array.fromBase64(text, { alphabet, lastChunkHandling: strict ? "strict" : "loose" });
30
+ } catch {
31
+ throw invalidInput(text, alphabet, strict);
32
+ }
33
+ }
34
+
35
+ export const toBase64 = defineOperation({
36
+ id: "to-base64",
37
+ name: "To Base64",
38
+ description: 'Encodes raw bytes as Base64 text. e.g. "hello" becomes "aGVsbG8=".',
39
+ category: "Encoding",
40
+ output: "text",
41
+ aliases: ["b64", "base64 encode"],
42
+ inverse: "from-base64",
43
+ infoUrl: "https://wikipedia.org/wiki/Base64",
44
+ args: {
45
+ padding: { type: "boolean", name: "Padding (=)", default: true },
46
+ },
47
+ examples: [
48
+ { input: "hello", output: "aGVsbG8=" },
49
+ { input: "hello", args: { padding: false }, output: "aGVsbG8" },
50
+ ],
51
+ run: (input, { padding }) => encodeBase64(input, "base64", padding),
52
+ });
53
+
54
+ export const fromBase64 = defineOperation({
55
+ id: "from-base64",
56
+ name: "From Base64",
57
+ description: 'Decodes Base64 text into raw bytes. e.g. "aGVsbG8=" becomes "hello".',
58
+ category: "Encoding",
59
+ output: "binary",
60
+ aliases: ["b64", "base64 decode"],
61
+ inverse: "to-base64",
62
+ infoUrl: "https://wikipedia.org/wiki/Base64",
63
+ args: {
64
+ strict: {
65
+ type: "boolean",
66
+ name: "Strict mode",
67
+ description: "Fail on non-alphabet characters and invalid padding instead of ignoring them.",
68
+ default: false,
69
+ },
70
+ },
71
+ examples: [
72
+ { input: "aGVsbG8=", output: "hello" },
73
+ { input: "aGVs\nbG8", output: "hello" },
74
+ ],
75
+ run: (input, { strict }) => decodeBase64(input, "base64", strict),
76
+ });
@@ -0,0 +1,45 @@
1
+ import { defineOperation } from "../../operation.ts";
2
+ import { decodeBase64, encodeBase64 } from "./base64.ts";
3
+
4
+ export const toBase64url = defineOperation({
5
+ id: "to-base64url",
6
+ name: "To Base64url",
7
+ description: 'Encodes raw bytes as URL-safe Base64 (- and _ instead of + and /), without padding by default. e.g. "ÿÿ" becomes "w7_Dvw".',
8
+ category: "Encoding",
9
+ output: "text",
10
+ aliases: ["b64url", "base64url encode"],
11
+ inverse: "from-base64url",
12
+ infoUrl: "https://datatracker.ietf.org/doc/html/rfc4648#section-5",
13
+ args: {
14
+ padding: { type: "boolean", name: "Padding (=)", default: false },
15
+ },
16
+ examples: [
17
+ { input: "ÿÿ", output: "w7_Dvw" },
18
+ { input: "ÿÿ", args: { padding: true }, output: "w7_Dvw==" },
19
+ ],
20
+ run: (input, { padding }) => encodeBase64(input, "base64url", padding),
21
+ });
22
+
23
+ export const fromBase64url = defineOperation({
24
+ id: "from-base64url",
25
+ name: "From Base64url",
26
+ description: 'Decodes URL-safe Base64 (- and _ instead of + and /) into raw bytes. e.g. "w7_Dvw" becomes "ÿÿ".',
27
+ category: "Encoding",
28
+ output: "binary",
29
+ aliases: ["b64url", "base64url decode", "jwt"],
30
+ inverse: "to-base64url",
31
+ infoUrl: "https://datatracker.ietf.org/doc/html/rfc4648#section-5",
32
+ args: {
33
+ strict: {
34
+ type: "boolean",
35
+ name: "Strict mode",
36
+ description: "Fail on non-alphabet characters and invalid padding instead of ignoring them.",
37
+ default: false,
38
+ },
39
+ },
40
+ examples: [
41
+ { input: "w7_Dvw", output: "ÿÿ" },
42
+ { input: "w7_Dvw==", output: "ÿÿ" },
43
+ ],
44
+ run: (input, { strict }) => decodeBase64(input, "base64url", strict),
45
+ });
@@ -0,0 +1,54 @@
1
+ import { OperationError } from "../../errors.ts";
2
+
3
+ /**
4
+ * Encodes bytes as a big number in the given base. Each leading zero byte becomes the alphabet's
5
+ * first character, so they survive a round trip (as in Bitcoin's Base58).
6
+ */
7
+ export function encodeBaseX(input: Uint8Array, alphabet: string): string {
8
+ const base = alphabet.length;
9
+ let zeros = 0;
10
+ while (zeros < input.length && input[zeros] === 0) zeros++;
11
+ // Little-endian digits of the number.
12
+ const digits: number[] = [];
13
+ for (let i = zeros; i < input.length; i++) {
14
+ let carry = input[i]!;
15
+ for (let j = 0; j < digits.length; j++) {
16
+ carry += digits[j]! << 8;
17
+ digits[j] = carry % base;
18
+ carry = Math.floor(carry / base);
19
+ }
20
+ while (carry > 0) {
21
+ digits.push(carry % base);
22
+ carry = Math.floor(carry / base);
23
+ }
24
+ }
25
+ return (
26
+ alphabet[0]!.repeat(zeros) +
27
+ digits
28
+ .reverse()
29
+ .map((d) => alphabet[d])
30
+ .join("")
31
+ );
32
+ }
33
+
34
+ export function decodeBaseX(text: string, alphabet: string, name: string): Uint8Array {
35
+ const base = alphabet.length;
36
+ let zeros = 0;
37
+ while (zeros < text.length && text[zeros] === alphabet[0]) zeros++;
38
+ // Little-endian bytes of the number.
39
+ const bytes: number[] = [];
40
+ for (let i = zeros; i < text.length; i++) {
41
+ let carry = alphabet.indexOf(text[i]!);
42
+ if (carry < 0) throw new OperationError(`Invalid ${name}: unexpected character "${text[i]}"`);
43
+ for (let j = 0; j < bytes.length; j++) {
44
+ carry += bytes[j]! * base;
45
+ bytes[j] = carry & 0xff;
46
+ carry >>= 8;
47
+ }
48
+ while (carry > 0) {
49
+ bytes.push(carry & 0xff);
50
+ carry >>= 8;
51
+ }
52
+ }
53
+ return new Uint8Array([...new Array<number>(zeros).fill(0), ...bytes.reverse()]);
54
+ }
@@ -0,0 +1,42 @@
1
+ import { OperationError } from "../../errors.ts";
2
+ import { defineOperation } from "../../operation.ts";
3
+ import { mapLines, mapText } from "../text/lines.ts";
4
+
5
+ const FORMATS = [
6
+ { value: "css", label: "CSS: rgb(255, 136, 0) or rgba(255, 136, 0, 0.5)" },
7
+ { value: "values", label: "Values: 255, 136, 0" },
8
+ ] as const;
9
+
10
+ function toRgb(color: string, format: (typeof FORMATS)[number]["value"]): string {
11
+ const hex = color.trim().replace(/^#/, "");
12
+ if (!/^(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(hex)) {
13
+ throw new OperationError(`Invalid hex color "${color.trim()}": expected #rgb, #rgba, #rrggbb or #rrggbbaa`);
14
+ }
15
+ const full = hex.length <= 4 ? [...hex].map((c) => c + c).join("") : hex;
16
+ const [r, g, b, a] = full.match(/../g)!.map((pair) => parseInt(pair, 16));
17
+ const values = [r, g, b];
18
+ // Alpha as a fraction, rounded to what 8 bits can tell apart.
19
+ if (a !== undefined) values.push(Math.round((a / 255) * 1000) / 1000);
20
+ if (format === "values") return values.join(", ");
21
+ return `${a === undefined ? "rgb" : "rgba"}(${values.join(", ")})`;
22
+ }
23
+
24
+ export const hexToRgb = defineOperation({
25
+ id: "hex-to-rgb",
26
+ name: "Hex to RGB",
27
+ description: 'Converts hex colors (one per line) to RGB. e.g. "#ff8800" becomes "rgb(255, 136, 0)".',
28
+ category: "Data format",
29
+ output: "binary",
30
+ aliases: ["color", "colour", "css color"],
31
+ infoUrl: "https://developer.mozilla.org/docs/Web/CSS/hex-color",
32
+ args: {
33
+ format: { type: "select", name: "Format", options: FORMATS, default: "css" },
34
+ },
35
+ examples: [
36
+ { input: "#ff8800", output: "rgb(255, 136, 0)" },
37
+ { input: "#f80\n#FF880080", output: "rgb(255, 136, 0)\nrgba(255, 136, 0, 0.502)" },
38
+ { input: "ff8800", args: { format: "values" }, output: "255, 136, 0" },
39
+ ],
40
+ run: (input, { format }) =>
41
+ mapText(input, (text) => mapLines(text, (lines) => lines.map((line) => (line.trim() ? toRgb(line, format) : line)))),
42
+ });