@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,33 @@
1
+ import { defineOperation } from "../../operation.ts";
2
+ import { mapText } from "../text/lines.ts";
3
+ import { formatJson, INDENT_STRINGS, JSON_INDENTS } from "./json.ts";
4
+
5
+ export const jsonFormat = defineOperation({
6
+ id: "json-format",
7
+ name: "JSON format",
8
+ description: "Pretty-prints or minifies JSON. Numbers are kept digit for digit, and keys in their original order.",
9
+ category: "Data format",
10
+ output: "binary",
11
+ aliases: ["json beautify", "json prettify", "json minify", "jq"],
12
+ infoUrl: "https://www.json.org",
13
+ args: {
14
+ indent: {
15
+ type: "select",
16
+ name: "Indentation",
17
+ options: JSON_INDENTS,
18
+ default: "2",
19
+ },
20
+ },
21
+ examples: [
22
+ {
23
+ input: '{"a":1,"b":[true,null],"c":{}}',
24
+ output: '{\n "a": 1,\n "b": [\n true,\n null\n ],\n "c": {}\n}',
25
+ },
26
+ {
27
+ input: '{ "id": 12345678901234567890,\n "tags": [ "x" ] }',
28
+ args: { indent: "minify" },
29
+ output: '{"id":12345678901234567890,"tags":["x"]}',
30
+ },
31
+ ],
32
+ run: (input, { indent }) => mapText(input, (text) => formatJson(text, INDENT_STRINGS[indent])),
33
+ });
@@ -0,0 +1,53 @@
1
+ import { OperationError } from "../../errors.ts";
2
+
3
+ export function parseJson(text: string): unknown {
4
+ try {
5
+ return JSON.parse(text);
6
+ } catch (error) {
7
+ throw new OperationError(`Invalid JSON: ${(error as Error).message}`);
8
+ }
9
+ }
10
+
11
+ // Strings, or runs of anything else that is not whitespace or punctuation (numbers, literals).
12
+ const TOKEN = /"(?:[^"\\]|\\.)*"|[{}[\],:]|[^\s{}[\],:"]+/g;
13
+
14
+ /**
15
+ * Re-indents valid JSON without parsing values, so numbers keep their exact digits (no precision
16
+ * loss on large integers) and key order and duplicate keys are preserved. An empty `indent` minifies.
17
+ */
18
+ export function formatJson(text: string, indent: string): string {
19
+ parseJson(text);
20
+ const tokens = text.match(TOKEN) ?? [];
21
+ const newline = (depth: number) => (indent ? "\n" + indent.repeat(depth) : "");
22
+ let out = "";
23
+ let depth = 0;
24
+ for (const [i, token] of tokens.entries()) {
25
+ const next = tokens[i + 1];
26
+ if (token === "{" || token === "[") {
27
+ // Keep empty objects and arrays on one line.
28
+ if (next === "}" || next === "]") out += token;
29
+ else out += token + newline(++depth);
30
+ } else if (token === "}" || token === "]") {
31
+ const previous = tokens[i - 1];
32
+ if (previous === "{" || previous === "[") out += token;
33
+ else out += newline(--depth) + token;
34
+ } else if (token === ",") out += "," + newline(depth);
35
+ else if (token === ":") out += indent ? ": " : ":";
36
+ else out += token;
37
+ }
38
+ return out;
39
+ }
40
+
41
+ export const JSON_INDENTS = [
42
+ { value: "2", label: "2 spaces" },
43
+ { value: "4", label: "4 spaces" },
44
+ { value: "tab", label: "Tab" },
45
+ { value: "minify", label: "None, on a single line" },
46
+ ] as const;
47
+
48
+ export const INDENT_STRINGS: Record<(typeof JSON_INDENTS)[number]["value"], string> = {
49
+ "2": " ",
50
+ "4": " ",
51
+ tab: "\t",
52
+ minify: "",
53
+ };
@@ -0,0 +1,56 @@
1
+ import YAML from "yaml";
2
+ import { OperationError } from "../../errors.ts";
3
+ import { defineOperation } from "../../operation.ts";
4
+ import { mapText } from "../text/lines.ts";
5
+ import { INDENT_STRINGS, JSON_INDENTS, parseJson } from "./json.ts";
6
+
7
+ export const jsonToYaml = defineOperation({
8
+ id: "json-to-yaml",
9
+ name: "JSON to YAML",
10
+ description: 'Converts JSON to YAML. Strings that some YAML parsers would read as another type, such as "yes", are quoted.',
11
+ category: "Data format",
12
+ output: "binary",
13
+ aliases: ["yaml"],
14
+ inverse: "yaml-to-json",
15
+ infoUrl: "https://yaml.org",
16
+ args: {},
17
+ examples: [
18
+ { input: '{"name":"ops","tags":["a","b"],"count":1}', output: "name: ops\ntags:\n - a\n - b\ncount: 1\n" },
19
+ { input: '{"enabled":"yes"}', output: 'enabled: "yes"\n' },
20
+ ],
21
+ run: (input) =>
22
+ mapText(input, (text) => {
23
+ parseJson(text);
24
+ // JSON is valid YAML: parsing it as such keeps large integers exact.
25
+ // YAML 1.1 rules quote strings like "yes" or "off" that older parsers would read as booleans.
26
+ return YAML.stringify(YAML.parse(text, { intAsBigInt: true }), { version: "1.1" });
27
+ }),
28
+ });
29
+
30
+ export const yamlToJson = defineOperation({
31
+ id: "yaml-to-json",
32
+ name: "YAML to JSON",
33
+ description: "Converts a YAML document to JSON.",
34
+ category: "Data format",
35
+ output: "binary",
36
+ aliases: ["yml", "yaml"],
37
+ inverse: "json-to-yaml",
38
+ infoUrl: "https://yaml.org",
39
+ args: {
40
+ indent: { type: "select", name: "Indentation", options: JSON_INDENTS, default: "2" },
41
+ },
42
+ examples: [
43
+ { input: "name: ops\ntags: [a, b]\nn: 1\n", args: { indent: "minify" }, output: '{"name":"ops","tags":["a","b"],"n":1}' },
44
+ { input: "a:\n - true\n - ~\n", output: '{\n "a": [\n true,\n null\n ]\n}' },
45
+ ],
46
+ run: (input, { indent }) =>
47
+ mapText(input, (text) => {
48
+ let value: unknown;
49
+ try {
50
+ value = YAML.parse(text);
51
+ } catch (error) {
52
+ throw new OperationError(`Invalid YAML: ${(error as Error).message}`);
53
+ }
54
+ return JSON.stringify(value ?? null, null, INDENT_STRINGS[indent]);
55
+ }),
56
+ });
@@ -0,0 +1,135 @@
1
+ import { defineOperation } from "../../operation.ts";
2
+ import { mapText } from "../text/lines.ts";
3
+
4
+ const OCTET = "(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)";
5
+ // Not part of a longer dotted sequence such as a version number (1.2.3.4.5).
6
+ const IPV4 = new RegExp(`(?<![\\d.])${OCTET}(?:\\.${OCTET}){3}(?!\\.?\\d)`, "g");
7
+ // Candidates, checked by isIPv6: runs of hex digits, colons and dots with at least two colons.
8
+ const IPV6_CANDIDATE = /(?<![\w:.])[0-9a-f]*:[0-9a-f.:]*:[0-9a-f.]*(?![\w:])/gi;
9
+ const IPV4_EXACT = new RegExp(`^${OCTET}(?:\\.${OCTET}){3}$`);
10
+
11
+ export function isIPv6(text: string): boolean {
12
+ const halves = text.split("::");
13
+ if (halves.length > 2) return false;
14
+ let groups = 0;
15
+ for (const [h, half] of halves.entries()) {
16
+ if (half === "") continue;
17
+ const parts = half.split(":");
18
+ for (const [i, part] of parts.entries()) {
19
+ // An embedded IPv4 address (::ffff:1.2.3.4) counts as two groups and must come last.
20
+ if (h === halves.length - 1 && i === parts.length - 1 && IPV4_EXACT.test(part)) groups += 2;
21
+ else if (/^[0-9a-f]{1,4}$/i.test(part)) groups++;
22
+ else return false;
23
+ }
24
+ }
25
+ return halves.length === 2 ? groups < 8 : groups === 8;
26
+ }
27
+
28
+ interface Match {
29
+ index: number;
30
+ value: string;
31
+ }
32
+
33
+ function* matches(text: string, pattern: RegExp): Generator<Match> {
34
+ for (const m of text.matchAll(pattern)) yield { index: m.index, value: m[0] };
35
+ }
36
+
37
+ const listArgs = {
38
+ unique: { type: "boolean", name: "Unique", description: "Only list each value once.", default: false },
39
+ sort: { type: "boolean", name: "Sort", default: false },
40
+ } as const;
41
+
42
+ function list(found: Iterable<Match>, { unique, sort }: { unique: boolean; sort: boolean }): string {
43
+ let values = [...found].sort((a, b) => a.index - b.index).map((m) => m.value);
44
+ if (unique) values = [...new Set(values)];
45
+ if (sort) values.sort();
46
+ return values.join("\n");
47
+ }
48
+
49
+ const IP_VERSIONS = [
50
+ { value: "both", label: "IPv4 and IPv6" },
51
+ { value: "ipv4", label: "IPv4 only" },
52
+ { value: "ipv6", label: "IPv6 only" },
53
+ ] as const;
54
+
55
+ export const extractIps = defineOperation({
56
+ id: "extract-ips",
57
+ name: "Extract IP addresses",
58
+ description: "Lists the IPv4 and IPv6 addresses found in the text, one per line.",
59
+ category: "Extractors",
60
+ output: "binary",
61
+ aliases: ["ipv4", "ipv6", "find ip", "grep ip"],
62
+ args: {
63
+ version: { type: "select", name: "Version", options: IP_VERSIONS, default: "both" },
64
+ ...listArgs,
65
+ },
66
+ examples: [
67
+ { input: "from 10.0.0.1 to 2001:db8::1, not 1.2.3.4.5 or 256.1.1.1", output: "10.0.0.1\n2001:db8::1" },
68
+ { input: "::1 ::ffff:192.168.1.1 8.8.8.8", args: { version: "ipv4" }, output: "8.8.8.8" },
69
+ ],
70
+ run(input, { version, unique, sort }) {
71
+ return mapText(input, (text) => {
72
+ const v6 = [...matches(text, IPV6_CANDIDATE)]
73
+ // Drop the final dot of a sentence.
74
+ .map((m) => ({ index: m.index, value: m.value.replace(/\.$/, "") }))
75
+ .filter((m) => isIPv6(m.value));
76
+ // IPv4 addresses embedded in an IPv6 one are not listed on their own.
77
+ const v4 = [...matches(text, IPV4)].filter((m) => !v6.some((v) => m.index >= v.index && m.index < v.index + v.value.length));
78
+ const found = version === "ipv4" ? v4 : version === "ipv6" ? v6 : [...v4, ...v6];
79
+ return list(found, { unique, sort });
80
+ });
81
+ },
82
+ });
83
+
84
+ const EMAIL =
85
+ /[\p{L}\p{N}!#$%&'*+/=?^_`{|}~-]+(?:\.[\p{L}\p{N}!#$%&'*+/=?^_`{|}~-]+)*@(?:[\p{L}\p{N}](?:[\p{L}\p{N}-]*[\p{L}\p{N}])?\.)+\p{L}[\p{L}\p{N}-]*[\p{L}\p{N}]/gu;
86
+
87
+ export const extractEmails = defineOperation({
88
+ id: "extract-emails",
89
+ name: "Extract email addresses",
90
+ description: "Lists the email addresses found in the text, one per line.",
91
+ category: "Extractors",
92
+ output: "binary",
93
+ aliases: ["find email", "grep email"],
94
+ args: listArgs,
95
+ examples: [
96
+ { input: "Contact: <john.doe@example.com>, jane+tag@mail.example.org.", output: "john.doe@example.com\njane+tag@mail.example.org" },
97
+ { input: "b@x.io a@x.io b@x.io", args: { unique: true, sort: true }, output: "a@x.io\nb@x.io" },
98
+ ],
99
+ run: (input, args) => mapText(input, (text) => list(matches(text, EMAIL), args)),
100
+ });
101
+
102
+ // A scheme followed by "://", up to whitespace, quotes or angle brackets.
103
+ const URL_PATTERN = /\b[a-z][a-z0-9+.-]*:\/\/[^\s<>"'`]+/gi;
104
+
105
+ /** Removes punctuation that more likely ends the sentence than the URL, and unbalanced closing brackets. */
106
+ function trimUrl(url: string): string {
107
+ for (;;) {
108
+ const last = url.at(-1)!;
109
+ if (/[.,;:!?*]/.test(last)) url = url.slice(0, -1);
110
+ else if (last === ")" && url.split("(").length < url.split(")").length) url = url.slice(0, -1);
111
+ else if (last === "]" && url.split("[").length < url.split("]").length) url = url.slice(0, -1);
112
+ else return url;
113
+ }
114
+ }
115
+
116
+ export const extractUrls = defineOperation({
117
+ id: "extract-urls",
118
+ name: "Extract URLs",
119
+ description: "Lists the URLs (anything like scheme://...) found in the text, one per line.",
120
+ category: "Extractors",
121
+ output: "binary",
122
+ aliases: ["find url", "grep url", "links"],
123
+ args: listArgs,
124
+ examples: [
125
+ {
126
+ input: "See https://example.com/a?b=1, (or http://wiki.org/A_(b)) and ftp://x.org/file.",
127
+ output: "https://example.com/a?b=1\nhttp://wiki.org/A_(b)\nftp://x.org/file",
128
+ },
129
+ ],
130
+ run: (input, args) =>
131
+ mapText(input, (text) => {
132
+ const urls = [...matches(text, URL_PATTERN)].map((m) => ({ index: m.index, value: trimUrl(m.value) }));
133
+ return list(urls, args);
134
+ }),
135
+ });
@@ -0,0 +1,44 @@
1
+ import bcryptjs from "bcryptjs";
2
+ import { OperationError } from "../../errors.ts";
3
+ import { defineOperation } from "../../operation.ts";
4
+
5
+ const SALT = /^[./A-Za-z0-9]{22}$/;
6
+
7
+ export const bcrypt = defineOperation({
8
+ id: "bcrypt",
9
+ name: "Bcrypt",
10
+ description: "Hashes the input (a password) with bcrypt, using a random salt unless one is given. Only the first 72 bytes are used.",
11
+ category: "Hashing",
12
+ output: "text",
13
+ aliases: ["password hash", "blowfish"],
14
+ infoUrl: "https://wikipedia.org/wiki/Bcrypt",
15
+ args: {
16
+ rounds: {
17
+ type: "number",
18
+ name: "Cost",
19
+ description: "Log2 of the number of rounds: each step doubles the computing time.",
20
+ min: 4,
21
+ max: 31,
22
+ default: 10,
23
+ },
24
+ salt: {
25
+ type: "string",
26
+ name: "Salt",
27
+ description: "22 characters from ./A-Za-z0-9. Leave empty for a random salt.",
28
+ default: "",
29
+ },
30
+ },
31
+ examples: [
32
+ {
33
+ input: "hello",
34
+ args: { rounds: 4, salt: "abcdefghijklmnopqrstuu" },
35
+ output: "$2b$04$abcdefghijklmnopqrstuuwHJMEGjfAzmL1lWmUmlphguIbWfjYey",
36
+ },
37
+ ],
38
+ run(input, { rounds, salt }) {
39
+ if (!Number.isInteger(rounds)) throw new OperationError(`Invalid cost: ${rounds} is not an integer`);
40
+ if (salt && !SALT.test(salt)) throw new OperationError("Invalid salt: expected 22 characters from ./A-Za-z0-9");
41
+ const fullSalt = salt ? `$2b$${String(rounds).padStart(2, "0")}$${salt}` : bcryptjs.genSaltSync(rounds);
42
+ return new TextEncoder().encode(bcryptjs.hashSync(new TextDecoder().decode(input), fullSalt));
43
+ },
44
+ });
@@ -0,0 +1,64 @@
1
+ import { createHash } from "node:crypto";
2
+ import { defineOperation } from "../../operation.ts";
3
+
4
+ /** An OpenSSL digest name, as accepted by node:crypto createHash(). */
5
+ type Algorithm = "md5" | "sha1" | "sha224" | "sha256" | "sha384" | "sha512" | "sha3-224" | "sha3-256" | "sha3-384" | "sha3-512";
6
+
7
+ function defineHash<const Id extends string>(id: Id, name: string, algorithm: Algorithm, infoUrl: string, hello: string) {
8
+ return defineOperation({
9
+ id,
10
+ name,
11
+ description: `Computes the ${name} digest of the input, as lowercase hex.`,
12
+ category: "Hashing",
13
+ output: "text",
14
+ aliases: [`${id}sum`, name],
15
+ infoUrl,
16
+ args: {},
17
+ examples: [{ input: "hello", output: hello }],
18
+ run: (input) => new TextEncoder().encode(createHash(algorithm).update(input).digest("hex")),
19
+ });
20
+ }
21
+
22
+ const SHA2 = "https://wikipedia.org/wiki/SHA-2";
23
+ const SHA3 = "https://wikipedia.org/wiki/SHA-3";
24
+
25
+ export const md5 = defineHash("md5", "MD5", "md5", "https://wikipedia.org/wiki/MD5", "5d41402abc4b2a76b9719d911017c592");
26
+ export const sha1 = defineHash("sha1", "SHA-1", "sha1", "https://wikipedia.org/wiki/SHA-1", "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d");
27
+ export const sha224 = defineHash("sha224", "SHA-224", "sha224", SHA2, "ea09ae9cc6768c50fcee903ed054556e5bfc8347907f12598aa24193");
28
+ export const sha256 = defineHash("sha256", "SHA-256", "sha256", SHA2, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824");
29
+ export const sha384 = defineHash(
30
+ "sha384",
31
+ "SHA-384",
32
+ "sha384",
33
+ SHA2,
34
+ "59e1748777448c69de6b800d7a33bbfb9ff1b463e44354c3553bcdb9c666fa90125a3c79f90397bdf5f6a13de828684f",
35
+ );
36
+ export const sha512 = defineHash(
37
+ "sha512",
38
+ "SHA-512",
39
+ "sha512",
40
+ SHA2,
41
+ "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043",
42
+ );
43
+ export const sha3_224 = defineHash("sha3-224", "SHA3-224", "sha3-224", SHA3, "b87f88c72702fff1748e58b87e9141a42c0dbedc29a78cb0d4a5cd81");
44
+ export const sha3_256 = defineHash(
45
+ "sha3-256",
46
+ "SHA3-256",
47
+ "sha3-256",
48
+ SHA3,
49
+ "3338be694f50c5f338814986cdf0686453a888b84f424d792af4b9202398f392",
50
+ );
51
+ export const sha3_384 = defineHash(
52
+ "sha3-384",
53
+ "SHA3-384",
54
+ "sha3-384",
55
+ SHA3,
56
+ "720aea11019ef06440fbf05d87aa24680a2153df3907b23631e7177ce620fa1330ff07c0fddee54699a4c3ee0ee9d887",
57
+ );
58
+ export const sha3_512 = defineHash(
59
+ "sha3-512",
60
+ "SHA3-512",
61
+ "sha3-512",
62
+ SHA3,
63
+ "75d527c368f2efe848ecf6b073a36767800805e9eef2b1857d5f984f036eb6df891d75f72d9b154518c1cd58835286d1da9a38deba3de98b5a53e5ed78a84976",
64
+ );
@@ -0,0 +1,31 @@
1
+ import { OperationError } from "../../errors.ts";
2
+ import { defineOperation } from "../../operation.ts";
3
+
4
+ // "0x" and "\x" prefixes, whitespace and common separators.
5
+ const IGNORED = /0x|\\x|[\s:,;.%-]/gi;
6
+
7
+ export const fromHex = defineOperation({
8
+ id: "from-hex",
9
+ name: "From Hex",
10
+ description:
11
+ 'Decodes hexadecimal text into raw bytes, ignoring case, whitespace, separators (: , ; . % -) and 0x or \\x prefixes. e.g. "68656c6c6f" becomes "hello".',
12
+ category: "Encoding",
13
+ output: "binary",
14
+ aliases: ["hex decode", "base16", "unhex", "xxd -r"],
15
+ inverse: "to-hex",
16
+ infoUrl: "https://wikipedia.org/wiki/Hexadecimal",
17
+ args: {},
18
+ examples: [
19
+ { input: "68656c6c6f", output: "hello" },
20
+ { input: "68:65:6C:6C:6F", output: "hello" },
21
+ { input: "0x68, 0x69", output: "hi" },
22
+ { input: "\\x68\\x69", output: "hi" },
23
+ ],
24
+ run(input) {
25
+ const hex = new TextDecoder().decode(input).replace(IGNORED, "");
26
+ const bad = /[^0-9a-f]/i.exec(hex);
27
+ if (bad) throw new OperationError(`Invalid hex: unexpected character "${bad[0]}"`);
28
+ if (hex.length % 2) throw new OperationError("Invalid hex: odd number of digits");
29
+ return Uint8Array.fromHex(hex);
30
+ },
31
+ });
@@ -0,0 +1,46 @@
1
+ import { defineOperation } from "../../operation.ts";
2
+
3
+ const DELIMITERS = [
4
+ { value: "none", label: "None: 68656c6c6f" },
5
+ { value: "space", label: "Space: 68 65 6c 6c 6f" },
6
+ { value: "colon", label: "Colon: 68:65:6c:6c:6f" },
7
+ { value: "comma", label: "Comma: 68,65,6c,6c,6f" },
8
+ { value: "escape", label: "Escape sequences: \\x68\\x65\\x6c\\x6c\\x6f" },
9
+ { value: "0x", label: "C array: 0x68, 0x65, 0x6c, 0x6c, 0x6f" },
10
+ ] as const;
11
+
12
+ const FORMATS: Record<(typeof DELIMITERS)[number]["value"], { prefix: string; separator: string }> = {
13
+ none: { prefix: "", separator: "" },
14
+ space: { prefix: "", separator: " " },
15
+ colon: { prefix: "", separator: ":" },
16
+ comma: { prefix: "", separator: "," },
17
+ escape: { prefix: "\\x", separator: "" },
18
+ "0x": { prefix: "0x", separator: ", " },
19
+ };
20
+
21
+ export const toHex = defineOperation({
22
+ id: "to-hex",
23
+ name: "To Hex",
24
+ description: 'Encodes raw bytes as hexadecimal text. e.g. "hello" becomes "68656c6c6f".',
25
+ category: "Encoding",
26
+ output: "text",
27
+ aliases: ["hex encode", "base16", "xxd"],
28
+ inverse: "from-hex",
29
+ infoUrl: "https://wikipedia.org/wiki/Hexadecimal",
30
+ args: {
31
+ delimiter: { type: "select", name: "Delimiter", options: DELIMITERS, default: "none" },
32
+ uppercase: { type: "boolean", name: "Uppercase", default: false },
33
+ },
34
+ examples: [
35
+ { input: "hello", output: "68656c6c6f" },
36
+ { input: "hello", args: { delimiter: "colon", uppercase: true }, output: "68:65:6C:6C:6F" },
37
+ { input: "hi", args: { delimiter: "escape" }, output: "\\x68\\x69" },
38
+ ],
39
+ run(input, { delimiter, uppercase }) {
40
+ const { prefix, separator } = FORMATS[delimiter];
41
+ const hex = input.toHex();
42
+ const digits = uppercase ? hex.toUpperCase() : hex;
43
+ const bytes = digits.match(/../g) ?? [];
44
+ return new TextEncoder().encode(bytes.map((byte) => prefix + byte).join(separator));
45
+ },
46
+ });
@@ -0,0 +1,71 @@
1
+ // HTML 4 entities for U+00A0 to U+00FF, in code point order.
2
+ const LATIN1 =
3
+ "nbsp iexcl cent pound curren yen brvbar sect uml copy ordf laquo not shy reg macr deg plusmn sup2 sup3 acute " +
4
+ "micro para middot cedil sup1 ordm raquo frac14 frac12 frac34 iquest Agrave Aacute Acirc Atilde Auml Aring AElig " +
5
+ "Ccedil Egrave Eacute Ecirc Euml Igrave Iacute Icirc Iuml ETH Ntilde Ograve Oacute Ocirc Otilde Ouml times Oslash " +
6
+ "Ugrave Uacute Ucirc Uuml Yacute THORN szlig agrave aacute acirc atilde auml aring aelig ccedil egrave eacute " +
7
+ "ecirc euml igrave iacute icirc iuml eth ntilde ograve oacute ocirc otilde ouml divide oslash ugrave uacute ucirc " +
8
+ "uuml yacute thorn yuml";
9
+
10
+ const OTHERS: Record<string, number> = {
11
+ quot: 0x22,
12
+ amp: 0x26,
13
+ apos: 0x27,
14
+ lt: 0x3c,
15
+ gt: 0x3e,
16
+ OElig: 0x152,
17
+ oelig: 0x153,
18
+ Scaron: 0x160,
19
+ scaron: 0x161,
20
+ Yuml: 0x178,
21
+ fnof: 0x192,
22
+ circ: 0x2c6,
23
+ tilde: 0x2dc,
24
+ ensp: 0x2002,
25
+ emsp: 0x2003,
26
+ thinsp: 0x2009,
27
+ zwnj: 0x200c,
28
+ zwj: 0x200d,
29
+ lrm: 0x200e,
30
+ rlm: 0x200f,
31
+ ndash: 0x2013,
32
+ mdash: 0x2014,
33
+ lsquo: 0x2018,
34
+ rsquo: 0x2019,
35
+ sbquo: 0x201a,
36
+ ldquo: 0x201c,
37
+ rdquo: 0x201d,
38
+ bdquo: 0x201e,
39
+ dagger: 0x2020,
40
+ Dagger: 0x2021,
41
+ bull: 0x2022,
42
+ hellip: 0x2026,
43
+ permil: 0x2030,
44
+ prime: 0x2032,
45
+ Prime: 0x2033,
46
+ lsaquo: 0x2039,
47
+ rsaquo: 0x203a,
48
+ oline: 0x203e,
49
+ euro: 0x20ac,
50
+ trade: 0x2122,
51
+ larr: 0x2190,
52
+ uarr: 0x2191,
53
+ rarr: 0x2192,
54
+ darr: 0x2193,
55
+ harr: 0x2194,
56
+ minus: 0x2212,
57
+ infin: 0x221e,
58
+ ne: 0x2260,
59
+ le: 0x2264,
60
+ ge: 0x2265,
61
+ spades: 0x2660,
62
+ clubs: 0x2663,
63
+ hearts: 0x2665,
64
+ diams: 0x2666,
65
+ };
66
+
67
+ /** Named character references supported by `html-decode`: HTML 4 entities and `&apos;`. */
68
+ export const NAMED_ENTITIES = new Map<string, number>([
69
+ ...LATIN1.split(" ").map((name, i) => [name, 0xa0 + i] as const),
70
+ ...Object.entries(OTHERS),
71
+ ]);
@@ -0,0 +1,36 @@
1
+ import { defineOperation } from "../../operation.ts";
2
+ import { NAMED_ENTITIES } from "./entities.ts";
3
+
4
+ const ENTITY = /&(?:#(\d+)|#x([0-9a-f]+)|([a-z][a-z0-9]*));/gi;
5
+
6
+ function fromCodePoint(code: number): string {
7
+ // NUL, surrogates and out of range values are replaced, as browsers do.
8
+ if (code === 0 || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) return "�";
9
+ return String.fromCodePoint(code);
10
+ }
11
+
12
+ export const htmlDecode = defineOperation({
13
+ id: "html-decode",
14
+ name: "HTML Decode",
15
+ description:
16
+ 'Decodes HTML entities: numeric (&#233; &#xE9;) and named HTML 4 ones (&eacute; &amp; ...). Unknown entities are left as is. e.g. "&lt;b&gt;" becomes "<b>".',
17
+ category: "Encoding",
18
+ output: "binary",
19
+ aliases: ["html unescape", "html entities"],
20
+ inverse: "html-encode",
21
+ infoUrl: "https://wikipedia.org/wiki/Character_encodings_in_HTML",
22
+ args: {},
23
+ examples: [
24
+ { input: "&lt;a href=&quot;x&quot;&gt;Tom &amp; Jerry&lt;/a&gt;", output: '<a href="x">Tom & Jerry</a>' },
25
+ { input: "caf&eacute; &#8364; &#x263A; &unknown;", output: "café € ☺ &unknown;" },
26
+ ],
27
+ run(input) {
28
+ const text = new TextDecoder().decode(input).replace(ENTITY, (entity, dec?: string, hex?: string, name?: string) => {
29
+ if (dec) return fromCodePoint(Number(dec));
30
+ if (hex) return fromCodePoint(parseInt(hex, 16));
31
+ const code = NAMED_ENTITIES.get(name!);
32
+ return code === undefined ? entity : String.fromCodePoint(code);
33
+ });
34
+ return new TextEncoder().encode(text);
35
+ },
36
+ });
@@ -0,0 +1,31 @@
1
+ import { defineOperation } from "../../operation.ts";
2
+
3
+ const SPECIAL: Record<string, string> = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
4
+
5
+ export const htmlEncode = defineOperation({
6
+ id: "html-encode",
7
+ name: "HTML Encode",
8
+ description: 'Escapes & < > " and \' as HTML entities. e.g. "<b>" becomes "&lt;b&gt;".',
9
+ category: "Encoding",
10
+ output: "binary",
11
+ aliases: ["html escape", "html entities"],
12
+ inverse: "html-decode",
13
+ infoUrl: "https://wikipedia.org/wiki/Character_encodings_in_HTML",
14
+ args: {
15
+ encodeNonAscii: {
16
+ type: "boolean",
17
+ name: "Encode non-ASCII characters",
18
+ description: "Also write characters outside ASCII as numeric entities, e.g. é as &#233;.",
19
+ default: false,
20
+ },
21
+ },
22
+ examples: [
23
+ { input: '<a href="x">Tom & Jerry</a>', output: "&lt;a href=&quot;x&quot;&gt;Tom &amp; Jerry&lt;/a&gt;" },
24
+ { input: "café €", args: { encodeNonAscii: true }, output: "caf&#233; &#8364;" },
25
+ ],
26
+ run(input, { encodeNonAscii }) {
27
+ const pattern = encodeNonAscii ? /[&<>"']|[^\x00-\x7f]/gu : /[&<>"']/g;
28
+ const text = new TextDecoder().decode(input).replace(pattern, (ch) => SPECIAL[ch] ?? `&#${ch.codePointAt(0)};`);
29
+ return new TextEncoder().encode(text);
30
+ },
31
+ });