Skip to content

Commit

Permalink
Add JSON syntax highlighting and schema validator (#652)
Browse files Browse the repository at this point in the history
Co-authored-by: Wojciech Kozyra <[email protected]>
  • Loading branch information
wkazmierczak and wkozyra95 authored Aug 13, 2024
1 parent a2667c0 commit 63133c7
Show file tree
Hide file tree
Showing 14 changed files with 693 additions and 349 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Schemas generated for TypeScript demos
/schemas/api_types.schema.json
/docs/component_types.schema.json

# Generated by Cargo
# will have compiled files and executables
Expand Down
667 changes: 376 additions & 291 deletions docs/package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@
"@docusaurus/plugin-client-redirects": "3.1.1",
"@docusaurus/preset-classic": "^3.1.1",
"@mdx-js/react": "^3.0.0",
"@types/jsoneditor": "^9.9.5",
"ajv": "^6.12.6",
"clsx": "^1.2.1",
"jsoneditor": "^10.1.0",
"prism-react-renderer": "^2.1.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-hot-toast": "^2.4.1",
"react-icons": "^4.12.0",
"react-tooltip": "^5.28.0",
"typewriter-effect": "^2.21.0"
},
"devDependencies": {
Expand All @@ -39,6 +43,7 @@
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-prettier": "^5.0.1",
"jsonrepair": "^3.8.0",
"prettier": "^3.1.0",
"typescript": "~5.2.2"
},
Expand Down
39 changes: 27 additions & 12 deletions docs/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ interface RequestObject {
body: string;
}

export class ApiError extends Error {
response: Response;
body: string | object;
constructor(message: string, response?: Response, body?: string | object) {
super(message);
this.response = response;
this.body = body;
}
}

