2023-06-09 18:59:45 +02:00
|
|
|
import { setDiagnostics } from '@codemirror/lint';
|
2023-06-02 15:24:32 +02:00
|
|
|
import { Text } from '@codemirror/state';
|
|
|
|
import { EditorView } from 'codemirror';
|
2023-06-10 18:31:43 +02:00
|
|
|
import debounce from 'lodash/debounce';
|
2023-06-21 16:33:05 +02:00
|
|
|
import { compileString, info, Logger, OutputStyle, Syntax } from 'sass';
|
2023-06-02 15:24:32 +02:00
|
|
|
|
2023-06-09 18:59:45 +02:00
|
|
|
import { displayForConsoleLog } from './playground/console-utils.js';
|
2023-06-09 18:22:57 +02:00
|
|
|
import { editorSetup, outputSetup } from './playground/editor-setup.js';
|
2023-06-09 18:59:45 +02:00
|
|
|
import {
|
|
|
|
base64ToState,
|
|
|
|
errorToDiagnostic,
|
|
|
|
ParseResult,
|
|
|
|
PlaygroundState,
|
|
|
|
stateToBase64,
|
|
|
|
} from './playground/utils.js';
|
2023-06-06 17:33:41 +02:00
|
|
|
|
2023-06-05 15:25:55 +02:00
|
|
|
function setupPlayground() {
|
2023-06-09 18:24:33 +02:00
|
|
|
const hashState = base64ToState(location.hash);
|
|
|
|
|
2023-06-07 15:46:39 +02:00
|
|
|
const initialState: PlaygroundState = {
|
2023-06-09 18:24:33 +02:00
|
|
|
inputFormat: hashState.inputFormat || 'scss',
|
|
|
|
outputFormat: hashState.outputFormat || 'expanded',
|
2023-06-06 22:02:26 +02:00
|
|
|
compilerHasError: false,
|
2023-06-09 18:24:33 +02:00
|
|
|
inputValue: hashState.inputValue || '',
|
2023-06-07 21:13:23 +02:00
|
|
|
debugOutput: [],
|
2023-06-06 22:02:26 +02:00
|
|
|
};
|
2023-06-07 15:46:39 +02:00
|
|
|
|
2023-06-09 18:59:45 +02:00
|
|
|
// Proxy intercepts setters and triggers side effects
|
2023-06-07 15:46:39 +02:00
|
|
|
const playgroundState = new Proxy(initialState, {
|
|
|
|
set(state: PlaygroundState, prop: keyof PlaygroundState, ...rest) {
|
|
|
|
// Set state first so called functions have access
|
|
|
|
const set = Reflect.set(state, prop, ...rest);
|
|
|
|
if (['inputFormat', 'outputFormat'].includes(prop)) {
|
|
|
|
updateButtonState();
|
|
|
|
debouncedUpdateCSS();
|
|
|
|
} else if (prop === 'compilerHasError') {
|
|
|
|
updateErrorState();
|
|
|
|
} else if (prop === 'inputValue') {
|
|
|
|
debouncedUpdateCSS();
|
|
|
|
}
|
2023-06-09 18:24:33 +02:00
|
|
|
if (['inputFormat', 'outputFormat', 'inputValue'].includes(prop)) {
|
|
|
|
updateURL();
|
|
|
|
}
|
2023-06-07 15:46:39 +02:00
|
|
|
return set;
|
|
|
|
},
|
|
|
|
});
|
2023-06-07 15:55:41 +02:00
|
|
|
|
2023-06-09 18:59:45 +02:00
|
|
|
// Setup input sass view
|
2023-06-05 15:25:55 +02:00
|
|
|
const editor = new EditorView({
|
2023-06-09 18:24:33 +02:00
|
|
|
doc: playgroundState.inputValue,
|
2023-06-05 15:25:55 +02:00
|
|
|
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-05 15:25:55 +02:00
|
|
|
}
|
|
|
|
}),
|
|
|
|
],
|
2023-06-15 21:19:37 +02:00
|
|
|
parent: document.querySelector('.sl-code-is-precompiled') || undefined,
|
2023-06-05 15:25:55 +02:00
|
|
|
});
|
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-15 21:19:37 +02:00
|
|
|
parent: document.querySelector('.sl-code-is-compiled') || undefined,
|
2023-06-05 15:25:55 +02:00
|
|
|
});
|
2023-06-02 15:24:32 +02:00
|
|
|
|
2023-06-09 18:24:33 +02:00
|
|
|
// Apply initial state to dom
|
|
|
|
function applyInitialState() {
|
|
|
|
updateButtonState();
|
|
|
|
debouncedUpdateCSS();
|
|
|
|
updateErrorState();
|
|
|
|
}
|
|
|
|
|
2023-06-07 15:17:37 +02:00
|
|
|
type TabbarItemDataset =
|
|
|
|
| {
|
|
|
|
value: Syntax;
|
|
|
|
setting: 'inputFormat';
|
|
|
|
}
|
|
|
|
| {
|
|
|
|
value: OutputStyle;
|
|
|
|
setting: 'outputFormat';
|
|
|
|
};
|
2023-06-05 15:25:55 +02:00
|
|
|
function attachListeners() {
|
2023-06-09 18:59:45 +02:00
|
|
|
// Settings buttons handlers
|
2023-06-07 15:17:37 +02:00
|
|
|
function clickHandler(event: Event) {
|
|
|
|
if (event.currentTarget instanceof HTMLElement) {
|
|
|
|
const settings = event.currentTarget.dataset as TabbarItemDataset;
|
|
|
|
if (settings.setting === 'inputFormat') {
|
|
|
|
playgroundState.inputFormat = settings.value;
|
|
|
|
} else {
|
|
|
|
playgroundState.outputFormat = settings.value;
|
|
|
|
}
|
|
|
|
}
|
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-09 18:24:33 +02:00
|
|
|
|
2023-06-09 18:59:45 +02:00
|
|
|
// Copy URL handlers
|
2023-06-09 18:24:33 +02:00
|
|
|
const copyURLButton = document.getElementById('playground-copy-url');
|
|
|
|
const copiedAlert = document.getElementById('playground-copied-alert');
|
|
|
|
|
|
|
|
let timer: undefined | number;
|
|
|
|
|
|
|
|
copyURLButton?.addEventListener('click', () => {
|
|
|
|
void navigator.clipboard.writeText(location.href);
|
|
|
|
copiedAlert?.classList.add('show');
|
|
|
|
if (timer) clearTimeout(timer);
|
|
|
|
timer = window.setTimeout(() => {
|
|
|
|
copiedAlert?.classList.remove('show');
|
|
|
|
}, 3000);
|
|
|
|
});
|
2023-06-02 18:52:24 +02:00
|
|
|
}
|
2023-06-07 15:46:39 +02:00
|
|
|
/**
|
|
|
|
* updateButtonState
|
|
|
|
* Applies playgroundState to the buttons
|
|
|
|
* Called by state's proxy setter
|
|
|
|
*/
|
2023-06-06 22:02:26 +02:00
|
|
|
function updateButtonState() {
|
|
|
|
const inputFormatTab = document.querySelector(
|
|
|
|
'[data-setting="inputFormat"]',
|
|
|
|
) as HTMLDivElement;
|
2023-06-15 20:50:34 +02:00
|
|
|
const inputButtons = inputFormatTab.querySelectorAll('[data-value]');
|
|
|
|
inputButtons.forEach((button) => {
|
|
|
|
if (!(button instanceof HTMLButtonElement)) return;
|
|
|
|
button.dataset.active = String(
|
|
|
|
button.dataset.value === playgroundState.inputFormat,
|
|
|
|
);
|
|
|
|
});
|
2023-06-06 22:02:26 +02:00
|
|
|
|
|
|
|
const outputFormatTab = document.querySelector(
|
|
|
|
'[data-setting="outputFormat"]',
|
|
|
|
) as HTMLDivElement;
|
2023-06-15 20:50:34 +02:00
|
|
|
const outputButtons = outputFormatTab.querySelectorAll('[data-value]');
|
|
|
|
outputButtons.forEach((button) => {
|
|
|
|
if (!(button instanceof HTMLButtonElement)) return;
|
|
|
|
button.dataset.active = String(
|
|
|
|
button.dataset.value === playgroundState.outputFormat,
|
|
|
|
);
|
|
|
|
});
|
2023-06-06 22:02:26 +02:00
|
|
|
}
|
2023-06-07 15:46:39 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* updateErrorState
|
|
|
|
* Applies error state
|
|
|
|
* Called by state's proxy setter
|
|
|
|
*/
|
2023-06-06 22:02:26 +02:00
|
|
|
function updateErrorState() {
|
|
|
|
const editorWrapper = document.querySelector(
|
|
|
|
'[data-compiler-has-error]',
|
|
|
|
) as HTMLDivElement;
|
|
|
|
editorWrapper.dataset.compilerHasError =
|
|
|
|
playgroundState.compilerHasError.toString();
|
|
|
|
}
|
|
|
|
|
2023-06-09 18:59:45 +02:00
|
|
|
/**
|
|
|
|
* updateDebugOutput
|
|
|
|
* Applies debug output state
|
|
|
|
* Called at end of updateCSS, and not by a debugOutput setter
|
|
|
|
* debugOutput may be updated multiple times during the sass compilation,
|
|
|
|
* so the output is collected through the compilation and the display updated just once.
|
|
|
|
*/
|
2023-06-07 21:13:23 +02:00
|
|
|
function updateDebugOutput() {
|
2023-06-15 21:19:37 +02:00
|
|
|
const console = document.querySelector(
|
|
|
|
'.sl-c-playground__console',
|
|
|
|
) as HTMLDivElement;
|
2023-06-07 21:13:23 +02:00
|
|
|
console.innerHTML = playgroundState.debugOutput
|
|
|
|
.map(displayForConsoleLog)
|
|
|
|
.join('\n');
|
|
|
|
}
|
|
|
|
|
2023-06-05 15:25:55 +02:00
|
|
|
function updateCSS() {
|
2023-06-07 21:13:23 +02:00
|
|
|
playgroundState.debugOutput = [];
|
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-07 21:13:23 +02:00
|
|
|
playgroundState.debugOutput = [
|
|
|
|
...playgroundState.debugOutput,
|
|
|
|
{ type: 'error', error: result.error },
|
|
|
|
];
|
2023-06-05 19:55:56 +02:00
|
|
|
}
|
2023-06-07 21:13:23 +02:00
|
|
|
updateDebugOutput();
|
2023-06-05 15:25:55 +02:00
|
|
|
}
|
2023-06-08 20:40:42 +02:00
|
|
|
const debouncedUpdateCSS = debounce(updateCSS, 200);
|
2023-06-05 20:40:15 +02:00
|
|
|
|
2023-06-07 21:13:23 +02:00
|
|
|
const logger: Logger = {
|
|
|
|
warn(message, options) {
|
|
|
|
playgroundState.debugOutput = [
|
|
|
|
...playgroundState.debugOutput,
|
|
|
|
{ message, options, type: 'warn' },
|
|
|
|
];
|
|
|
|
},
|
|
|
|
debug(message, options) {
|
|
|
|
playgroundState.debugOutput = [
|
|
|
|
...playgroundState.debugOutput,
|
|
|
|
{ message, options, type: 'debug' },
|
|
|
|
];
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2023-06-05 19:55:56 +02:00
|
|
|
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-07 21:13:23 +02:00
|
|
|
logger: logger,
|
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-09 18:24:33 +02:00
|
|
|
function updateURL() {
|
|
|
|
const hash = stateToBase64(playgroundState);
|
2023-06-21 15:54:25 +02:00
|
|
|
history.replaceState('playground', '', `?#${hash}`);
|
2023-06-09 18:24:33 +02:00
|
|
|
}
|
|
|
|
|
2023-06-21 16:33:05 +02:00
|
|
|
function updateSassVersion() {
|
|
|
|
const version = info.split('\t')[1];
|
|
|
|
const versionSpan = document.querySelector(
|
|
|
|
'.sl-c-playground__tabbar-version',
|
|
|
|
) as HTMLSpanElement;
|
|
|
|
versionSpan.innerText = `v${version}`;
|
|
|
|
}
|
|
|
|
|
2023-06-05 15:25:55 +02:00
|
|
|
attachListeners();
|
2023-06-09 18:24:33 +02:00
|
|
|
applyInitialState();
|
2023-06-21 16:33:05 +02:00
|
|
|
updateSassVersion();
|
2023-06-05 15:25:55 +02:00
|
|
|
}
|
2023-06-10 18:31:43 +02:00
|
|
|
|
2023-06-05 15:25:55 +02:00
|
|
|
if (document.readyState === 'loading') {
|
|
|
|
document.addEventListener('DOMContentLoaded', setupPlayground);
|
|
|
|
} else {
|
|
|
|
setupPlayground();
|
2023-06-02 15:24:32 +02:00
|
|
|
}
|