@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,140 @@
1
+ import { defineOperation } from "../../operation.ts";
2
+ import { mapLines, mapText } from "./lines.ts";
3
+
4
+ // Words of an identifier or a sentence: "XMLHttpRequest v2" -> XML, Http, Request, v, 2.
5
+ const WORD = /\p{Lu}+(?=\p{Lu}\p{Ll})|\p{Lu}?[\p{Ll}\p{Lo}\p{M}]+|\p{Lu}+\p{M}*|\p{Lt}[\p{Ll}\p{M}]*|\p{N}+/gu;
6
+
7
+ function words(line: string): string[] {
8
+ return line.replace(/['’]/g, "").match(WORD) ?? [];
9
+ }
10
+
11
+ const capitalize = (word: string) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
12
+
13
+ /** Defines an operation that rebuilds each line from its words. */
14
+ function defineCaseOperation<const Id extends string>(
15
+ id: Id,
16
+ name: string,
17
+ description: string,
18
+ aliases: string[],
19
+ join: (words: string[]) => string,
20
+ example: { input: string; output: string },
21
+ ) {
22
+ return defineOperation({
23
+ id,
24
+ name,
25
+ description: `${description} Each line is converted separately.`,
26
+ category: "Text",
27
+ output: "binary",
28
+ aliases,
29
+ args: {},
30
+ examples: [example],
31
+ run: (input) => mapText(input, (text) => mapLines(text, (lines) => lines.map((line) => join(words(line))))),
32
+ });
33
+ }
34
+
35
+ export const toCamelCase = defineCaseOperation(
36
+ "to-camel-case",
37
+ "To camelCase",
38
+ 'Joins the words with the first one in lowercase and the others capitalized. e.g. "hello world" becomes "helloWorld".',
39
+ ["camelcase", "lower camel case"],
40
+ (words) => words.map((word, i) => (i ? capitalize(word) : word.toLowerCase())).join(""),
41
+ { input: "Hello big_world\nXMLHttpRequest", output: "helloBigWorld\nxmlHttpRequest" },
42
+ );
43
+
44
+ export const toPascalCase = defineCaseOperation(
45
+ "to-pascal-case",
46
+ "To PascalCase",
47
+ 'Joins the capitalized words. e.g. "hello world" becomes "HelloWorld".',
48
+ ["pascalcase", "upper camel case"],
49
+ (words) => words.map(capitalize).join(""),
50
+ { input: "hello big_world", output: "HelloBigWorld" },
51
+ );
52
+
53
+ export const toSnakeCase = defineCaseOperation(
54
+ "to-snake-case",
55
+ "To snake_case",
56
+ 'Joins the lowercase words with underscores. e.g. "helloWorld" becomes "hello_world".',
57
+ ["snakecase", "underscore"],
58
+ (words) => words.map((word) => word.toLowerCase()).join("_"),
59
+ { input: "Hello bigWorld", output: "hello_big_world" },
60
+ );
61
+
62
+ export const toKebabCase = defineCaseOperation(
63
+ "to-kebab-case",
64
+ "To kebab-case",
65
+ 'Joins the lowercase words with hyphens. e.g. "helloWorld" becomes "hello-world".',
66
+ ["kebabcase", "dash case", "hyphen"],
67
+ (words) => words.map((word) => word.toLowerCase()).join("-"),
68
+ { input: "Hello bigWorld", output: "hello-big-world" },
69
+ );
70
+
71
+ // Letters that Unicode decomposition does not reduce to ASCII.
72
+ const TRANSLITERATIONS: Record<string, string> = { ß: "ss", æ: "ae", œ: "oe", ø: "o", đ: "d", ł: "l", þ: "th", ð: "d" };
73
+
74
+ export const toSlug = defineOperation({
75
+ id: "to-slug",
76
+ name: "To slug",
77
+ description:
78
+ 'Makes a URL-friendly slug: removes accents, lowercases, and replaces everything but letters and digits with a separator. e.g. "Héllo, World!" becomes "hello-world". Each line is converted separately.',
79
+ category: "Text",
80
+ output: "binary",
81
+ aliases: ["slugify", "permalink"],
82
+ args: {
83
+ separator: { type: "string", name: "Separator", default: "-" },
84
+ },
85
+ examples: [
86
+ { input: "Héllo, World!", output: "hello-world" },
87
+ { input: "Straße & Œuvre", args: { separator: "_" }, output: "strasse_oeuvre" },
88
+ ],
89
+ run: (input, { separator }) =>
90
+ mapText(input, (text) =>
91
+ mapLines(text, (lines) =>
92
+ lines.map((line) =>
93
+ line
94
+ .toLowerCase()
95
+ .replace(/[ßæœøđłþð]/g, (ch) => TRANSLITERATIONS[ch]!)
96
+ .normalize("NFKD")
97
+ .replace(/\p{M}/gu, "")
98
+ .replace(/[^a-z0-9]+/g, " ")
99
+ .trim()
100
+ .replaceAll(" ", separator),
101
+ ),
102
+ ),
103
+ ),
104
+ });
105
+
106
+ export const toTitleCase = defineOperation({
107
+ id: "to-title-case",
108
+ name: "To Title Case",
109
+ description: 'Capitalizes the first letter of every word and lowercases the others. e.g. "hello WORLD" becomes "Hello World".',
110
+ category: "Text",
111
+ output: "binary",
112
+ aliases: ["capitalize", "titlecase"],
113
+ args: {},
114
+ examples: [{ input: "hello WORLD, don't panic", output: "Hello World, Don't Panic" }],
115
+ run: (input) => mapText(input, (text) => text.replace(/[\p{L}\p{N}][\p{L}\p{N}\p{M}'’]*/gu, capitalize)),
116
+ });
117
+
118
+ export const toUpperCase = defineOperation({
119
+ id: "to-upper-case",
120
+ name: "To UPPER CASE",
121
+ description: 'Converts the text to uppercase. e.g. "héllo" becomes "HÉLLO".',
122
+ category: "Text",
123
+ output: "binary",
124
+ aliases: ["uppercase", "capitals"],
125
+ args: {},
126
+ examples: [{ input: "héllo world", output: "HÉLLO WORLD" }],
127
+ run: (input) => mapText(input, (text) => text.toUpperCase()),
128
+ });
129
+
130
+ export const toLowerCase = defineOperation({
131
+ id: "to-lower-case",
132
+ name: "To lower case",
133
+ description: 'Converts the text to lowercase. e.g. "HÉLLO" becomes "héllo".',
134
+ category: "Text",
135
+ output: "binary",
136
+ aliases: ["lowercase"],
137
+ args: {},
138
+ examples: [{ input: "HÉLLO World", output: "héllo world" }],
139
+ run: (input) => mapText(input, (text) => text.toLowerCase()),
140
+ });
@@ -0,0 +1,61 @@
1
+ import { defineOperation } from "../../operation.ts";
2
+ import { splitLines } from "./lines.ts";
3
+
4
+ const encodeNumber = (n: number) => new TextEncoder().encode(String(n));
5
+
6
+ const UNITS = [
7
+ { value: "graphemes", label: "Characters as displayed (é and 👍🏽 count as 1)" },
8
+ { value: "codepoints", label: "Unicode code points" },
9
+ { value: "bytes", label: "Bytes" },
10
+ ] as const;
11
+
12
+ export const countChars = defineOperation({
13
+ id: "count-chars",
14
+ name: "Count characters",
15
+ description: "Counts the characters of the input, line breaks included, like `wc -m`.",
16
+ category: "Text",
17
+ output: "text",
18
+ aliases: ["length", "wc -m", "char count"],
19
+ args: {
20
+ unit: { type: "select", name: "Unit", options: UNITS, default: "graphemes" },
21
+ },
22
+ examples: [
23
+ { input: "héllo 👍🏽", output: "7" },
24
+ { input: "héllo 👍🏽", args: { unit: "codepoints" }, output: "8" },
25
+ { input: "héllo 👍🏽", args: { unit: "bytes" }, output: "15" },
26
+ ],
27
+ run(input, { unit }) {
28
+ if (unit === "bytes") return encodeNumber(input.length);
29
+ const text = new TextDecoder().decode(input);
30
+ if (unit === "codepoints") return encodeNumber([...text].length);
31
+ return encodeNumber([...new Intl.Segmenter().segment(text)].length);
32
+ },
33
+ });
34
+
35
+ export const countLines = defineOperation({
36
+ id: "count-lines",
37
+ name: "Count lines",
38
+ description: "Counts the lines of the input. Unlike `wc -l`, a last line without a line break is counted.",
39
+ category: "Text",
40
+ output: "text",
41
+ aliases: ["wc -l", "line count"],
42
+ args: {},
43
+ examples: [
44
+ { input: "a\nb\nc", output: "3" },
45
+ { input: "a\nb\n", output: "2" },
46
+ { input: "", output: "0" },
47
+ ],
48
+ run: (input) => encodeNumber(splitLines(new TextDecoder().decode(input)).lines.length),
49
+ });
50
+
51
+ export const countWords = defineOperation({
52
+ id: "count-words",
53
+ name: "Count words",
54
+ description: "Counts the words of the input, separated by whitespace, like `wc -w`.",
55
+ category: "Text",
56
+ output: "text",
57
+ aliases: ["wc -w", "word count"],
58
+ args: {},
59
+ examples: [{ input: " hello big\n world ", output: "3" }],
60
+ run: (input) => encodeNumber(new TextDecoder().decode(input).split(/\s+/).filter(Boolean).length),
61
+ });
@@ -0,0 +1,144 @@
1
+ import { defineOperation } from "../../operation.ts";
2
+ import { mapLines, mapText } from "./lines.ts";
3
+
4
+ export const sortLines = defineOperation({
5
+ id: "sort-lines",
6
+ name: "Sort lines",
7
+ description: "Sorts the lines by Unicode code point, like `LC_ALL=C sort`, or in natural order.",
8
+ category: "Text",
9
+ output: "binary",
10
+ aliases: ["sort", "order"],
11
+ args: {
12
+ reverse: { type: "boolean", name: "Reverse", description: "Sort in descending order.", default: false },
13
+ ignoreCase: { type: "boolean", name: "Ignore case", default: false },
14
+ natural: {
15
+ type: "boolean",
16
+ name: "Natural order",
17
+ description: 'Compare numbers by value ("2" before "10") and letters alphabetically ("a" before "B").',
18
+ default: false,
19
+ },
20
+ },
21
+ examples: [
22
+ { input: "b\nB\na\n10\n2", output: "10\n2\nB\na\nb" },
23
+ { input: "b\nB\na\n10\n2", args: { natural: true, ignoreCase: true }, output: "2\n10\na\nb\nB" },
24
+ { input: "a\nc\nb", args: { reverse: true }, output: "c\nb\na" },
25
+ ],
26
+ run(input, { reverse, ignoreCase, natural }) {
27
+ const collator = new Intl.Collator("en", { numeric: true, sensitivity: ignoreCase ? "accent" : "variant" });
28
+ const key = (line: string) => (ignoreCase ? line.toLowerCase() : line);
29
+ const compare = natural ? collator.compare : (a: string, b: string) => (key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0);
30
+ return mapText(input, (text) => mapLines(text, (lines) => lines.toSorted(reverse ? (a, b) => compare(b, a) : compare)));
31
+ },
32
+ });
33
+
34
+ /** Mulberry32, seeded with the FNV-1a hash of the seed: reproducible, not cryptographically secure. */
35
+ function seededRandom(seed: string): () => number {
36
+ let state = 0x811c9dc5;
37
+ for (const byte of new TextEncoder().encode(seed)) state = Math.imul(state ^ byte, 0x01000193);
38
+ return () => {
39
+ state = (state + 0x6d2b79f5) | 0;
40
+ let t = Math.imul(state ^ (state >>> 15), 1 | state);
41
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
42
+ return ((t ^ (t >>> 14)) >>> 0) / 2 ** 32;
43
+ };
44
+ }
45
+
46
+ const secureRandom = () => crypto.getRandomValues(new Uint32Array(1))[0]! / 2 ** 32;
47
+
48
+ export const shuffleLines = defineOperation({
49
+ id: "shuffle-lines",
50
+ name: "Shuffle lines",
51
+ description: "Puts the lines in a random order.",
52
+ category: "Text",
53
+ output: "binary",
54
+ aliases: ["shuf", "randomize"],
55
+ args: {
56
+ seed: {
57
+ type: "string",
58
+ name: "Seed",
59
+ description: "Gives the same order every time for the same seed and input. Leave empty for a random order.",
60
+ default: "",
61
+ },
62
+ },
63
+ examples: [{ input: "a\nb\nc\nd", args: { seed: "42" }, output: "c\na\nd\nb" }],
64
+ run(input, { seed }) {
65
+ const random = seed ? seededRandom(seed) : secureRandom;
66
+ return mapText(input, (text) =>
67
+ mapLines(text, (lines) => {
68
+ // Fisher-Yates.
69
+ const shuffled = [...lines];
70
+ for (let i = shuffled.length - 1; i > 0; i--) {
71
+ const j = Math.floor(random() * (i + 1));
72
+ [shuffled[i], shuffled[j]] = [shuffled[j]!, shuffled[i]!];
73
+ }
74
+ return shuffled;
75
+ }),
76
+ );
77
+ },
78
+ });
79
+
80
+ export const uniqueLines = defineOperation({
81
+ id: "unique-lines",
82
+ name: "Unique lines",
83
+ description: "Removes duplicate lines, keeping the first occurrence of each, even when duplicates are not adjacent.",
84
+ category: "Text",
85
+ output: "binary",
86
+ aliases: ["uniq", "dedupe", "remove duplicates"],
87
+ args: {
88
+ ignoreCase: { type: "boolean", name: "Ignore case", default: false },
89
+ },
90
+ examples: [
91
+ { input: "a\nb\na\nc\nb", output: "a\nb\nc" },
92
+ { input: "a\nA\nb", args: { ignoreCase: true }, output: "a\nb" },
93
+ ],
94
+ run(input, { ignoreCase }) {
95
+ return mapText(input, (text) =>
96
+ mapLines(text, (lines) => {
97
+ const seen = new Set<string>();
98
+ return lines.filter((line) => {
99
+ const key = ignoreCase ? line.toLowerCase() : line;
100
+ if (seen.has(key)) return false;
101
+ seen.add(key);
102
+ return true;
103
+ });
104
+ }),
105
+ );
106
+ },
107
+ });
108
+
109
+ export const reverseLines = defineOperation({
110
+ id: "reverse-lines",
111
+ name: "Reverse lines",
112
+ description: "Reverses the order of the lines, like `tac`.",
113
+ category: "Text",
114
+ output: "binary",
115
+ aliases: ["tac"],
116
+ args: {},
117
+ examples: [{ input: "a\nb\nc\n", output: "c\nb\na\n" }],
118
+ run: (input) => mapText(input, (text) => mapLines(text, (lines) => lines.toReversed())),
119
+ });
120
+
121
+ export const addLineNumbers = defineOperation({
122
+ id: "add-line-numbers",
123
+ name: "Add line numbers",
124
+ description: "Prefixes every line with its number, right-aligned, like `cat -n`.",
125
+ category: "Text",
126
+ output: "binary",
127
+ aliases: ["line numberer", "nl", "cat -n"],
128
+ args: {
129
+ start: { type: "number", name: "First number", default: 1 },
130
+ separator: { type: "string", name: "Separator", description: "Placed between the number and the line.", default: " " },
131
+ },
132
+ examples: [
133
+ { input: "a\nb", output: "1 a\n2 b" },
134
+ { input: "a\nb\nc", args: { start: 9, separator: ": " }, output: " 9: a\n10: b\n11: c" },
135
+ ],
136
+ run(input, { start, separator }) {
137
+ return mapText(input, (text) =>
138
+ mapLines(text, (lines) => {
139
+ const width = Math.max(String(start).length, String(start + lines.length - 1).length);
140
+ return lines.map((line, i) => String(start + i).padStart(width) + separator + line);
141
+ }),
142
+ );
143
+ },
144
+ });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Splits text into lines. The line ending is CRLF if the text contains one, else LF. A final line
3
+ * ending does not start a new line: "a\nb\n" gives ["a", "b"], and "" gives no lines.
4
+ */
5
+ export function splitLines(text: string): { lines: string[]; eol: string; trailing: boolean } {
6
+ const eol = text.includes("\r\n") ? "\r\n" : "\n";
7
+ const trailing = text.endsWith(eol);
8
+ const body = trailing ? text.slice(0, -eol.length) : text;
9
+ return { lines: text === "" ? [] : body.split(eol), eol, trailing };
10
+ }
11
+
12
+ /** Applies `fn` to the lines of `text`, keeping its line ending and final line ending. */
13
+ export function mapLines(text: string, fn: (lines: string[]) => string[]): string {
14
+ const { lines, eol, trailing } = splitLines(text);
15
+ if (!lines.length) return text;
16
+ return fn(lines).join(eol) + (trailing ? eol : "");
17
+ }
18
+
19
+ /** Decodes the input as UTF-8, transforms it, and encodes the result back. */
20
+ export function mapText(input: Uint8Array, fn: (text: string) => string): Uint8Array {
21
+ return new TextEncoder().encode(fn(new TextDecoder().decode(input)));
22
+ }
@@ -0,0 +1,85 @@
1
+ import { defineOperation } from "../../operation.ts";
2
+ import { mapText } from "./lines.ts";
3
+
4
+ export const reverseText = defineOperation({
5
+ id: "reverse-text",
6
+ name: "Reverse text",
7
+ description: 'Reverses the order of the characters. Accented letters and emoji stay intact. e.g. "héllo" becomes "olléh".',
8
+ category: "Text",
9
+ output: "binary",
10
+ aliases: ["reverse string", "mirror"],
11
+ args: {},
12
+ examples: [
13
+ { input: "héllo", output: "olléh" },
14
+ { input: "a👍🏽b", output: "b👍🏽a" },
15
+ ],
16
+ run: (input) =>
17
+ mapText(input, (text) => {
18
+ const graphemes = [...new Intl.Segmenter().segment(text)].map((s) => s.segment);
19
+ return graphemes.reverse().join("");
20
+ }),
21
+ });
22
+
23
+ export const removeNewlines = defineOperation({
24
+ id: "remove-newlines",
25
+ name: "Remove newlines",
26
+ description: "Removes every line break (LF, CRLF and CR), or replaces it with the given text.",
27
+ category: "Text",
28
+ output: "binary",
29
+ aliases: ["join lines", "single line", "strip newlines"],
30
+ args: {
31
+ replacement: { type: "string", name: "Replacement", default: "" },
32
+ },
33
+ examples: [
34
+ { input: "a\nb\r\nc", output: "abc" },
35
+ { input: "a\nb\nc", args: { replacement: ", " }, output: "a, b, c" },
36
+ ],
37
+ run: (input, { replacement }) => mapText(input, (text) => text.replace(/\r\n|\r|\n/g, replacement)),
38
+ });
39
+
40
+ export const removeWhitespace = defineOperation({
41
+ id: "remove-whitespace",
42
+ name: "Remove whitespace",
43
+ description: "Removes all spaces, tabs, line breaks and other Unicode whitespace.",
44
+ category: "Text",
45
+ output: "binary",
46
+ aliases: ["remove spaces", "strip whitespace", "trim"],
47
+ args: {
48
+ keepNewlines: { type: "boolean", name: "Keep line breaks", default: false },
49
+ },
50
+ examples: [
51
+ { input: "a b\tc\nd", output: "abcd" },
52
+ { input: "a b\nc d", args: { keepNewlines: true }, output: "ab\ncd" },
53
+ ],
54
+ run: (input, { keepNewlines }) => mapText(input, (text) => text.replace(keepNewlines ? /[^\S\r\n]+/g : /\s+/g, "")),
55
+ });
56
+
57
+ const QUOTES = [
58
+ { value: "double", label: 'Double quotes (")' },
59
+ { value: "single", label: "Single quotes (')" },
60
+ { value: "both", label: "Both" },
61
+ ] as const;
62
+
63
+ const QUOTE_PATTERNS: Record<(typeof QUOTES)[number]["value"], string> = { double: '"', single: "'", both: "\"'" };
64
+
65
+ export const escapeQuotes = defineOperation({
66
+ id: "escape-quotes",
67
+ name: "Escape quotes",
68
+ description: 'Puts a backslash before quotes, and before backslashes so the result is unambiguous. e.g. say "hi" becomes say \\"hi\\".',
69
+ category: "Text",
70
+ output: "binary",
71
+ aliases: ["escape string", "addslashes"],
72
+ args: {
73
+ quotes: { type: "select", name: "Quotes", options: QUOTES, default: "double" },
74
+ escapeBackslashes: { type: "boolean", name: "Escape backslashes", default: true },
75
+ },
76
+ examples: [
77
+ { input: 'say "hi" \\o/', output: 'say \\"hi\\" \\\\o/' },
78
+ { input: `it's "ok"`, args: { quotes: "single" }, output: `it\\'s "ok"` },
79
+ { input: `'a' "b" \\`, args: { quotes: "both", escapeBackslashes: false }, output: `\\'a\\' \\"b\\" \\` },
80
+ ],
81
+ run(input, { quotes, escapeBackslashes }) {
82
+ const pattern = new RegExp(`[${QUOTE_PATTERNS[quotes]}${escapeBackslashes ? "\\\\" : ""}]`, "g");
83
+ return mapText(input, (text) => text.replace(pattern, "\\$&"));
84
+ },
85
+ });
@@ -0,0 +1,55 @@
1
+ import { defineOperation } from "../../operation.ts";
2
+
3
+ const PERCENT = 0x25;
4
+ const PLUS = 0x2b;
5
+ const SPACE = 0x20;
6
+
7
+ function hexValue(byte: number | undefined): number {
8
+ if (byte === undefined) return -1;
9
+ if (byte >= 0x30 && byte <= 0x39) return byte - 0x30;
10
+ const lower = byte | 0x20;
11
+ if (lower >= 0x61 && lower <= 0x66) return lower - 0x61 + 10;
12
+ return -1;
13
+ }
14
+
15
+ export const urlDecode = defineOperation({
16
+ id: "url-decode",
17
+ name: "URL Decode",
18
+ description: "Decodes %XX sequences (percent-encoding) into bytes. Invalid sequences are left as is.",
19
+ category: "Encoding",
20
+ output: "binary",
21
+ aliases: ["percent decode", "urldecode"],
22
+ inverse: "url-encode",
23
+ infoUrl: "https://wikipedia.org/wiki/Percent-encoding",
24
+ args: {
25
+ plusAsSpace: {
26
+ type: "boolean",
27
+ name: 'Treat "+" as space',
28
+ description: "HTML form behavior (application/x-www-form-urlencoded).",
29
+ default: true,
30
+ },
31
+ },
32
+ examples: [
33
+ { input: "a%20b%26c%3D%C3%A9", output: "a b&c=é" },
34
+ { input: "a+b", output: "a b" },
35
+ { input: "a+b", args: { plusAsSpace: false }, output: "a+b" },
36
+ ],
37
+ run(input, { plusAsSpace }) {
38
+ const out = new Uint8Array(input.length);
39
+ let n = 0;
40
+ for (let i = 0; i < input.length; i++) {
41
+ const byte = input[i]!;
42
+ if (byte === PERCENT) {
43
+ const hi = hexValue(input[i + 1]);
44
+ const lo = hexValue(input[i + 2]);
45
+ if (hi >= 0 && lo >= 0) {
46
+ out[n++] = (hi << 4) | lo;
47
+ i += 2;
48
+ continue;
49
+ }
50
+ }
51
+ out[n++] = byte === PLUS && plusAsSpace ? SPACE : byte;
52
+ }
53
+ return out.slice(0, n);
54
+ },
55
+ });
@@ -0,0 +1,38 @@
1
+ import { defineOperation } from "../../operation.ts";
2
+
3
+ // RFC 3986 unreserved characters.
4
+ const UNRESERVED = /[A-Za-z0-9\-_.~]/;
5
+ // Characters also left untouched by `encodeURI`.
6
+ const URI_SAFE = /[A-Za-z0-9\-_.~!*'();/?:@&=+$,#]/;
7
+
8
+ export const urlEncode = defineOperation({
9
+ id: "url-encode",
10
+ name: "URL Encode",
11
+ description: 'Encodes bytes as %XX sequences (percent-encoding). e.g. "a b" becomes "a%20b".',
12
+ category: "Encoding",
13
+ output: "text",
14
+ aliases: ["percent encode", "urlencode"],
15
+ inverse: "url-decode",
16
+ infoUrl: "https://wikipedia.org/wiki/Percent-encoding",
17
+ args: {
18
+ encodeAll: {
19
+ type: "boolean",
20
+ name: "Encode all special chars",
21
+ description: "Also encode reserved characters such as / ? & = (like encodeURIComponent).",
22
+ default: false,
23
+ },
24
+ },
25
+ examples: [
26
+ { input: "a b&c=é", output: "a%20b&c=%C3%A9" },
27
+ { input: "a b&c=é", args: { encodeAll: true }, output: "a%20b%26c%3D%C3%A9" },
28
+ ],
29
+ run(input, { encodeAll }) {
30
+ const keep = encodeAll ? UNRESERVED : URI_SAFE;
31
+ let out = "";
32
+ for (const byte of input) {
33
+ const ch = String.fromCharCode(byte);
34
+ out += byte < 0x80 && keep.test(ch) ? ch : "%" + byte.toString(16).toUpperCase().padStart(2, "0");
35
+ }
36
+ return new TextEncoder().encode(out);
37
+ },
38
+ });