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

This commit is contained in:
2024-10-27 19:38:59 +09:00
parent 5bd0bf1a95
commit 4ac92e5fa2
12 changed files with 261 additions and 40 deletions

View File

@ -16,7 +16,7 @@ export const Editor: FC = () => {
loading={<></>}
value={value}
language="yaml"
onChange={(v) => setValue(v ?? '')}
onChange={(v) => setValue(v?.trim() ?? '')}
options={{
automaticLayout: true,
lineNumbersMinChars: 3,
@ -31,7 +31,7 @@ export const Editor: FC = () => {
smoothScrolling: true,
cursorSmoothCaretAnimation: 'on',
cursorBlinking: 'smooth',
cursorStyle: 'block'
cursorStyle: 'line'
}}
theme="vs-dark" />
</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 { Button } from "../Components/Button";
import { Input } from "../Components/Input";
import { TextArea } from "../Components/TextArea";
import { TransformCheckboxOption, TransformOption, TransformTextboxOption, WrappedTransform } from "../Transforms/Transform";
import clsx from "clsx";
import { useRecoilState } from "recoil";
import { EditorValueState } from "../GlobalStates/EditorValueState";
import clsx from "clsx";
import {
isTransformCheckboxOption,
isTransformIntboxOption,
isTransformTextboxOption,
TransformCheckboxOption,
TransformIntboxOption,
TransformTextboxOption,
WrappedTransform,
WrappedTransformResult
} from "../Transforms/Transform";
interface TransformGridItemProp {
transform: WrappedTransform
@ -14,9 +23,17 @@ interface TransformGridItemProp {
export const TransformGridItem: FC<TransformGridItemProp> = ({ transform }) => {
const [value, setValue] = useRecoilState(EditorValueState)
const [options, setOptions] = useState(new Map<string, TransformOption>())
const result = transform.fn(value, options)
const [options, setOptions] = useState(transform.options)
const [result, setResult] = useState<WrappedTransformResult>({
error: false,
value: ''
})
useEffect(() => {
transform
.fn(value, options)
.then(setResult.bind(this))
}, [value, options])
const onCheckboxOptionChanged =
(option: TransformCheckboxOption) =>
@ -42,6 +59,18 @@ export const TransformGridItem: FC<TransformGridItemProp> = ({ transform }) => {
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 = () => {
if (result.error)
@ -60,33 +89,49 @@ export const TransformGridItem: FC<TransformGridItemProp> = ({ transform }) => {
<h2 className={style.name}>{transform.name}</h2>
<div className={style.options}>
{transform.options
?.filter((option) => option.type === 'CHECKBOX')
{[...options.values()]
?.filter(isTransformCheckboxOption)
.map((option, i) => (
<label key={i} className={style.optionItem}>
<p>{option.label ?? option.key}:</p>
<Input
checked={option.value}
onChange={onCheckboxOptionChanged(option)}
type="checkbox" />
</label>))}
{transform.options
?.filter((option) => option.type === 'TEXTBOX')
{[...options.values()]
?.filter(isTransformTextboxOption)
.map((option, i) => (
<label key={i} className={style.optionItem}>
<p>{option.label ?? option.key}:</p>
<Input
value={option.value}
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>))}
</div>
</div>
<TextArea
value={result.value}
readOnly
value={result.value}
placeholder="(empty)"
className={clsx(result.error && style.error)} />
</div>

View File

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

View File

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