Skip to content

Commit

Permalink
Add new tools and refactor existing ones
Browse files Browse the repository at this point in the history
  • Loading branch information
255kb committed Feb 9, 2024
1 parent eae424f commit eaaa159
Show file tree
Hide file tree
Showing 29 changed files with 1,125 additions and 471 deletions.
121 changes: 121 additions & 0 deletions components/editors/base-editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { lintGutter } from '@codemirror/lint';
import { nordInit } from '@uiw/codemirror-theme-nord';
import CodeMirror, {
EditorSelection,
EditorView,
Extension,
ReactCodeMirrorRef,
ViewUpdate
} from '@uiw/react-codemirror';
import { FunctionComponent, useRef, useState } from 'react';

type parseError = {
message: string;
line?: number;
column?: number;
position?: number;
};

const BaseEditor: FunctionComponent<{
value: string;
lang: string;
showValidMsg?: boolean;
hideGoToLine?: boolean;
editorExtensions: Extension[];
onErrorChange?: (value: string, view?: ViewUpdate) => parseError;
onValueChange?: (value: string, view?: ViewUpdate) => void;
}> = function ({
value,
lang,
showValidMsg,
editorExtensions,
hideGoToLine,
onErrorChange,
onValueChange
}) {
const [error, setError] = useState<parseError>(null);
const editor = useRef<ReactCodeMirrorRef>();

const scrollDocToView = (error: parseError) => {
if (!editor?.current?.state?.doc) {
return;
}
const position =
error.position !== undefined
? error.position
: editor.current.view.state.doc.line(error.line + 1).from +
error.column;

editor.current.view?.dispatch({
selection: EditorSelection.single(position, position),
scrollIntoView: true
});
};

return (
<div className='d-flex flex-column code-editor-container'>
<CodeMirror
theme={nordInit({
settings: {
fontFamily:
'"Fira Code", Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace'
}
})}
extensions={[
EditorView.lineWrapping,
lintGutter(),
...editorExtensions
]}
basicSetup={{ lintKeymap: false }}
height={'100%'}
style={{ minHeight: '0' }}
className={'h-100'}
value={value}
lang={lang}
ref={editor}
onChange={(value, view) => {
if (onErrorChange) {
setError(onErrorChange(value, view));
}

if (onValueChange) {
onValueChange(value, view);
}
}}
></CodeMirror>
{error && (
<div className='bg-danger-subtle border-start border-danger border-4 p-4 mt-4 position-relative d-flex justify-content-between'>
<div
style={{
whiteSpace: 'pre-wrap',
fontFamily:
'"Fira Code", Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace'
}}
>
{error.message}
</div>
{!hideGoToLine && (
<div className='flex-shrink-0'>
<a
href='#'
onClick={(a) => {
a.preventDefault();
scrollDocToView(error);
}}
>
Go to line
</a>
</div>
)}
</div>
)}
{!error && showValidMsg && (
<div className='bg-success-subtle border-start border-success border-4 p-4 my-4'>
<div>{lang.toUpperCase()} is valid!</div>
</div>
)}
</div>
);
};

export default BaseEditor;
43 changes: 43 additions & 0 deletions components/editors/base64-editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { FunctionComponent } from 'react';
import BaseEditor from './base-editor';

export function base64ToBytes(base64) {
const binString = atob(base64);
return Uint8Array.from(binString, (m) => m.codePointAt(0));
}

export function bytesToBase64(bytes) {
const binString = String.fromCodePoint(...bytes);
return btoa(binString);
}

const Base64Editor: FunctionComponent<{
value: string;
showValidMsg?: boolean;
onValueChange?: (value: string) => void;
}> = function (props) {
return (
<BaseEditor
lang='text'
hideGoToLine
editorExtensions={[]}
onErrorChange={(currentValue: string, viewUpdate) => {
try {
new TextDecoder().decode(base64ToBytes(currentValue));

return null;
} catch (error) {
return {
message: error.message.replace(
"Failed to execute 'atob' on 'Window': ",
''
)
};
}
}}
{...props}
/>
);
};

export default Base64Editor;
52 changes: 52 additions & 0 deletions components/editors/json-editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { json, jsonParseLinter } from '@codemirror/lang-json';
import { linter } from '@codemirror/lint';
import { Text } from '@uiw/react-codemirror';
import { FunctionComponent } from 'react';
import BaseEditor from './base-editor';

const jsonLinter = linter(jsonParseLinter(), { delay: 100 });

