feat: add many transforms
All checks were successful
/ deploy_site (push) Successful in 1m33s

This commit is contained in:
Minhyeok Park 2024-10-27 19:38:59 +09:00
parent 5bd0bf1a95
commit 4ac92e5fa2
Signed by: pmh_only
SSH Key Fingerprint: SHA256:g/OyGvi2pcd8ub9mqge/ohmDP0fZX/xOPWPIcM+9XpI
12 changed files with 261 additions and 40 deletions

View File

@ -14,11 +14,13 @@
"@monaco-editor/react": "^4.6.0", "@monaco-editor/react": "^4.6.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^11.11.10", "framer-motion": "^11.11.10",
"json5": "^2.2.3",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"recoil": "^0.7.7", "recoil": "^0.7.7",
"styled-components": "^6.1.13" "styled-components": "^6.1.13",
"yaml": "^2.6.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.13.0", "@eslint/js": "^9.13.0",

View File

@ -20,6 +20,9 @@ importers:
framer-motion: framer-motion:
specifier: ^11.11.10 specifier: ^11.11.10
version: 11.11.10(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 11.11.10(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
json5:
specifier: ^2.2.3
version: 2.2.3
normalize.css: normalize.css:
specifier: ^8.0.1 specifier: ^8.0.1
version: 8.0.1 version: 8.0.1
@ -35,6 +38,9 @@ importers:
styled-components: styled-components:
specifier: ^6.1.13 specifier: ^6.1.13
version: 6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 6.1.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
yaml:
specifier: ^2.6.0
version: 2.6.0
devDependencies: devDependencies:
'@eslint/js': '@eslint/js':
specifier: ^9.13.0 specifier: ^9.13.0
@ -950,6 +956,11 @@ packages:
json-stable-stringify-without-jsonify@1.0.1: json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
json5@2.2.3:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
engines: {node: '>=6'}
hasBin: true
keyv@4.5.4: keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@ -1368,6 +1379,11 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
yaml@2.6.0:
resolution: {integrity: sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==}
engines: {node: '>= 14'}
hasBin: true
yocto-queue@0.1.0: yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -2117,6 +2133,8 @@ snapshots:
json-stable-stringify-without-jsonify@1.0.1: {} json-stable-stringify-without-jsonify@1.0.1: {}
json5@2.2.3: {}
keyv@4.5.4: keyv@4.5.4:
dependencies: dependencies:
json-buffer: 3.0.1 json-buffer: 3.0.1
@ -2490,4 +2508,6 @@ snapshots:
word-wrap@1.2.5: {} word-wrap@1.2.5: {}
yaml@2.6.0: {}
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}

View File

@ -16,7 +16,7 @@ export const Editor: FC = () => {
loading={<></>} loading={<></>}
value={value} value={value}
language="yaml" language="yaml"
onChange={(v) => setValue(v ?? '')} onChange={(v) => setValue(v?.trim() ?? '')}
options={{ options={{
automaticLayout: true, automaticLayout: true,
lineNumbersMinChars: 3, lineNumbersMinChars: 3,
@ -31,7 +31,7 @@ export const Editor: FC = () => {
smoothScrolling: true, smoothScrolling: true,
cursorSmoothCaretAnimation: 'on', cursorSmoothCaretAnimation: 'on',
cursorBlinking: 'smooth', cursorBlinking: 'smooth',
cursorStyle: 'block' cursorStyle: 'line'
}} }}
theme="vs-dark" /> theme="vs-dark" />
</motion.div> </motion.div>

View File

@ -1,12 +1,21 @@
import { ChangeEvent, FC, useState } from "react"; import { ChangeEvent, FC, useEffect, useState } from "react";
import style from './style.module.scss' import style from './style.module.scss'
import { Button } from "../Components/Button"; import { Button } from "../Components/Button";
import { Input } from "../Components/Input"; import { Input } from "../Components/Input";
import { TextArea } from "../Components/TextArea"; import { TextArea } from "../Components/TextArea";
import { TransformCheckboxOption, TransformOption, TransformTextboxOption, WrappedTransform } from "../Transforms/Transform"; import clsx from "clsx";
import { useRecoilState } from "recoil"; import { useRecoilState } from "recoil";
import { EditorValueState } from "../GlobalStates/EditorValueState"; import { EditorValueState } from "../GlobalStates/EditorValueState";
import clsx from "clsx"; import {
isTransformCheckboxOption,
isTransformIntboxOption,
isTransformTextboxOption,
TransformCheckboxOption,
TransformIntboxOption,
TransformTextboxOption,
WrappedTransform,
WrappedTransformResult
} from "../Transforms/Transform";
interface TransformGridItemProp { interface TransformGridItemProp {
transform: WrappedTransform transform: WrappedTransform
@ -14,9 +23,17 @@ interface TransformGridItemProp {
export const TransformGridItem: FC<TransformGridItemProp> = ({ transform }) => { export const TransformGridItem: FC<TransformGridItemProp> = ({ transform }) => {
const [value, setValue] = useRecoilState(EditorValueState) const [value, setValue] = useRecoilState(EditorValueState)
const [options, setOptions] = useState(new Map<string, TransformOption>()) const [options, setOptions] = useState(transform.options)
const [result, setResult] = useState<WrappedTransformResult>({
error: false,
value: ''
})
const result = transform.fn(value, options) useEffect(() => {
transform
.fn(value, options)
.then(setResult.bind(this))
}, [value, options])
const onCheckboxOptionChanged = const onCheckboxOptionChanged =
(option: TransformCheckboxOption) => (option: TransformCheckboxOption) =>
@ -43,6 +60,18 @@ export const TransformGridItem: FC<TransformGridItemProp> = ({ transform }) => {
setOptions(new Map(options)) setOptions(new Map(options))
} }
const onIntboxOptionChanged =
(option: TransformIntboxOption) =>
(event: ChangeEvent<HTMLInputElement>) => {
options.set(option.key, {
...option,
value: parseInt(event.target.value)
})
setOptions(new Map(options))
}
const onForwardButtonPressed = () => { const onForwardButtonPressed = () => {
if (result.error) if (result.error)
return return
@ -60,33 +89,49 @@ export const TransformGridItem: FC<TransformGridItemProp> = ({ transform }) => {
<h2 className={style.name}>{transform.name}</h2> <h2 className={style.name}>{transform.name}</h2>
<div className={style.options}> <div className={style.options}>
{transform.options {[...options.values()]
?.filter((option) => option.type === 'CHECKBOX') ?.filter(isTransformCheckboxOption)
.map((option, i) => ( .map((option, i) => (
<label key={i} className={style.optionItem}> <label key={i} className={style.optionItem}>
<p>{option.label ?? option.key}:</p> <p>{option.label ?? option.key}:</p>
<Input <Input
checked={option.value}
onChange={onCheckboxOptionChanged(option)} onChange={onCheckboxOptionChanged(option)}
type="checkbox" /> type="checkbox" />
</label>))} </label>))}
{transform.options {[...options.values()]
?.filter((option) => option.type === 'TEXTBOX') ?.filter(isTransformTextboxOption)
.map((option, i) => ( .map((option, i) => (
<label key={i} className={style.optionItem}> <label key={i} className={style.optionItem}>
<p>{option.label ?? option.key}:</p> <p>{option.label ?? option.key}:</p>
<Input <Input
value={option.value}
onChange={onTextboxOptionChanged(option)} onChange={onTextboxOptionChanged(option)}
type="checkbox" /> type="text" />
</label>))}
{[...options.values()]
?.filter(isTransformIntboxOption)
.map((option, i) => (
<label key={i} className={style.optionItem}>
<p>{option.label ?? option.key}:</p>
<Input
min={1}
value={option.value}
onChange={onIntboxOptionChanged(option)}
type="number" />
</label>))} </label>))}
</div> </div>
</div> </div>
<TextArea <TextArea
value={result.value}
readOnly readOnly
value={result.value}
placeholder="(empty)" placeholder="(empty)"
className={clsx(result.error && style.error)} /> className={clsx(result.error && style.error)} />
</div> </div>

View File

@ -2,10 +2,10 @@ import { Transform } from "./Transform";
export const Base64DecodeTransform: Transform = { export const Base64DecodeTransform: Transform = {
name: 'base64d', name: 'base64d',
fn: (v) => btoa(v) fn: async (v) => btoa(v)
} }
export const Base64EncodeTransform: Transform = { export const Base64EncodeTransform: Transform = {
name: 'base64e', name: 'base64e',
fn: (v) => atob(v) fn: async (v) => atob(v)
} }

View File

@ -0,0 +1,23 @@
import { Transform } from "./Transform"
const decompressGzip = async (base64: string) => {
const byteCharacters = atob(base64)
const byteNumbers = new Array(byteCharacters.length)
for (let i = 0; i < byteCharacters.length; i++)
byteNumbers[i] = byteCharacters.charCodeAt(i)
const decompressedStream =
new Blob([new Uint8Array(byteNumbers)])
.stream()
.pipeThrough(new DecompressionStream("gzip"))
return await new Response(decompressedStream).text()
}
export const GzipDecompressTransform: Transform = {
name: 'gzipd',
fn: (v) => decompressGzip(v)
}

View File

@ -0,0 +1,50 @@
import JSON5 from 'json5'
import { Transform } from "./Transform";
export const JSONBeautifyTransform: Transform = {
name: 'jsonbtf',
fn: async (v, o) =>
o.get('multiline')?.value === true
? JSON.stringify(JSON5.parse(v), null, o.get('tab')?.value as number ?? 2)
: v.split('\n').map((v2) => JSON.stringify(JSON5.parse(v2), null, o.get('tab')?.value as number ?? 2)).join('\n'),
options: [
{
type: 'CHECKBOX',
key: 'multiline'
},
{
type: 'INTBOX',
key: 'tab',
value: 2
}
]
}
export const JSONSimplifyTransform: Transform = {
name: 'jsonsmp',
fn: async (v) =>
JSON.stringify(JSON5.parse(v))
}
export const JSONEscapeTransform: Transform = {
name: 'jsonesc',
fn: async (v) =>
JSON.stringify(v)
}
export const JSONUnescapeTransform: Transform = {
name: 'jsonunesc',
fn: async (v) => {
const result = JSON5.parse(v)
if (typeof result !== 'string')
throw new Error('Not JSON escaped')
return result
}
}

View File

@ -0,0 +1,21 @@
import { Transform } from "./Transform";
export const RegexpTransform: Transform = {
name: 'regexp',
fn: async (v) => {
const [expression, samples] = v.split('\n\n')
const regexp = new RegExp(expression)
const parsedSamples = samples
.split('\n')
.map((sample) =>
JSON.stringify(regexp.exec(sample)?.groups))
.join('\n')
return [
expression,
samples,
parsedSamples
].join('\n\n')
}
}

View File

@ -1,5 +1,9 @@
import { Base64DecodeTransform, Base64EncodeTransform } from "./Base64Transforms" import { Base64DecodeTransform, Base64EncodeTransform } from "./Base64Transforms"
import { GzipDecompressTransform } from "./GzipTransform"
import { JSONBeautifyTransform, JSONEscapeTransform, JSONSimplifyTransform, JSONUnescapeTransform } from "./JSONTransforms"
import { RegexpTransform } from "./RegexpTransform"
import { URIDecodeTransform, URIEncodeTransform } from "./URITransforms" import { URIDecodeTransform, URIEncodeTransform } from "./URITransforms"
import { JSON2YAMLTransform, YAML2JSONTransform } from "./YAMLTransforms"
export interface TransformCheckboxOption { export interface TransformCheckboxOption {
type: 'CHECKBOX', type: 'CHECKBOX',
@ -16,44 +20,72 @@ export interface TransformTextboxOption {
value?: string value?: string
} }
export interface TransformIntboxOption {
type: 'INTBOX',
key: string,
label?: string,
value?: number
}
export type TransformOption = export type TransformOption =
TransformCheckboxOption | TransformTextboxOption TransformCheckboxOption | TransformTextboxOption | TransformIntboxOption
export const isTransformCheckboxOption = (object: any): object is TransformCheckboxOption =>
object.type === 'CHECKBOX'
export const isTransformTextboxOption = (object: any): object is TransformTextboxOption =>
object.type === 'TEXTBOX'
export const isTransformIntboxOption = (object: any): object is TransformIntboxOption =>
object.type === 'INTBOX'
export interface Transform { export interface Transform {
name: string name: string
fn: (value: string, options: Map<string, TransformOption>) => string fn: (value: string, options: Map<string, TransformOption>) => Promise<string>
options?: TransformOption[] options?: TransformOption[]
} }
export interface WrappedTransform extends Omit<Transform, 'fn'> { export interface WrappedTransformResult {
error: boolean
value: string
}
export interface WrappedTransform extends Omit<Transform, 'fn' | 'options'> {
wrapped: true, wrapped: true,
fn: (value: string, options: Map<string, TransformOption>) => {
error: boolean, fn: (value: string, options: Map<string, TransformOption>) =>
value: string Promise<WrappedTransformResult>,
}
options: Map<string, TransformOption>
} }
export const wrapTransform = (transform: Transform): WrappedTransform => ({ export const wrapTransform = (transform: Transform): WrappedTransform => ({
...transform, ...transform,
fn: (...args) => {
try { fn: async (...args) =>
return { await transform.fn(...args)
error: false, .then((value) =>
value: transform.fn(...args) ({ error: false, value: value.toString() }))
} .catch((error) =>
} catch (err: any) { ({ error: true, value: error.toString() })),
return {
error: true, options: new Map<string, TransformOption>(
value: err.toString() (transform.options ?? []).map((v) => [v.key, v])
} ),
}
},
wrapped: true wrapped: true
}) })
export const transforms: Transform[] = [ export const transforms: Transform[] = [
RegexpTransform,
GzipDecompressTransform,
Base64DecodeTransform, Base64DecodeTransform,
Base64EncodeTransform, Base64EncodeTransform,
URIDecodeTransform, URIDecodeTransform,
URIEncodeTransform URIEncodeTransform,
JSONBeautifyTransform,
JSONSimplifyTransform,
JSONEscapeTransform,
JSONUnescapeTransform,
JSON2YAMLTransform,
YAML2JSONTransform
] ]

View File

@ -3,7 +3,7 @@ import { Transform } from "./Transform";
export const URIDecodeTransform: Transform = { export const URIDecodeTransform: Transform = {
name: 'urid', name: 'urid',
fn: (v, o) => fn: async (v, o) =>
o.get('cmp')?.value === true o.get('cmp')?.value === true
? decodeURIComponent(v) ? decodeURIComponent(v)
: decodeURI(v), : decodeURI(v),
@ -17,7 +17,7 @@ export const URIDecodeTransform: Transform = {
export const URIEncodeTransform: Transform = { export const URIEncodeTransform: Transform = {
name: 'urie', name: 'urie',
fn: (v, o) => fn: async (v, o) =>
o.get('cmp')?.value === true o.get('cmp')?.value === true
? encodeURIComponent(v) ? encodeURIComponent(v)
: encodeURI(v), : encodeURI(v),

View File

@ -0,0 +1,17 @@
import { Transform } from "./Transform";
import YAML from 'yaml'
import JSON5 from 'json5'
export const YAML2JSONTransform: Transform = {
name: 'yaml2json',
fn: async (v) =>
JSON.stringify(YAML.parse(v))
}
export const JSON2YAMLTransform: Transform = {
name: 'json2yaml',
fn: async (v) =>
YAML.stringify(JSON5.parse(v))
}

View File

@ -10,3 +10,14 @@ h1, h2, h3, h4, h5, h6, p, pre, textarea, input, button, li, ul, ol {
margin: 0; margin: 0;
list-style-type: none; list-style-type: none;
} }
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
appearance: textfield;
-moz-appearance: textfield;
}