Skip to content

Commit 464787f

Browse files
committed
feat: introduce backslash-escapist and ip-info
1 parent 5fe4356 commit 464787f

File tree

3 files changed

+869
-0
lines changed

3 files changed

+869
-0
lines changed
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
/**
2+
* Copyright (c) 2023-2025, AprilNEA LLC.
3+
*
4+
* Dual licensed under:
5+
* - GPL-3.0 (open source)
6+
* - Commercial license (contact us)
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU General Public License as published by
10+
* the Free Software Foundation, either version 3 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* See LICENSE file for details or contact admin@aprilnea.com
14+
*/
15+
16+
import { useDebouncedValue } from "foxact/use-debounced-value";
17+
import { FlaskConicalIcon } from "lucide-react";
18+
import { useCallback, useEffect, useState } from "react";
19+
import CodecModeRadio, { CodecMode } from "@/components/codec-mode";
20+
import { Callout } from "@/components/derived-ui/callout";
21+
import InputOutputLayout from "@/components/layout/input-output";
22+
import {
23+
ClearTool,
24+
ContinuousModeTool,
25+
CopyTool,
26+
LoadFileTool,
27+
PasteTool,
28+
} from "@/components/tools";
29+
import { Button } from "@/components/ui/button";
30+
31+
// Sample text with various escape sequences
32+
const sampleText = `Hello "World"!
33+
This is a test with:
34+
- New lines
35+
- Tabs\tlike this
36+
- Special chars: \\, ', "
37+
- Unicode: \u4e2d\u6587
38+
- Path: C:\\Users\\Test\\file.txt`;
39+
40+
export default function BackslashEscapistPage() {
41+
const [mode, setMode] = useState<CodecMode>(CodecMode.Encode);
42+
const [input, setInput] = useState("");
43+
const [output, setOutput] = useState("");
44+
const [error, setError] = useState("");
45+
const [continuousMode, setContinuousMode] = useState(true);
46+
47+
// Escape string by adding backslashes
48+
const escapeString = useCallback((str: string): string => {
49+
try {
50+
const escapeMap: { [key: string]: string } = {
51+
"\n": "\\n",
52+
"\r": "\\r",
53+
"\t": "\\t",
54+
"\b": "\\b",
55+
"\f": "\\f",
56+
"\v": "\\v",
57+
"\0": "\\0",
58+
"\\": "\\\\",
59+
'"': '\\"',
60+
"'": "\\'",
61+
};
62+
63+
let result = "";
64+
for (let i = 0; i < str.length; i++) {
65+
const char = str[i];
66+
if (escapeMap[char]) {
67+
result += escapeMap[char];
68+
} else {
69+
const code = char.charCodeAt(0);
70+
// Escape non-printable ASCII characters and Unicode characters
71+
if (code < 32 || code > 126) {
72+
if (code <= 0xff) {
73+
// Use \xHH for extended ASCII
74+
result += `\\x${code.toString(16).padStart(2, "0")}`;
75+
} else {
76+
// Use \uHHHH for Unicode
77+
result += `\\u${code.toString(16).padStart(4, "0")}`;
78+
}
79+
} else {
80+
result += char;
81+
}
82+
}
83+
}
84+
return result;
85+
} catch (e) {
86+
throw new Error("Failed to escape string");
87+
}
88+
}, []);
89+
90+
// Unescape string by interpreting escape sequences
91+
const unescapeString = useCallback((str: string): string => {
92+
try {
93+
let result = "";
94+
let i = 0;
95+
96+
while (i < str.length) {
97+
if (str[i] === "\\" && i + 1 < str.length) {
98+
const nextChar = str[i + 1];
99+
switch (nextChar) {
100+
case "n":
101+
result += "\n";
102+
i += 2;
103+
break;
104+
case "r":
105+
result += "\r";
106+
i += 2;
107+
break;
108+
case "t":
109+
result += "\t";
110+
i += 2;
111+
break;
112+
case "b":
113+
result += "\b";
114+
i += 2;
115+
break;
116+
case "f":
117+
result += "\f";
118+
i += 2;
119+
break;
120+
case "v":
121+
result += "\v";
122+
i += 2;
123+
break;
124+
case "0":
125+
result += "\0";
126+
i += 2;
127+
break;
128+
case "\\":
129+
result += "\\";
130+
i += 2;
131+
break;
132+
case '"':
133+
result += '"';
134+
i += 2;
135+
break;
136+
case "'":
137+
result += "'";
138+
i += 2;
139+
break;
140+
case "x": {
141+
// Handle \xHH
142+
if (i + 3 < str.length) {
143+
const hex = str.substring(i + 2, i + 4);
144+
const code = parseInt(hex, 16);
145+
if (!isNaN(code)) {
146+
result += String.fromCharCode(code);
147+
i += 4;
148+
} else {
149+
result += str[i];
150+
i++;
151+
}
152+
} else {
153+
result += str[i];
154+
i++;
155+
}
156+
break;
157+
}
158+
case "u": {
159+
// Handle \uHHHH
160+
if (i + 5 < str.length) {
161+
const hex = str.substring(i + 2, i + 6);
162+
const code = parseInt(hex, 16);
163+
if (!isNaN(code)) {
164+
result += String.fromCharCode(code);
165+
i += 6;
166+
} else {
167+
result += str[i];
168+
i++;
169+
}
170+
} else {
171+
result += str[i];
172+
i++;
173+
}
174+
break;
175+
}
176+
default:
177+
// If not a recognized escape sequence, keep the backslash
178+
result += str[i];
179+
i++;
180+
}
181+
} else {
182+
result += str[i];
183+
i++;
184+
}
185+
}
186+
return result;
187+
} catch (e) {
188+
throw new Error("Failed to unescape string");
189+
}
190+
}, []);
191+
192+
const handleConvert = useCallback(
193+
(text: string, convertMode: CodecMode) => {
194+
if (!text) {
195+
setOutput("");
196+
setError("");
197+
return;
198+
}
199+
200+
try {
201+
setError("");
202+
const result =
203+
convertMode === CodecMode.Encode
204+
? escapeString(text)
205+
: unescapeString(text);
206+
setOutput(result);
207+
} catch (e) {
208+
setError(e instanceof Error ? e.message : "Conversion failed");
209+
setOutput("");
210+
}
211+
},
212+
[escapeString, unescapeString],
213+
);
214+
215+
const handleClear = () => {
216+
setInput("");
217+
setOutput("");
218+
setError("");
219+
};
220+
221+
const debouncedInput = useDebouncedValue(input, 100, true);
222+
223+
useEffect(() => {
224+
if (continuousMode && debouncedInput) {
225+
handleConvert(debouncedInput, mode);
226+
}
227+
}, [continuousMode, debouncedInput, mode, handleConvert]);
228+
229+
const inputToolbar = (
230+
<>
231+
<CodecModeRadio mode={mode} setMode={setMode} />
232+
<ContinuousModeTool />
233+
<PasteTool onPaste={(text) => setInput(text)} />
234+
<LoadFileTool />
235+
<Button
236+
variant="ghost"
237+
size="icon"
238+
onClick={() => {
239+
setInput(sampleText);
240+
if (!continuousMode) {
241+
handleConvert(sampleText, mode);
242+
}
243+
}}
244+
className="text-muted-foreground hover:text-foreground h-8 w-8"
245+
>
246+
<FlaskConicalIcon size={18} />
247+
</Button>
248+
<ClearTool button={{ onClick: handleClear }} />
249+
</>
250+
);
251+
252+
const inputBottombar = error && (
253+
<Callout variant="error" className="w-full">
254+
{error}
255+
</Callout>
256+
);
257+
258+
const outputToolbar = (
259+
<>
260+
<Button variant="outline" size="sm" className="h-8 min-w-8">
261+
{mode === CodecMode.Encode ? "Escaped" : "Unescaped"}
262+
</Button>
263+
<CopyTool content={output} />
264+
</>
265+
);
266+
267+
return (
268+
<InputOutputLayout
269+
inputToolbar={inputToolbar}
270+
inputBottombar={inputBottombar}
271+
inputProps={{
272+
value: input,
273+
onChange: (e) => {
274+
setInput(e.target.value);
275+
if (!continuousMode) {
276+
setOutput("");
277+
setError("");
278+
}
279+
},
280+
onKeyDown: (e) => {
281+
if (
282+
!continuousMode &&
283+
e.key === "Enter" &&
284+
(e.ctrlKey || e.metaKey)
285+
) {
286+
e.preventDefault();
287+
handleConvert(input, mode);
288+
}
289+
},
290+
placeholder:
291+
mode === CodecMode.Encode
292+
? "Enter text to escape special characters..."
293+
: "Enter escaped text to unescape...",
294+
}}
295+
outputToolbar={outputToolbar}
296+
outputProps={{
297+
value: output,
298+
readOnly: true,
299+
placeholder:
300+
mode === CodecMode.Encode
301+
? "Escaped text will appear here..."
302+
: "Unescaped text will appear here...",
303+
}}
304+
/>
305+
);
306+
}

