blob: 81ce4bb0991ffc6d43741981f8f17b355bf2f4e8 [file] [log] [blame] [edit]
import * as localWasm from '../../wasm';
import { EditorView, basicSetup } from 'codemirror';
import { javascript } from '@codemirror/lang-javascript';
import { css } from '@codemirror/lang-css';
import { oneDark } from '@codemirror/theme-one-dark';
import { syntaxTree } from '@codemirror/language';
import { linter, lintGutter } from '@codemirror/lint'
import { Compartment } from '@codemirror/state'
const linterCompartment = new Compartment;
const visitorLinterCompartment = new Compartment;
let wasm;
let editor, visitorEditor, outputEditor, modulesEditor, depsEditor;
let enc = new TextEncoder();
let dec = new TextDecoder();
let inputs = document.querySelectorAll('input[type=number]');
async function loadVersions() {
const { versions } = await fetch('https://data.jsdelivr.com/v1/package/npm/lightningcss-wasm').then(r => r.json());
versions
.map(v => {
const option = document.createElement('option');
option.value = v;
option.textContent = v;
return option;
})
.forEach(o => {
version.appendChild(o);
})
}
async function loadWasm() {
if (version.value === 'local') {
wasm = localWasm;
} else {
wasm = await new Function('version', 'return import(`https://esm.sh/lightningcss-wasm@${version}?bundle`)')(version.value);
}
await wasm.default();
}
function loadPlaygroundState() {
const hash = window.location.hash.slice(1);
try {
return JSON.parse(decodeURIComponent(hash));
} catch {
return {
minify: minify.checked,
visitorEnabled: visitorEnabled.checked,
targets: getTargets(),
include: 0,
exclude: 0,
source: `@custom-media --modern (color), (hover);
.foo {
background: yellow;
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
border-radius: 2px;
-webkit-transition: background 200ms;
-moz-transition: background 200ms;
transition: background 200ms;
&.bar {
color: green;
}
}
@media (--modern) and (width > 1024px) {
.a {
color: green;
}
}`,
version: version.value,
visitor: `{
Color(color) {
if (color.type === 'rgb') {
color.g = 0;
return color;
}
}
}`
};
}
}
function reflectPlaygroundState(playgroundState) {
if (typeof playgroundState.minify !== 'undefined') {
minify.checked = playgroundState.minify;
}
if (typeof playgroundState.cssModules !== 'undefined') {
cssModules.checked = playgroundState.cssModules;
compiledModules.hidden = !playgroundState.cssModules;
}
if (typeof playgroundState.analyzeDependencies !== 'undefined') {
analyzeDependencies.checked = playgroundState.analyzeDependencies;
compiledDependencies.hidden = !playgroundState.analyzeDependencies;
}
if (typeof playgroundState.customMedia !== 'undefined') {
customMedia.checked = playgroundState.customMedia;
}
if (typeof playgroundState.visitorEnabled !== 'undefined') {
visitorEnabled.checked = playgroundState.visitorEnabled;
}
if (playgroundState.targets) {
const { targets } = playgroundState;
for (let input of inputs) {
let value = targets[input.id];
input.value = value == null ? '' : value >> 16;
}
}
updateFeatures(sidebar.elements.include, playgroundState.include);
updateFeatures(sidebar.elements.exclude, playgroundState.exclude);
if (playgroundState.include) {
include.parentElement.open = true;
}
if (playgroundState.exclude) {
exclude.parentElement.open = true;
}
if (playgroundState.unusedSymbols) {
unusedSymbols.value = playgroundState.unusedSymbols.join('\n');
}
if (playgroundState.version) {
version.value = playgroundState.version;
}
}
function savePlaygroundState() {
let data = new FormData(sidebar);
const playgroundState = {
minify: minify.checked,
customMedia: customMedia.checked,
cssModules: cssModules.checked,
analyzeDependencies: analyzeDependencies.checked,
targets: getTargets(),
include: getFeatures(data.getAll('include')),
exclude: getFeatures(data.getAll('exclude')),
source: editor.state.doc.toString(),
visitorEnabled: visitorEnabled.checked,
visitor: visitorEditor.state.doc.toString(),
unusedSymbols: unusedSymbols.value.split('\n').map(v => v.trim()).filter(Boolean),
version: version.value,
};
const hash = encodeURIComponent(JSON.stringify(playgroundState));
if (
typeof URL === 'function' &&
typeof history === 'object' &&
typeof history.replaceState === 'function'
) {
const url = new URL(location.href);
url.hash = hash;
history.replaceState(null, null, url);
} else {
location.hash = hash;
}
}
function getTargets() {
let targets = {};
for (let input of inputs) {
if (input.value !== '') {
targets[input.id] = input.valueAsNumber << 16;
}
}
return targets;
}
function getFeatures(vals) {
let features = 0;
for (let name of vals) {
features |= wasm.Features[name];
}
return features;
}
function updateFeatures(elements, include) {
for (let checkbox of elements) {
let feature = wasm.Features[checkbox.value];
checkbox.checked = (include & feature) === feature;
checkbox.indeterminate = !checkbox.checked && (include & feature);
}
}
function update() {
const { transform } = wasm;
const targets = getTargets();
let data = new FormData(sidebar);
let include = getFeatures(data.getAll('include'));
let exclude = getFeatures(data.getAll('exclude'));
try {
let res = transform({
filename: 'test.css',
code: enc.encode(editor.state.doc.toString()),
minify: minify.checked,
targets: Object.keys(targets).length === 0 ? null : targets,
include,
exclude,
drafts: {
customMedia: customMedia.checked
},
cssModules: cssModules.checked,
analyzeDependencies: analyzeDependencies.checked,
unusedSymbols: unusedSymbols.value.split('\n').map(v => v.trim()).filter(Boolean),
visitor: visitorEnabled.checked ? (0, eval)('(' + visitorEditor.state.doc.toString() + ')') : undefined,
});
let update = outputEditor.state.update({ changes: { from: 0, to: outputEditor.state.doc.length, insert: dec.decode(res.code) } });
outputEditor.update([update]);
if (res.exports) {
let update = modulesEditor.state.update({ changes: { from: 0, to: modulesEditor.state.doc.length, insert: '// CSS module exports\n' + JSON.stringify(res.exports, false, 2) } });
modulesEditor.update([update]);
}
if (res.dependencies) {
let update = depsEditor.state.update({ changes: { from: 0, to: depsEditor.state.doc.length, insert: '// Dependencies\n' + JSON.stringify(res.dependencies, false, 2) } });
depsEditor.update([update]);
}
compiledModules.hidden = !cssModules.checked;
compiledDependencies.hidden = !analyzeDependencies.checked;
visitor.hidden = !visitorEnabled.checked;
source.dataset.expanded = visitor.hidden;
compiled.dataset.expanded = compiledModules.hidden && compiledDependencies.hidden;
compiledModules.dataset.expanded = compiledDependencies.hidden;
compiledDependencies.dataset.expanded = compiledModules.hidden;
editor.dispatch({
effects: linterCompartment.reconfigure(createCssLinter(null, res.warnings))
});
visitorEditor.dispatch({
effects: visitorLinterCompartment.reconfigure(createVisitorLinter(null))
});
} catch (e) {
let update = outputEditor.state.update({ changes: { from: 0, to: outputEditor.state.doc.length, insert: `/* ERROR: ${e.message} */` } });
outputEditor.update([update]);
editor.dispatch({
effects: linterCompartment.reconfigure(createCssLinter(e))
});
visitorEditor.dispatch({
effects: visitorLinterCompartment.reconfigure(createVisitorLinter(e))
});
}
savePlaygroundState();
}
function createCssLinter(lastError, warnings) {
return linter(view => {
let diagnostics = [];
if (lastError && lastError.loc) {
let l = view.state.doc.line(lastError.loc.line);
let loc = l.from + lastError.loc.column - 1;
let node = syntaxTree(view.state).resolveInner(loc, 1);
diagnostics.push(
{
from: node.from,
to: node.to,
message: lastError.message,
severity: 'error'
}
);
}
if (warnings) {
for (let warning of warnings) {
let l = view.state.doc.line(warning.loc.line);
let loc = l.from + warning.loc.column - 1;
let node = syntaxTree(view.state).resolveInner(loc, 1);
diagnostics.push({
from: node.from,
to: node.to,
message: warning.message,
severity: 'warning'
});
}
}
return diagnostics;
}, { delay: 0 });
}
function createVisitorLinter(lastError) {
return linter(view => {
if (lastError && !lastError.loc) {
// Firefox has lineNumber and columnNumber, Safari has line and column.
let line = lastError.lineNumber ?? lastError.line;
let column = lastError.columnNumber ?? lastError.column;
if (lastError.column != null) {
column--;
}
if (line == null) {
// Chrome.
let match = lastError.stack.match(/(?:(?:eval.*<anonymous>:)|(?:eval:))(?<line>\d+):(?<column>\d+)/);
if (match) {
line = Number(match.groups.line);
column = Number(match.groups.column);
// Chrome's column numbers are off by the amount of leading whitespace in the line.
let l = view.state.doc.line(line);
let m = l.text.match(/^\s*/);
if (m) {
column += m[0].length;
}
}
}
if (line != null) {
let l = view.state.doc.line(line);
let loc = l.from + column;
let node = syntaxTree(view.state).resolveInner(loc, -1);
return [
{
from: node.from,
to: node.to,
message: lastError.message,
severity: 'error'
}
];
}
}
return [];
}, { delay: 0 });
}
function renderFeatures(parent, name) {
for (let feature in wasm.Features) {
let label = document.createElement('label');
let checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.name = name;
checkbox.value = feature;
checkbox.oninput = () => {
let data = new FormData(sidebar);
let flags = getFeatures(data.getAll(name));
let f = wasm.Features[feature];
if (checkbox.checked) {
flags |= f;
} else {
flags &= ~f;
}
updateFeatures(sidebar.elements[name], flags);
};
label.appendChild(checkbox);
label.appendChild(document.createTextNode(' ' + feature))
parent.appendChild(label);
}
}
async function main() {
await loadWasm();
renderFeatures(include, 'include');
renderFeatures(exclude, 'exclude');
let state = loadPlaygroundState();
reflectPlaygroundState(state);
editor = new EditorView({
extensions: [lintGutter(), basicSetup, css(), oneDark, linterCompartment.of(createCssLinter())],
parent: source,
doc: state.source,
dispatch(tr) {
editor.update([tr]);
if (tr.docChanged) {
update();
}
}
});
visitorEditor = new EditorView({
extensions: [lintGutter(), basicSetup, javascript(), oneDark, visitorLinterCompartment.of(createVisitorLinter())],
parent: visitor,
doc: state.visitor,
dispatch(tr) {
visitorEditor.update([tr]);
if (tr.docChanged) {
update();
}
}
});
outputEditor = new EditorView({
extensions: [basicSetup, css(), oneDark, EditorView.editable.of(false), EditorView.lineWrapping],
parent: compiled,
});
modulesEditor = new EditorView({
extensions: [basicSetup, javascript(), oneDark, EditorView.editable.of(false), EditorView.lineWrapping],
parent: compiledModules,
});
depsEditor = new EditorView({
extensions: [basicSetup, javascript(), oneDark, EditorView.editable.of(false), EditorView.lineWrapping],
parent: compiledDependencies,
});
update();
sidebar.oninput = update;
await loadVersions();
version.onchange = async () => {
await loadWasm();
update();
};
}
main();