2023-06-05 19:55:56 +02:00
|
|
|
import { Diagnostic, setDiagnostics } from '@codemirror/lint';
|
2023-06-02 15:24:32 +02:00
|
|
|
import { Text } from '@codemirror/state';
|
|
|
|
import { EditorView } from 'codemirror';
|
|
|
|
|
2023-06-06 22:02:26 +02:00
|
|
|
import { compileString, OutputStyle, Syntax } from '../vendor/playground';
|
2023-06-02 16:53:32 +02:00
|
|
|
import { editorSetup, outputSetup } from './editor-setup.js';
|
2023-06-02 15:24:32 +02:00
|
|
|
|
2023-06-06 22:02:26 +02:00
|
|
|
type PlaygroundState = {
|
|
|
|
inputFormat: Syntax;
|
|
|
|
outputFormat: OutputStyle;
|
|
|
|
inputValue: string;
|
|
|
|
compilerHasError: boolean;
|
|
|
|
};
|
|
|
|
|
2023-06-06 17:33:41 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
|
|
function debounce(func: Function, timeout = 200) {
|
|
|
|
let timer: number;
|
|
|
|
return function (this: unknown, ...args: unknown[]) {
|
|
|
|
clearTimeout(timer);
|
|
|
|
// Call window.setTimeout, as this is run in the browser, and not in the NodeJS context as the rest of the project
|
|
|
|
timer = window.setTimeout(() => {
|
|
|
|
func.apply(this, args);
|
|
|
|
}, timeout);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-06-05 15:25:55 +02:00
|
|
|
function setupPlayground() {
|
2023-06-06 22:02:26 +02:00
|
|
|
const playgroundState: PlaygroundState = {
|
|
|
|
inputFormat: 'scss',
|
|
|
|
outputFormat: 'expanded',
|
|
|
|
compilerHasError: false,
|
|
|
|
inputValue: '',
|
|
|
|
};
|
2023-06-05 15:25:55 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
|
|
const editor = new EditorView({
|
|
|
|
extensions: [
|
2023-06-05 18:02:44 +02:00
|
|
|
...editorSetup,
|
2023-06-05 15:25:55 +02:00
|
|
|
EditorView.updateListener.of((v) => {
|
|
|
|
if (v.docChanged) {
|
2023-06-06 22:02:26 +02:00
|
|
|
playgroundState.inputValue = editor.state.doc.toString();
|
2023-06-06 17:33:41 +02:00
|
|
|
debouncedUpdateCSS();
|
2023-06-05 15:25:55 +02:00
|
|
|
}
|
|
|
|
}),
|
|
|
|
],
|
|
|
|
parent: document.getElementById('editor') || document.body,
|
|
|
|
});
|
2023-06-02 18:52:24 +02:00
|
|
|
|
2023-06-05 15:25:55 +02:00
|
|
|
// Setup CSS view
|
|
|
|
const viewer = new EditorView({
|
2023-06-05 18:02:44 +02:00
|
|
|
extensions: [...outputSetup],
|
2023-06-05 15:25:55 +02:00
|
|
|
parent: document.getElementById('css-view') || document.body,
|
|
|
|
});
|
2023-06-02 15:24:32 +02:00
|
|
|
|
2023-06-05 15:25:55 +02:00
|
|
|
type TabbarItemDataset = {
|
|
|
|
value: string;
|
|
|
|
setting: string;
|
|
|
|
};
|
|
|
|
function attachListeners() {
|
|
|
|
function clickHandler(event) {
|
|
|
|
const settings = event.currentTarget.dataset as TabbarItemDataset;
|
2023-06-02 18:52:24 +02:00
|
|
|
|
2023-06-06 22:02:26 +02:00
|
|
|
playgroundState[settings.setting] = settings.value;
|
|
|
|
updateButtonState();
|
|
|
|
debouncedUpdateCSS();
|
2023-06-02 18:52:24 +02:00
|
|
|
}
|
2023-06-06 22:02:26 +02:00
|
|
|
const options = document.querySelectorAll('[data-value]');
|
2023-06-05 15:25:55 +02:00
|
|
|
Array.from(options).forEach((option) => {
|
|
|
|
option.addEventListener('click', clickHandler);
|
|
|
|
});
|
2023-06-02 18:52:24 +02:00
|
|
|
}
|
|
|
|
|
2023-06-06 22:02:26 +02:00
|
|
|
function updateButtonState() {
|
|
|
|
const inputFormatTab = document.querySelector(
|
|
|
|
'[data-setting="inputFormat"]',
|
|
|
|
) as HTMLDivElement;
|
|
|
|
inputFormatTab.dataset.active = playgroundState.inputFormat;
|
|
|
|
|
|
|
|
const outputFormatTab = document.querySelector(
|
|
|
|
'[data-setting="outputFormat"]',
|
|
|
|
) as HTMLDivElement;
|
|
|
|
outputFormatTab.dataset.active = playgroundState.outputFormat;
|
|
|
|
}
|
|
|
|
function updateErrorState() {
|
|
|
|
const editorWrapper = document.querySelector(
|
|
|
|
'[data-compiler-has-error]',
|
|
|
|
) as HTMLDivElement;
|
|
|
|
editorWrapper.dataset.compilerHasError =
|
|
|
|
playgroundState.compilerHasError.toString();
|
|
|
|
}
|
|
|
|
|
2023-06-05 15:25:55 +02:00
|
|
|
function updateCSS() {
|
2023-06-06 22:02:26 +02:00
|
|
|
const result = parse(playgroundState.inputValue);
|
2023-06-05 20:40:15 +02:00
|
|
|
if ('css' in result) {
|
2023-06-05 19:55:56 +02:00
|
|
|
const text = Text.of(result.css.split('\n'));
|
|
|
|
viewer.dispatch({
|
|
|
|
changes: {
|
|
|
|
from: 0,
|
|
|
|
to: viewer.state.doc.toString().length,
|
|
|
|
insert: text,
|
|
|
|
},
|
|
|
|
});
|
2023-06-05 20:40:15 +02:00
|
|
|
editor.dispatch(setDiagnostics(editor.state, []));
|
2023-06-06 22:02:26 +02:00
|
|
|
playgroundState.compilerHasError = false;
|
2023-06-05 19:55:56 +02:00
|
|
|
} else {
|
|
|
|
const diagnostic = errorToDiagnostic(result.error);
|
|
|
|
const transaction = setDiagnostics(editor.state, [diagnostic]);
|
|
|
|
editor.dispatch(transaction);
|
2023-06-06 22:02:26 +02:00
|
|
|
playgroundState.compilerHasError = true;
|
2023-06-05 19:55:56 +02:00
|
|
|
}
|
2023-06-06 22:02:26 +02:00
|
|
|
updateErrorState();
|
2023-06-05 15:25:55 +02:00
|
|
|
}
|
2023-06-06 19:47:43 +02:00
|
|
|
const debouncedUpdateCSS = debounce(updateCSS);
|
2023-06-05 20:40:15 +02:00
|
|
|
|
2023-06-05 19:55:56 +02:00
|
|
|
type ParseResultSuccess = { css: string };
|
2023-06-06 22:02:26 +02:00
|
|
|
type ParseResultError = { error: unknown };
|
2023-06-05 19:55:56 +02:00
|
|
|
type ParseResult = ParseResultSuccess | ParseResultError;
|
|
|
|
|
|
|
|
function parse(css: string): ParseResult {
|
2023-06-05 15:25:55 +02:00
|
|
|
try {
|
2023-06-05 19:55:56 +02:00
|
|
|
const result = compileString(css, {
|
2023-06-06 22:02:26 +02:00
|
|
|
syntax: playgroundState.inputFormat,
|
|
|
|
style: playgroundState.outputFormat,
|
2023-06-05 19:55:56 +02:00
|
|
|
});
|
|
|
|
return { css: result.css };
|
2023-06-05 15:25:55 +02:00
|
|
|
} catch (error) {
|
2023-06-05 19:55:56 +02:00
|
|
|
return { error };
|
2023-06-05 15:25:55 +02:00
|
|
|
}
|
2023-06-05 19:55:56 +02:00
|
|
|
}
|
2023-06-05 15:25:55 +02:00
|
|
|
|
2023-06-06 22:02:26 +02:00
|
|
|
function errorToDiagnostic(error: unknown): Diagnostic {
|
2023-06-05 19:55:56 +02:00
|
|
|
return {
|
|
|
|
from: error.span.start.offset,
|
|
|
|
to: error.span.end.offset,
|
|
|
|
severity: 'error',
|
|
|
|
message: error?.toString() || 'Compilation error',
|
|
|
|
};
|
2023-06-02 15:24:32 +02:00
|
|
|
}
|
|
|
|
|
2023-06-05 15:25:55 +02:00
|
|
|
attachListeners();
|
|
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
|
|
document.addEventListener('DOMContentLoaded', setupPlayground);
|
|
|
|
} else {
|
|
|
|
setupPlayground();
|
2023-06-02 15:24:32 +02:00
|
|
|
}
|