Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JSON syntax highlighting and schema validator #652

Merged
merged 7 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add @types/jsoneditor package to have type support

mode: 'code',
enableSort: false,
enableTransform: false,
statusBar: false,
mainMenuBar: false,
ajv,
onChange: () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add format when changing focus

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