packages/frontend/src/utilities/meta.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import NumberIcon from "@/assets/number-icon";
4141
import StringIcon from "@/assets/string-icon";
4242
import UUIDIcon from "@/assets/uuid-icon";
4343
import JwtDecoderPage from "./codec/jwt";
44+
import BackslashEscapistPage from "./converter/backslash-escapist";
4445
import NumberCasePage from "./converter/number-case";
4546
import StringInspectorPage from "./converter/string-inspector";
4647
import Fido2Page from "./cryptography/fido/fido2";
@@ -61,6 +62,7 @@ const NumberCaseConverterPage = lazy(() => import("./converter/number-case"));
6162
const TotpDebuggerPage = lazy(() => import("./cryptography/oath/totp"));
6263
// const HidDevicesPage = lazy(() => import("./hardware/hid"));
6364
const UnixTimePage = lazy(() => import("./converter/unix-time"));
65+
const IpInfoPage = lazy(() => import("./network/ip-info"));
6466

6567
export type Utility = {
6668
key: string;
@@ -164,6 +166,12 @@ const utilities: UtilityMeta[] = [
164166
title: msg`Unix Time Converter`,
165167
page: UnixTimePage,
166168
},
169+
{
170+
key: "backslash-escapist",
171+
icon: FileCodeIcon,
172+
title: msg`Backslash Escape/Unescape`,
173+
page: BackslashEscapistPage,
174+
},
167175
// {
168176
// key: "url-parser",
169177
// title: msg`URL Parser`,
@@ -246,6 +254,12 @@ const utilities: UtilityMeta[] = [
246254
title: msg`IP Address Calculator`,
247255
page: IpPage,
248256
},
257+
{
258+
key: "ip-info",
259+
icon: LinkIcon,
260+
title: msg`IP Info`,
261+
page: IpInfoPage,
262+
},
249263
],
250264
},
251265
// {

0 commit comments

Comments
 (0)