From faf64a0ff9d5528076264a5c50cec7453513deec Mon Sep 17 00:00:00 2001
From: Pedro Carreno <34664891+Pkcarreno@users.noreply.github.com>
Date: Fri, 25 Oct 2024 19:07:06 -0400
Subject: [PATCH 1/4] fix(engine): increase memory limit & max stack size in
quickjs runtime
---
src/features/editor/utils/engine/runtime.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/features/editor/utils/engine/runtime.ts b/src/features/editor/utils/engine/runtime.ts
index 9d4d468..31c92b3 100644
--- a/src/features/editor/utils/engine/runtime.ts
+++ b/src/features/editor/utils/engine/runtime.ts
@@ -92,8 +92,8 @@ export const executeCode: (
ctxRef = ctx;
let interruptCycles = 0;
- ctx.runtime.setMemoryLimit(1024 * 640);
- ctx.runtime.setMaxStackSize(1024 * 320);
+ ctx.runtime.setMemoryLimit(1024 * 1024);
+ ctx.runtime.setMaxStackSize(1024 * 1024);
ctx.runtime.setInterruptHandler(() => {
DEBUG('interrupt handler triggered. time: ', interruptCycles);
return ++interruptCycles > loopThreshold;
From 7f5d322747187391dc1c860949bfbcae32a979bd Mon Sep 17 00:00:00 2001
From: Pedro Carreno <34664891+Pkcarreno@users.noreply.github.com>
Date: Fri, 1 Nov 2024 19:49:37 -0400
Subject: [PATCH 2/4] fix: improve clickability of untrusted mode sign
---
src/features/editor/components/header/untrusted-mode-sign.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/features/editor/components/header/untrusted-mode-sign.tsx b/src/features/editor/components/header/untrusted-mode-sign.tsx
index 4c40ae7..81a8899 100644
--- a/src/features/editor/components/header/untrusted-mode-sign.tsx
+++ b/src/features/editor/components/header/untrusted-mode-sign.tsx
@@ -10,7 +10,7 @@ export const UntrustedModeSign = () => {
setUntrustedDialogOpen(true)}
>
Editor in Untrusted Mode
From 0b7d36dc384358984de9ded69916aa1cccd6274a Mon Sep 17 00:00:00 2001
From: Pedro Carreno <34664891+Pkcarreno@users.noreply.github.com>
Date: Fri, 1 Nov 2024 22:59:53 -0400
Subject: [PATCH 3/4] fix(meta): improve metatags
---
index.html | 22 +++++++++++++++++++++-
vite.config.ts | 2 +-
2 files changed, 22 insertions(+), 2 deletions(-)
diff --git a/index.html b/index.html
index f0e64d1..1459491 100644
--- a/index.html
+++ b/index.html
@@ -3,7 +3,27 @@
- JSoD - JS on Demand
+
+ JSoD
+
+
+
+
+
+
+
+
+
+
diff --git a/vite.config.ts b/vite.config.ts
index 27d3d04..53fbc69 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -66,7 +66,7 @@ export default defineConfig(({ mode }) => {
registerType: 'prompt',
injectRegister: 'auto',
manifest: {
- name: 'JS on Demand',
+ name: 'JSOD',
short_name: 'JSOD',
id: 'com.pkcarreno.jsod',
start_url: `${process.env.BASE_URL}/`,
From 318eb2c7b713ae9ca1d3ea1296617f0c77511453 Mon Sep 17 00:00:00 2001
From: Pedro Carreno <34664891+Pkcarreno@users.noreply.github.com>
Date: Sat, 2 Nov 2024 01:15:21 -0400
Subject: [PATCH 4/4] feat: add helmet & script title and description
---
package.json | 4 +
pnpm-lock.yaml | 53 ++++++
src/app.tsx | 16 +-
src/components/ui/form.tsx | 171 ++++++++++++++++++
src/components/ui/textarea.tsx | 23 +++
.../editor/components/header/index.tsx | 17 +-
.../editor/components/header/info/index.tsx | 67 +++++++
.../components/header/info/info-form.tsx | 85 +++++++++
.../components/header/untrusted-mode-sign.tsx | 6 +-
src/features/editor/providers/index.tsx | 17 +-
.../editor/providers/meta-provider.tsx | 20 ++
.../editor/stores/editor/code-store.ts | 15 +-
src/features/not-found/index.tsx | 22 ++-
13 files changed, 482 insertions(+), 34 deletions(-)
create mode 100644 src/components/ui/form.tsx
create mode 100644 src/components/ui/textarea.tsx
create mode 100644 src/features/editor/components/header/info/index.tsx
create mode 100644 src/features/editor/components/header/info/info-form.tsx
create mode 100644 src/features/editor/providers/meta-provider.tsx
diff --git a/package.json b/package.json
index 92446b8..878498f 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"@codemirror/lint": "^6.8.1",
"@fontsource/ibm-plex-mono": "^5.1.0",
"@fontsource/ibm-plex-sans": "^5.1.0",
+ "@hookform/resolvers": "^3.9.1",
"@jitl/quickjs-ng-wasmfile-release-sync": "0.31.0",
"@lezer/highlight": "^1.2.1",
"@radix-ui/react-accordion": "^1.2.1",
@@ -60,6 +61,8 @@
"quickjs-emscripten-sync": "^1.5.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-helmet-async": "^2.0.5",
+ "react-hook-form": "^7.53.1",
"react-hotkeys-hook": "^4.5.1",
"react-resizable-panels": "^2.1.4",
"react-virtuoso": "^4.12.0",
@@ -67,6 +70,7 @@
"sonner": "^1.5.0",
"tailwind-merge": "^2.5.4",
"wouter": "^3.3.5",
+ "zod": "^3.23.8",
"zustand": "^5.0.0"
},
"devDependencies": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e5028ac..87a35f7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -20,6 +20,9 @@ importers:
'@fontsource/ibm-plex-sans':
specifier: ^5.1.0
version: 5.1.0
+ '@hookform/resolvers':
+ specifier: ^3.9.1
+ version: 3.9.1(react-hook-form@7.53.1(react@18.3.1))
'@jitl/quickjs-ng-wasmfile-release-sync':
specifier: 0.31.0
version: 0.31.0
@@ -128,6 +131,12 @@ importers:
react-dom:
specifier: ^18.2.0
version: 18.3.1(react@18.3.1)
+ react-helmet-async:
+ specifier: ^2.0.5
+ version: 2.0.5(react@18.3.1)
+ react-hook-form:
+ specifier: ^7.53.1
+ version: 7.53.1(react@18.3.1)
react-hotkeys-hook:
specifier: ^4.5.1
version: 4.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -149,6 +158,9 @@ importers:
wouter:
specifier: ^3.3.5
version: 3.3.5(react@18.3.1)
+ zod:
+ specifier: ^3.23.8
+ version: 3.23.8
zustand:
specifier: ^5.0.0
version: 5.0.0(@types/react@18.3.11)(react@18.3.1)(use-sync-external-store@1.2.2(react@18.3.1))
@@ -1144,6 +1156,11 @@ packages:
'@fontsource/ibm-plex-sans@5.1.0':
resolution: {integrity: sha512-v2aFHGh33ogG+At6dVNUCX6vWlNAhQ6STWj5WrBKPxVWX1SsAnHNq8sXQBa7WHEt29Irmozuk7GTp6GzFlpwdQ==}
+ '@hookform/resolvers@3.9.1':
+ resolution: {integrity: sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==}
+ peerDependencies:
+ react-hook-form: ^7.0.0
+
'@humanfs/core@0.19.0':
resolution: {integrity: sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==}
engines: {node: '>=18.18.0'}
@@ -3712,6 +3729,20 @@ packages:
peerDependencies:
react: ^18.3.1
+ react-fast-compare@3.2.2:
+ resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
+
+ react-helmet-async@2.0.5:
+ resolution: {integrity: sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg==}
+ peerDependencies:
+ react: ^16.6.0 || ^17.0.0 || ^18.0.0
+
+ react-hook-form@7.53.1:
+ resolution: {integrity: sha512-6aiQeBda4zjcuaugWvim9WsGqisoUk+etmFEsSUMm451/Ic8L/UAb7sRtMj3V+Hdzm6mMjU1VhiSzYUZeBm0Vg==}
+ engines: {node: '>=18.0.0'}
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18 || ^19
+
react-hotkeys-hook@4.5.1:
resolution: {integrity: sha512-scAEJOh3Irm0g95NIn6+tQVf/OICCjsQsC9NBHfQws/Vxw4sfq1tDQut5fhTEvPraXhu/sHxRd9lOtxzyYuNAg==}
peerDependencies:
@@ -3917,6 +3948,9 @@ packages:
resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
engines: {node: '>= 0.4'}
+ shallowequal@1.1.0:
+ resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
+
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
@@ -5797,6 +5831,10 @@ snapshots:
'@fontsource/ibm-plex-sans@5.1.0': {}
+ '@hookform/resolvers@3.9.1(react-hook-form@7.53.1(react@18.3.1))':
+ dependencies:
+ react-hook-form: 7.53.1(react@18.3.1)
+
'@humanfs/core@0.19.0': {}
'@humanfs/node@0.16.5':
@@ -8487,6 +8525,19 @@ snapshots:
react: 18.3.1
scheduler: 0.23.2
+ react-fast-compare@3.2.2: {}
+
+ react-helmet-async@2.0.5(react@18.3.1):
+ dependencies:
+ invariant: 2.2.4
+ react: 18.3.1
+ react-fast-compare: 3.2.2
+ shallowequal: 1.1.0
+
+ react-hook-form@7.53.1(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+
react-hotkeys-hook@4.5.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
react: 18.3.1
@@ -8711,6 +8762,8 @@ snapshots:
functions-have-names: 1.2.3
has-property-descriptors: 1.0.2
+ shallowequal@1.1.0: {}
+
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
diff --git a/src/app.tsx b/src/app.tsx
index 67ae4a4..e038dc9 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -1,3 +1,5 @@
+import { HelmetProvider } from 'react-helmet-async';
+
import { Toaster } from '@/components/ui/sonner';
import { ThemeProvider } from '@/providers/theme-provider';
import Router from '@/routes';
@@ -5,12 +7,14 @@ import Router from '@/routes';
import { PromptProvider } from './providers/prompt-provider';
const App = () => (
-
-
-
-
-
-
+
+
+
+
+
+
+
+
);
export default App;
diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx
new file mode 100644
index 0000000..69cc7d5
--- /dev/null
+++ b/src/components/ui/form.tsx
@@ -0,0 +1,171 @@
+import type * as LabelPrimitive from '@radix-ui/react-label';
+import { Slot } from '@radix-ui/react-slot';
+import * as React from 'react';
+import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
+import { Controller, FormProvider, useFormContext } from 'react-hook-form';
+
+import { Label } from '@/components/ui/label';
+import { cn } from '@/lib/utils';
+
+const Form = FormProvider;
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath,
+> = {
+ name: TName;
+};
+
+const FormFieldContext = React.createContext(
+ {} as FormFieldContextValue,
+);
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath,
+>({
+ ...props
+}: ControllerProps) => {
+ return (
+
+
+
+ );
+};
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext);
+ const itemContext = React.useContext(FormItemContext);
+ const { getFieldState, formState } = useFormContext();
+
+ const fieldState = getFieldState(fieldContext.name, formState);
+
+ if (!fieldContext) {
+ throw new Error('useFormField should be used within ');
+ }
+
+ const { id } = itemContext;
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ };
+};
+
+type FormItemContextValue = {
+ id: string;
+};
+
+const FormItemContext = React.createContext(
+ {} as FormItemContextValue,
+);
+
+const FormItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const id = React.useId();
+
+ return (
+
+
+
+ );
+});
+FormItem.displayName = 'FormItem';
+
+const FormLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ const { error, formItemId } = useFormField();
+
+ return (
+
+ );
+});
+FormLabel.displayName = 'FormLabel';
+
+const FormControl = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ ...props }, ref) => {
+ const { error, formItemId, formDescriptionId, formMessageId } =
+ useFormField();
+
+ return (
+
+ );
+});
+FormControl.displayName = 'FormControl';
+
+const FormDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { formDescriptionId } = useFormField();
+
+ return (
+
+ );
+});
+FormDescription.displayName = 'FormDescription';
+
+const FormMessage = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, children, ...props }, ref) => {
+ const { error, formMessageId } = useFormField();
+ const body = error ? String(error?.message) : children;
+
+ if (!body) {
+ return null;
+ }
+
+ return (
+
+ {body}
+
+ );
+});
+FormMessage.displayName = 'FormMessage';
+
+export {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+ useFormField,
+};
diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx
new file mode 100644
index 0000000..be6aed7
--- /dev/null
+++ b/src/components/ui/textarea.tsx
@@ -0,0 +1,23 @@
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+
+export type TextareaProps = React.TextareaHTMLAttributes;
+
+const Textarea = React.forwardRef(
+ ({ className, ...props }, ref) => {
+ return (
+
+ );
+ },
+);
+Textarea.displayName = 'Textarea';
+
+export { Textarea };
diff --git a/src/features/editor/components/header/index.tsx b/src/features/editor/components/header/index.tsx
index 4da8c7b..8f4a6c0 100644
--- a/src/features/editor/components/header/index.tsx
+++ b/src/features/editor/components/header/index.tsx
@@ -3,29 +3,28 @@ import { useMemo } from 'react';
import { ActionButtons } from './action-buttons';
import { AutorunToggler } from './autorun-toggler';
+import { InfoEdit, InfoTitle } from './info';
import { MainMenu } from './main-menu';
import { OpenInSite } from './open-in-site';
import { SharingMenu } from './sharing-menu';
-import {
- BottomHeaderUntrustedModeSign,
- UntrustedModeSign,
-} from './untrusted-mode-sign';
+import { BottomHeaderUntrustedModeSign } from './untrusted-mode-sign';
export const Header = () => {
const isIframe = useMemo(() => window.self !== window.top, []);
return (
<>
-
-
+
+
-
-
+
+
+
-
+
{isIframe &&
}
{!isIframe &&
}
diff --git a/src/features/editor/components/header/info/index.tsx b/src/features/editor/components/header/info/index.tsx
new file mode 100644
index 0000000..9d4bb23
--- /dev/null
+++ b/src/features/editor/components/header/info/index.tsx
@@ -0,0 +1,67 @@
+import { InfoCircledIcon } from '@radix-ui/react-icons';
+import { useCallback, useMemo, useState } from 'react';
+
+import { Button } from '@/components/ui/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@/components/ui/dialog';
+import { useCodeStore } from '@/features/editor/stores/editor';
+
+import { InfoForm } from './info-form';
+
+interface InfoDialogProps {
+ children: React.ReactNode;
+ asChild?: boolean;
+}
+
+export const InfoDialog: React.FC
= ({
+ children,
+ asChild,
+}) => {
+ const [open, setOpen] = useState(false);
+
+ const closeDialog = useCallback(() => {
+ setOpen(false);
+ }, []);
+
+ return (
+
+ );
+};
+
+export const InfoTitle = () => {
+ const { title } = useCodeStore();
+
+ const titleTag = useMemo(() => (title ? title : 'No Title'), [title]);
+
+ return (
+
+
+ {titleTag}
+
+
+ );
+};
+
+export const InfoEdit = () => {
+ return (
+
+
+
+ );
+};
diff --git a/src/features/editor/components/header/info/info-form.tsx b/src/features/editor/components/header/info/info-form.tsx
new file mode 100644
index 0000000..242acd9
--- /dev/null
+++ b/src/features/editor/components/header/info/info-form.tsx
@@ -0,0 +1,85 @@
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useForm } from 'react-hook-form';
+import { toast } from 'sonner';
+import { z } from 'zod';
+
+import { Button } from '@/components/ui/button';
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@/components/ui/form';
+import { Input } from '@/components/ui/input';
+import { Textarea } from '@/components/ui/textarea';
+import { useUntrustedMode } from '@/features/editor/hooks/use-untrusted-mode';
+import { useCodeStore } from '@/features/editor/stores/editor';
+
+const FormSchema = z.object({
+ title: z.string().optional(),
+ description: z.string().optional(),
+});
+
+interface Props {
+ closeDialog: () => void;
+}
+
+export const InfoForm: React.FC = ({ closeDialog }) => {
+ const { title, description, setMetadata } = useCodeStore();
+ const { isUntrustedMode } = useUntrustedMode();
+ const form = useForm>({
+ resolver: zodResolver(FormSchema),
+ defaultValues: {
+ title,
+ description,
+ },
+ });
+
+ async function onSubmit(data: z.infer) {
+ setMetadata(data);
+
+ toast('Script details changed properly!');
+
+ closeDialog();
+ }
+ return (
+
+
+ );
+};
diff --git a/src/features/editor/components/header/untrusted-mode-sign.tsx b/src/features/editor/components/header/untrusted-mode-sign.tsx
index 81a8899..0cb7e40 100644
--- a/src/features/editor/components/header/untrusted-mode-sign.tsx
+++ b/src/features/editor/components/header/untrusted-mode-sign.tsx
@@ -10,10 +10,10 @@ export const UntrustedModeSign = () => {
setUntrustedDialogOpen(true)}
>
- Editor in Untrusted Mode
+ Untrusted Mode
);
@@ -24,7 +24,7 @@ export const BottomHeaderUntrustedModeSign = () => {
if (!isUntrustedMode) return null;
return (
-
+
);
diff --git a/src/features/editor/providers/index.tsx b/src/features/editor/providers/index.tsx
index 36b6d71..f9e29c4 100644
--- a/src/features/editor/providers/index.tsx
+++ b/src/features/editor/providers/index.tsx
@@ -3,6 +3,7 @@ import React from 'react';
import { EditorBehaviorProvider } from './editor-behavior';
import { FirstTimeDialogProvider } from './first-time-dialog-provider';
import { HelpProvider } from './help-provider';
+import { MetatagsProvider } from './meta-provider';
import { SettingsDialogProvider } from './settings-dialog-provider';
import { UntrustedModeProvider } from './untrusted-mode-provider';
@@ -14,13 +15,15 @@ const EditorProviders: React.FC
= ({ children }) => {
return (
-
-
-
- <>{children}>
-
-
-
+
+
+
+
+ <>{children}>
+
+
+
+
);
diff --git a/src/features/editor/providers/meta-provider.tsx b/src/features/editor/providers/meta-provider.tsx
new file mode 100644
index 0000000..8c5d6af
--- /dev/null
+++ b/src/features/editor/providers/meta-provider.tsx
@@ -0,0 +1,20 @@
+import { Helmet } from 'react-helmet-async';
+
+import { useCodeStore } from '../stores/editor';
+
+interface Props {
+ children: React.ReactNode;
+}
+
+export const MetatagsProvider: React.FC = ({ children }) => {
+ const { title } = useCodeStore();
+
+ return (
+ <>
+
+ JSoD - {title ? title : 'Script'}
+
+ {children}
+ >
+ );
+};
diff --git a/src/features/editor/stores/editor/code-store.ts b/src/features/editor/stores/editor/code-store.ts
index fff393b..f03f2be 100644
--- a/src/features/editor/stores/editor/code-store.ts
+++ b/src/features/editor/stores/editor/code-store.ts
@@ -5,16 +5,29 @@ import { createSelectors } from '@/lib/utils';
import { queryStorage } from '../../api/query-storage';
-interface CodeState {
+interface Metadata {
+ title?: string;
+ description?: string;
+}
+
+interface CodeState extends Metadata {
code: string;
setCode: (code: string) => void;
+ setMetadata: (value: Metadata) => void;
}
const _useCodeStore = create()(
persist(
(set) => ({
+ title: undefined,
+ description: undefined,
code: '',
setCode: (code) => set({ code: code }),
+ setMetadata: (meta) =>
+ set({
+ title: meta?.title,
+ description: meta?.description,
+ }),
}),
{
name: 'code',
diff --git a/src/features/not-found/index.tsx b/src/features/not-found/index.tsx
index 438392b..f807181 100644
--- a/src/features/not-found/index.tsx
+++ b/src/features/not-found/index.tsx
@@ -1,16 +1,22 @@
+import { Helmet } from 'react-helmet-async';
import { Link } from 'wouter';
import { Button } from '@/components/ui/button';
export default function NotFound() {
return (
-
-
- Are you searching for a JS editor?
-
-
-
+ <>
+
+ JSoD - Not Found
+
+
+
+ Are you searching for a JS editor?
+
+
+
+ >
);
}