Skip to content

Commit

Permalink
Merge pull request #76 from raxhvl/feat/playground
Browse files Browse the repository at this point in the history
feat(Playground)
  • Loading branch information
gnidan authored May 17, 2024
2 parents 2def043 + 157a315 commit 9896b3f
Show file tree
Hide file tree
Showing 6 changed files with 2,484 additions and 2,007 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,8 @@
"jest": "^29.7.0",
"lerna": "^8.0.2",
"nodemon": "^3.0.2"
},
"dependencies": {
"@apideck/better-ajv-errors": "^0.3.6"
}
}
4 changes: 4 additions & 0 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"typecheck": "tsc"
},
"dependencies": {
"@apideck/better-ajv-errors": "^0.3.6",
"@docusaurus/core": "^3.0.1",
"@docusaurus/preset-classic": "^3.0.1",
"@ethdebug/format": "^0.1.0-0",
Expand All @@ -23,6 +24,9 @@
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@mdx-js/react": "^3.0.0",
"@mischnic/json-sourcemap": "^0.1.1",
"@monaco-editor/react": "^4.6.0",
"ajv": "^8.12.0",
"clsx": "^1.2.1",
"docusaurus-json-schema-plugin": "^1.11.0",
"prism-react-renderer": "^2.1.0",
Expand Down
199 changes: 199 additions & 0 deletions packages/web/src/components/Playground.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { type SchemaById, describeSchema, schemas } from "@ethdebug/format";
import Editor, { useMonaco } from "@monaco-editor/react";
import { editor } from "monaco-editor";
import { useEffect, useRef, useState } from "react";
import { useColorMode } from "@docusaurus/theme-common";
import JSONSourceMap from "@mischnic/json-sourcemap";
import { betterAjvErrors } from "@apideck/better-ajv-errors";

// To use Ajv with the support of all JSON Schema draft-2019-09/2020-12
// features you need to use a different export:
// refer: https://github.com/ajv-validator/ajv/issues/2335
import Ajv, { ErrorObject } from "ajv/dist/2020";

export interface PlaygroundProps {
schema: SchemaById;
}

/* Source map types */
type SourceMap = { data: any; pointers: Record<string, Pointer> };

type Pointer = {
value: Position;
valueEnd: Position;
key?: Position;
keyEnd?: Position;
};

type Position = {
line: number;
column: number;
pos: number;
};

type ValidationError = {
message: string;
suggestion?: string;
path: string;
context: {
errorType: string;
allowedValue?: string;
};
};

/* The EthDebug Playground: An interactive component for developers */
/* and tinkerers to build and test EthDebug schemas. */
export default function Playground(props: PlaygroundProps): JSX.Element {
const { schema } = describeSchema(props);
const { colorMode } = useColorMode();

// Setting exampleSchema to the first example or an empty object
const exampleSchema = schema.examples?.[0] ?? {};

// Ref to hold the Monaco editor instance
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);

// Ref to hold the Monaco editor instance
const monaco = useMonaco();

// Tab width
const TAB_WIDTH = 2;

// Compile all schemas to Ajv instance
const ajv = new Ajv({
schemas: Object.values(schemas),
allErrors: true,
strict: false,
});

// State to hold editor input
const [editorInput, setEditorInput] = useState(exampleSchema);
const [ready, setReady] = useState(false);

// Validate schema on editor input change
useEffect(() => {
ready && validateSchema();
}, [editorInput]);

/**
* Handles editor did mount event
* @param {editor.IStandaloneCodeEditor} editor - Monaco editor instance
*/
function handleEditorDidMount(editor: editor.IStandaloneCodeEditor) {
editorRef.current = editor;
}

/**
* Validates the schema using Ajv and displays errors in the Monaco editor
*/
function validateSchema() {
const validate = ajv.getSchema(props.schema.id);
if (!validate) return showError("Unable to validate schema");
const sourceMap = getParsedEditorInput();
validate(sourceMap.data);
const betterErrors = betterAjvErrors({
//@ts-ignore
schema: schemas[props.schema.id],
data: sourceMap.data,
errors: validate.errors,
});
console.log(betterErrors, validate.errors);
showValidationErrors(betterErrors, sourceMap);
}

/**
* Shows validation error in the Monaco editor
* @param {ValidationError[]} errors - Validation errors if any
* @param {string} sourceMap - The source map of the editor input
*/
function showValidationErrors(
errors: ValidationError[],
sourceMap: SourceMap
) {
const model = editorRef.current?.getModel();
if (!model || !monaco) return showError("Unable to validate schema");
let markers = [];
if (errors) {
for (const [_, error] of Object.entries(errors)) {
let instancePath = error.path.replace("{base}", "").replace(/\./g, "/");
let node = sourceMap.pointers[instancePath];
let message = error.message.replace("{base}", "").replace(/\./g, "/");
if (error.context.errorType == "const") {
message = `Expecting a constant value of "${error.context.allowedValue}"`;
}

if (!node || !message) continue;

markers.push({
startLineNumber: node.value.line + 1,
startColumn: node.value.column + 1,
endColumn: node.valueEnd.column + 1,
endLineNumber: node.valueEnd.line + 1,
message,
severity: monaco.MarkerSeverity.Error,
});

if (node.key && node.keyEnd) {
markers.push({
startLineNumber: node.key.line + 1,
startColumn: node.key.column + 1,
endColumn: node.keyEnd.column + 1,
endLineNumber: node.keyEnd.line + 1,
message,
severity: monaco.MarkerSeverity.Error,
});
}
}
}
monaco.editor.setModelMarkers(model, "EthDebug", markers);
}

/**
* Parses the editor input into a JSON object
* @returns {Object} - Parsed JSON object
*/
function getParsedEditorInput(): SourceMap {
try {
return JSONSourceMap.parse(editorInput, undefined, {
tabWidth: TAB_WIDTH,
});
} catch {
return { data: "", pointers: {} };
}
}

/**
* Displays an error message in the console
* @param {string} error - The error message
*/
function showError(error: string) {
console.error(error);
}

/**
* Handles editor value change event
* @param {string | undefined} value - The new value of the editor
*/
function handleEditorChange(value: string | undefined) {
setReady(true);
setEditorInput(value);
}

return (
<section className="playground-container">
<Editor
height="50vh"
language="json"
theme={colorMode == "dark" ? "vs-dark" : "vs-light"}
defaultValue={JSON.stringify(exampleSchema, undefined, TAB_WIDTH)}
onChange={handleEditorChange}
onMount={handleEditorDidMount}
options={{
contextmenu: false,
autoIndent: "advanced",
tabSize: TAB_WIDTH,
}}
/>
</section>
);
}
4 changes: 4 additions & 0 deletions packages/web/src/components/SchemaViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from "@site/src/contexts/SchemaContext";
import ReactMarkdown from "react-markdown";
import SchemaListing from "./SchemaListing";
import Playground from "./Playground";