function getErrorPosition(
error: SyntaxError,
doc: Text
): { line?: number; column?: number; position?: number } {
let match;
if ((match = error.message.match(/at position (\d+)/)))
return { position: Math.min(+match[1], doc.length) };
if ((match = error.message.match(/at line (\d+) column (\d+)/)))
return {
position: Math.min(doc.line(+match[1]).from + +match[2] - 1, doc.length)
};

return {
position: 1
};
}

const JsonEditor: FunctionComponent<{
value: string;
showValidMsg?: boolean;
onValueChange?: (value: string) => void;
}> = function (props) {
return (
<BaseEditor
lang='json'
editorExtensions={[json(), jsonLinter]}
onErrorChange={(currentValue: string, viewUpdate) => {
try {
JSON.parse(currentValue);

return null;
} catch (error) {
return {
message: error.message,
...getErrorPosition(error, viewUpdate.state.doc)
};
}
}}
{...props}
/>
);
};

export default JsonEditor;
12 changes: 12 additions & 0 deletions components/editors/text-editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { FunctionComponent } from 'react';
import BaseEditor from './base-editor';

const TextEditor: FunctionComponent<{
value: string;
showValidMsg?: boolean;
onValueChange?: (value: string) => void;
}> = function (props) {
return <BaseEditor lang='text' editorExtensions={[]} {...props} />;
};

export default TextEditor;
78 changes: 78 additions & 0 deletions components/editors/xml-editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { xml } from '@codemirror/lang-xml';
import { linter } from '@codemirror/lint';
import { Text } from '@uiw/react-codemirror';
import { XMLValidator } from 'fast-xml-parser';
import { FunctionComponent } from 'react';
import BaseEditor from './base-editor';

const xmlLinter = linter(
(view) => {
const validation = XMLValidator.validate(view.state.doc.toString());

if (validation !== true && validation.err) {
const startChar =
view.state.doc.line(validation.err.line).from + validation.err.col;

return [
{
from: startChar,
to: startChar,
severity: 'error',
message: validation.err.msg
}
];
} else {
return [];
}
},
{ delay: 100 }
);

function getErrorPosition(
error: SyntaxError,
doc: Text
): { line?: number; column?: number; position?: number } {
let match;
if ((match = error.message.match(/at position (\d+)/)))
return { position: Math.min(+match[1], doc.length) };
if ((match = error.message.match(/at line (\d+) column (\d+)/)))
return {
position: Math.min(doc.line(+match[1]).from + +match[2] - 1, doc.length)
};

return {
position: 1
};
}

const XmlEditor: FunctionComponent<{
value: string;
showValidMsg?: boolean;
onValueChange?: (value: string) => void;
}> = function (props) {
return (
<BaseEditor
lang='xml'
editorExtensions={[xml(), xmlLinter]}
onErrorChange={(currentValue: string, viewUpdate) => {
const validation = XMLValidator.validate(currentValue);

if (validation !== true && validation.err) {
const position =
viewUpdate.state.doc.line(validation.err.line).from +
validation.err.col;

return {
message: validation.err.msg,
position
};
}

return null;
}}
{...props}
/>
);
};

export default XmlEditor;
67 changes: 67 additions & 0 deletions components/editors/yaml-editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { yaml } from '@codemirror/lang-yaml';
import { linter } from '@codemirror/lint';
import jsyaml from 'js-yaml';
import { FunctionComponent } from 'react';
import BaseEditor from './base-editor';

const yamlLinter = linter(
(view) => {
try {
jsyaml.load(view.state.doc.toString());

return [];
} catch (error) {
// errors are 0-based, but codemirror is 1-based
error.mark.line = error.mark.line + 1;

let startChar: number;

// some errors are reported on a final extra line. If this is the case, we put the cursor at the end of the previous line
if (view.state.doc.lines < error.mark.line) {
startChar = view.state.doc.line(error.mark.line - 1).to;
} else {
startChar =
view.state.doc.line(error.mark.line).from + error.mark.column;
}

return [
{
from: startChar,
to: startChar,
severity: 'error',
message: error.reason
}
];
}
},
{ delay: 100 }
);

const YamlEditor: FunctionComponent<{
value: string;
showValidMsg?: boolean;
onValueChange?: (value: string) => void;
}> = function (props) {
return (
<BaseEditor
lang='yaml'
editorExtensions={[yaml(), yamlLinter]}
onErrorChange={(currentValue: string) => {
try {
jsyaml.load(currentValue);

return null;
} catch (error) {
return {
message: error.message,
line: error.mark.line,
column: error.mark.column
};
}
}}
{...props}
/>
);
};

export default YamlEditor;
Loading

0 comments on commit eaaa159

Please sign in to comment.