function buildRequestRenderImage(body: object): RequestObject {
return {
method: 'POST',
Expand All @@ -18,33 +28,38 @@ function buildRequestRenderImage(body: object): RequestObject {
};
}

async function getErrorDescription(response: Response): Promise<string> {
async function createError(response: Response): Promise<ApiError> {
const contentType = response.headers.get('Content-Type');
const errorStatus = `Error status: ${response.status}`;
const errorStatus = `Error status: ${response.status} ${response.statusText}`;
let errorMessage = '';

let body;
if (contentType === 'application/json') {
const apiError = await response.json();
body = apiError;
if (apiError.stack) {
errorMessage = `Error message: ${apiError.stack.map(String).join('\n')}`;
errorMessage = apiError.stack.map(String).join('\n');
} else {
errorMessage = `Error message: ${apiError.message}`;
errorMessage = apiError.message;
}
} else {
const txt = await response.text();
errorMessage = `Error message: ${txt}`;
errorMessage = await response.text();
body = errorMessage;
}
return `${errorStatus};\n${errorMessage}`;
console.log(`${errorStatus};\nError message: ${errorMessage}`);
return new ApiError(errorMessage, response, body);
}

export async function renderImage(body: object): Promise<Blob> {
const requestObject = buildRequestRenderImage(body);
const renderImageUrl = new URL('/render_image', BACKEND_URL);
const response = await fetch(renderImageUrl, requestObject);

let response;
try {
response = await fetch(renderImageUrl, requestObject);
} catch (error) {
throw new ApiError(error.message);
}
if (response.status >= 400) {
const errorDescription = await getErrorDescription(response);
throw new Error(errorDescription);
throw await createError(response);
}
return await response.blob();
}
2 changes: 1 addition & 1 deletion docs/src/components/PlaygroundCodeEditor.module.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.codeEditor{
.codeEditor {
height: 100%;
width: 100%;
min-width: 300px;
Expand Down
107 changes: 83 additions & 24 deletions docs/src/components/PlaygroundCodeEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,92 @@
import styles from './PlaygroundCodeEditor.module.css';
import { ChangeEvent, useState } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import 'jsoneditor/dist/jsoneditor.css';
import './jsoneditor-dark.css';
import componentTypesJsonSchema from '../../component_types.schema.json';
import JSONEditor from '../jsonEditor';
import { jsonrepair } from 'jsonrepair';
import Ajv from 'ajv';

interface PlaygroundCodeEditorProps {
onChange: (content: object | Error) => void;
initialCodeEditorContent: string;
initialCodeEditorContent: object;
}

function ajvInitialization(): Ajv.Ajv {
const ajv = new Ajv({
allErrors: true,
verbose: true,
schemaId: 'auto',
$data: true,
});

ajv.addFormat('float', '^-?d+(.d+)?([eE][+-]?d+)?$');
ajv.addFormat('double', '^-?d+(.d+)?([eE][+-]?d+)?$');
ajv.addFormat('int32', '^-?d+$');
ajv.addFormat('uint32', '^d+$');
ajv.addFormat('uint', '^d+$');

return ajv;
}

function PlaygroundCodeEditor({ onChange, initialCodeEditorContent }: PlaygroundCodeEditorProps) {
const [content, setContent] = useState<string>(initialCodeEditorContent);

const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
const codeContent = event.target.value;
setContent(codeContent);
try {
const scene = JSON.parse(codeContent);
onChange(scene);
} catch (error) {
onChange(error);
const [jsonEditor, setJsonEditor] = useState<JSONEditor | null>(null);

const editorContainer = useCallback((node: HTMLElement) => {
if (node === null) {
return;
}
};

return (
<textarea
className={styles.codeEditor}
name="inputArea"
placeholder="Enter your code to try it out"
value={content}
onChange={handleChange}
/>
);
const ajv = ajvInitialization();
const validate = ajv.compile(componentTypesJsonSchema);

const editor = new JSONEditor(node, {
mode: 'code',
enableSort: false,
enableTransform: false,
statusBar: false,
mainMenuBar: false,
ajv,
onChange: () => {
try {
const jsonContent = editor.get();
onChange(jsonContent);
if (!validate(jsonContent)) {
throw new Error('Invalid JSON!');
}
} catch (error) {
onChange(error);
}
},
onBlur: () => {
try {
const repaired = jsonrepair(editor.getText());
const formated = JSON.stringify(JSON.parse(repaired), null, 2);
editor.updateText(formated);
const jsonContent = editor.get();
onChange(jsonContent);
if (!validate(jsonContent)) {
throw new Error('Invalid JSON!');
}
} catch (error) {
onChange(error);
}
},
});

editor.setSchema(componentTypesJsonSchema);
editor.set(initialCodeEditorContent);

setJsonEditor(editor);
}, []);

useEffect(() => {
return () => {
if (jsonEditor) {
jsonEditor.destroy();
}
};
}, [jsonEditor]);

return <div ref={editorContainer} style={{ height: '100%' }} />;
}

export default PlaygroundCodeEditor;
14 changes: 10 additions & 4 deletions docs/src/components/PlaygroundRenderSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
function PlaygroundRenderSettings({ onSubmit }: { onSubmit: () => Promise<void> }) {
import SubmitButton from './SubmitButton';

function PlaygroundRenderSettings({
onSubmit,
readyToSubmit,
}: {
onSubmit: () => Promise<void>;
readyToSubmit: boolean;
}) {
return (
<div style={{ margin: '10px' }}>
<div className="row">
Expand All @@ -22,9 +30,7 @@ function PlaygroundRenderSettings({ onSubmit }: { onSubmit: () => Promise<void>
</select>
</div>
<div className="col">
<button className="button button--outline button--primary" onClick={onSubmit}>
Submit
</button>
<SubmitButton onSubmit={onSubmit} readyToSubmit={readyToSubmit} />
</div>
</div>
</div>
Expand Down
28 changes: 28 additions & 0 deletions docs/src/components/SubmitButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Tooltip } from 'react-tooltip';

function SubmitButton({
onSubmit,
readyToSubmit,
}: {
onSubmit: () => Promise<void>;
readyToSubmit: boolean;
}) {
return (
<div
data-tooltip-id={readyToSubmit ? null : 'disableSubmit'}
data-tooltip-content={readyToSubmit ? null : 'Invalid scene provided!'}
data-tooltip-place={readyToSubmit ? null : 'top'}>
<button
className={`button ${
readyToSubmit ? 'button--outline button--primary' : 'disabled button--secondary'
}`}
style={readyToSubmit ? {} : { color: '#f5f5f5', backgroundColor: '#dbdbdb' }}
onClick={onSubmit}>
Submit
</button>
<Tooltip id="disableSubmit" />
</div>
);
}

export default SubmitButton;
119 changes: 119 additions & 0 deletions docs/src/components/jsoneditor-dark.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
.jsoneditor {
color: none;
}
div.jsoneditor {
border-radius: 3px;
}
div.jsoneditor,
div.jsoneditor-menu {
border-color: #4b4b4b;
color: #fcf5f5;
}
div.jsoneditor-menu {
background-color: #4b4b4b;
}

[data-theme='dark'] .ace_editor {
font-size: 16px;
color: rgb(108, 103, 131);
}

[data-theme='light'] .ace_editor {
font-size: 16px;
color: rgb(0, 0, 0);
}

[data-theme='dark'] .ace_editor .ace_gutter {
background: rgb(42, 39, 52);
color: rgb(108, 103, 131);
}

[data-theme='light'] .ace_editor .ace_gutter {
background: rgb(255, 255, 255);
color: rgb(97, 97, 97);
}
[data-theme='dark'] .ace_content {
background-color: rgb(42, 39, 52);
}

[data-theme='light'] .ace_content {
background-color: rgb(255, 255, 255);
}

[data-theme='dark'] .ace_content .ace_variable {
color: #9a86fd;
}
[data-theme='light'] .ace_content .ace_variable {
color: #6f54fa;
}

[data-theme='dark'] .ace_line {
color: rgb(108, 103, 131);
}
[data-theme='light'] .ace_line {
color: rgb(97, 97, 97);
}

[data-theme='dark'] .ace_editor .ace_content .ace_string {
color: rgb(255, 204, 153);
}

[data-theme='light'] .ace_editor .ace_content .ace_string {
color: rgb(255, 153, 51);
}

[data-theme='dark'] .ace_editor .ace_content .ace_bool,
.ace_editor .ace_content .ace_numeric {
color: rgb(224, 145, 66);
}

[data-theme='light'] .ace_editor .ace_content .ace_bool,
.ace_editor .ace_content .ace_numeric {
color: rgb(224, 112, 0);
}

[data-theme='dark'] .ace-jsoneditor .ace_marker-layer .ace_active-line {
color: rgb(108, 103, 131);
background-color: rgb(62, 58, 76);
}

[data-theme='light'] .ace-jsoneditor .ace_marker-layer .ace_active-line {
color: rgb(108, 103, 131);
background-color: rgb(235, 235, 235);
}

[data-theme='dark'] .ace-jsoneditor .ace_gutter-active-line {
color: rgb(108, 103, 131);
background-color: rgb(62, 58, 76);
}
[data-theme='light'] .ace-jsoneditor .ace_gutter-active-line {
color: rgb(108, 103, 131);
background-color: rgb(235, 235, 235);
}

[data-theme='dark'] .ace_content .ace_cursor {
border-color: #949494;
}

[data-theme='light'] .ace_content .ace_cursor {
border-color: #000000;
}

[data-theme='dark'] .ace_editor .ace_marker-layer .ace_selection {
background: rgb(98, 94, 113);
}

[data-theme='light'] .ace_editor .ace_marker-layer .ace_selection {
background: rgb(222, 222, 222);
}
.ace_editor .ace_ .ace_editor .ace_marker-layer .ace_selected-word {
border: none;
}

.ace-jsoneditor .ace_indent-guide {
background: none;
}

.ace-jsoneditor .ace_marker-layer .ace_selected-word {
border: none;
}
Loading

0 comments on commit 63133c7

Please sign in to comment.