export interface SchemaViewerProps extends DescribeSchemaOptions {
}
Expand Down Expand Up @@ -85,6 +86,9 @@ export default function SchemaViewer(props: SchemaViewerProps): JSX.Element {
<TabItem value="listing" label="View source">
<SchemaListing schema={props.schema} pointer={props.pointer} />
</TabItem>
<TabItem value="playground" label="Playground">
<Playground schema={props.schema} pointer={props.pointer} />
</TabItem>
</Tabs>
);
}
Expand Down
39 changes: 25 additions & 14 deletions packages/web/src/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,25 @@
/* NOTE: config here is just a placeholder (suggestions by GPT-4) */

:root {
--ifm-color-primary: #1A7F7E; /* Deep Blue-Green */
--ifm-color-primary-dark: #146B6C; /* Darker Shade for Hover States */
--ifm-color-primary: #1a7f7e; /* Deep Blue-Green */
--ifm-color-primary-dark: #146b6c; /* Darker Shade for Hover States */
--ifm-color-primary-darker: #105957; /* Even Darker for Deep Contrast */
--ifm-color-primary-darkest: #0B4748; /* Almost Black Blue-Green */
--ifm-color-primary-light: #33A6A6; /* Lighter Shade for Active States */
--ifm-color-primary-lighter: #66BFBF; /* Even Lighter for Variety */
--ifm-color-primary-lightest: #99D8D8; /* Very Light for Subtle Backgrounds */
--ifm-color-primary-darkest: #0b4748; /* Almost Black Blue-Green */
--ifm-color-primary-light: #33a6a6; /* Lighter Shade for Active States */
--ifm-color-primary-lighter: #66bfbf; /* Even Lighter for Variety */
--ifm-color-primary-lightest: #99d8d8; /* Very Light for Subtle Backgrounds */
--ifm-code-font-size: 95%;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
}

[data-theme='dark'] {
--ifm-color-primary: #25C2A0; /* Lighter Blue-Green for Dark Mode */
--ifm-color-primary-dark: #21AF90; /* Slightly Darker for Contrast */
--ifm-color-primary-darker: #1FA588; /* Further Darkened for Depth */
--ifm-color-primary-darkest: #1A8870; /* Rich Dark Green-Blue */
--ifm-color-primary-light: #29D5B0; /* Brighter for Highlights */
--ifm-color-primary-lighter: #32D8B4; /* Softer Shade for Balance */
--ifm-color-primary-lightest: #4FDDC1; /* Almost Pastel for Light Backgrounds */
[data-theme="dark"] {
--ifm-color-primary: #25c2a0; /* Lighter Blue-Green for Dark Mode */
--ifm-color-primary-dark: #21af90; /* Slightly Darker for Contrast */
--ifm-color-primary-darker: #1fa588; /* Further Darkened for Depth */
--ifm-color-primary-darkest: #1a8870; /* Rich Dark Green-Blue */
--ifm-color-primary-light: #29d5b0; /* Brighter for Highlights */
--ifm-color-primary-lighter: #32d8b4; /* Softer Shade for Balance */
--ifm-color-primary-lightest: #4fddc1; /* Almost Pastel for Light Backgrounds */
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
}

Expand All @@ -38,3 +38,14 @@
font-weight: bold;
text-decoration: underline;
}

/* Styling for Playground */
.playground-container {
border-radius: 10px;
padding: 15px;
background: rgb(238, 249, 253);
}

[data-theme="dark"] .playground-container {
background: rgb(25, 60, 71);
}
Loading

0 comments on commit 9896b3f

Please sign in to comment.