Merge branch 'feature/playground'

This commit is contained in:
Moritz Hölting 2024-10-01 02:34:37 +02:00
commit b3677165ce
34 changed files with 4203 additions and 341 deletions

View File

@ -20,12 +20,20 @@ jobs:
steps: steps:
- name: Checkout your repository using git - name: Checkout your repository using git
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Cache cargo & target directories
uses: Swatinem/rust-cache@v2
with:
key: "webcompiler"
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- name: Install wasm-pack-cli
uses: jetli/wasm-pack-action@v0.2.0
- name: Install, build, and upload your site output - name: Install, build, and upload your site output
uses: withastro/action@v2 uses: withastro/action@v2
with: with:
# path: . # The root location of your Astro project inside the repository. (optional) package-manager: pnpm@latest
# node-version: 18 # The specific version of Node that should be used to build your site. Defaults to 18. (optional)
package-manager: pnpm@latest # The Node package manager that should be used to install dependencies and build your site. Automatically detected based on your lockfile. (optional)
deploy: deploy:
needs: build needs: build

5
.gitignore vendored
View File

@ -16,7 +16,10 @@ pnpm-debug.log*
# environment variables # environment variables
.env .env
.env.production .env.production
secrets.env *.env
# macOS-specific files # macOS-specific files
.DS_Store .DS_Store
# wasm build files
/target_rust

View File

@ -2,17 +2,29 @@
This is the documentation for Shulkerscript. It is a work in progress and will be updated as the language evolves. This is the documentation for Shulkerscript. It is a work in progress and will be updated as the language evolves.
## Getting Started ## Getting Started
This documentation is created using Astro and Starlight. To get started, you need to install the dependencies and start the development server. This documentation is created using Astro and Starlight. To get started, you need to install the dependencies and start the development server.
## Requirements
Required tools:
- [Node.js](https://nodejs.org)
- [pnpm](https://pnpm.io)
- [Cargo](https://rustup.rs)
- with `wasm32-unknown-unknown` target installed
- [`wasm-bindgen-cli`](https://crates.io/crates/wasm-bindgen-cli)
## 🧞 Commands ## 🧞 Commands
All commands are run from the root of the project, from a terminal: All commands are run from the root of the project, from a terminal:
| Command | Action | | Command | Action |
| :------------------------ | :----------------------------------------------- | | :------------------------- | :----------------------------------------------- |
| `pnpm install` | Installs dependencies | | `pnpm install` | Installs dependencies |
| `pnpm run dev` | Starts local dev server at `localhost:4321` | | `pnpm run dev` | Starts local dev server at `localhost:4321` |
| `pnpm run build` | Build your production site to `./dist/` | | `pnpm run build` | Build your production site to `./dist/` |
| `pnpm run build-wasm` | Build your wasm modules |
| `pnpm run preview` | Preview your build locally, before deploying | | `pnpm run preview` | Preview your build locally, before deploying |
| `pnpm run astro ...` | Run CLI commands like `astro add`, `astro check` | | `pnpm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `pnpm run astro -- --help` | Get help using the Astro CLI | | `pnpm run astro -- --help` | Get help using the Astro CLI |

View File

@ -1,12 +1,25 @@
import { defineConfig } from 'astro/config'; import { defineConfig } from "astro/config";
import starlight from '@astrojs/starlight'; import starlight from "@astrojs/starlight";
import starlightLinksValidator from "starlight-links-validator"; import react from "@astrojs/react";
import shikiConfig from './src/utils/shiki'; // import starlightLinksValidator from "starlight-links-validator";
import starlightUtils from "@lorenzo_lewis/starlight-utils";
import wasm from "vite-plugin-wasm";
import shikiConfig from "./src/utils/shiki";
const playgroundSidebarEntry = {
label: 'Playground',
link: '/playground',
translations: { de: 'Spielplatz' },
};
const navLinks = [playgroundSidebarEntry];
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
site: "https://shulkerscript.hoelting.dev", site: "https://shulkerscript.hoelting.dev",
integrations: [ integrations: [
react(),
starlight({ starlight({
title: 'Shulkerscript', title: 'Shulkerscript',
logo: { logo: {
@ -34,16 +47,31 @@ export default defineConfig({
baseUrl: 'https://github.com/moritz-hoelting/shulkerscript-webpage/edit/main', baseUrl: 'https://github.com/moritz-hoelting/shulkerscript-webpage/edit/main',
}, },
customCss: ['./src/styles/style.css'], customCss: ['./src/styles/style.css'],
plugins: [starlightLinksValidator({ plugins: [starlightUtils({
errorOnFallbackPages: false, navLinks: {
})], leading: {
useSidebarLabelled: "leadingNavLinks",
},
},
}),
// starlightLinksValidator({
// errorOnFallbackPages: false,
// })
],
expressiveCode: { expressiveCode: {
shiki: shikiConfig, shiki: shikiConfig,
}, },
components: { components: {
PageTitle: "./src/components/override/PageTitle.astro",
ContentPanel: "./src/components/override/ContentPanel.astro",
Pagination: "./src/components/override/Pagination.astro",
SocialIcons: './src/components/override/SocialIcons.astro', SocialIcons: './src/components/override/SocialIcons.astro',
}, },
sidebar: [ sidebar: [
{
label: "leadingNavLinks",
items: navLinks,
},
{ {
label: 'Guides', label: 'Guides',
autogenerate: { autogenerate: {
@ -53,6 +81,19 @@ export default defineConfig({
de: 'Anleitungen', de: 'Anleitungen',
} }
}, },
{
label: "More",
translations: {
de: "Mehr",
},
items: [
{
label: "Roadmap",
link: "/roadmap",
translations: {
de: "Zukunftspläne",
},
},
{ {
label: 'Differences to other languages', label: 'Differences to other languages',
link: '/differences', link: '/differences',
@ -61,11 +102,13 @@ export default defineConfig({
}, },
}, },
{ {
label: 'Roadmap', ...playgroundSidebarEntry,
link: '/roadmap', badge: {
translations: { text: "WIP",
de: 'Zukunftspläne', variant: "caution",
}, },
},
],
}, },
{ {
label: 'Reference', label: 'Reference',
@ -84,4 +127,9 @@ export default defineConfig({
] ]
}), }),
], ],
vite: {
plugins: [
wasm(),
],
}
}); });

View File

@ -3,20 +3,40 @@
"type": "module", "type": "module",
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "pnpm build-wasm && astro dev",
"start": "astro dev", "start": "pnpm build-wasm && astro dev",
"build": "astro check && astro build", "build-wasm": "wasm-pack build --release --no-pack ./src/wasm/webcompiler -- --features wee_alloc",
"build": "pnpm build-wasm && astro check && astro build",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.3", "@astrojs/check": "^0.9.3",
"@astrojs/react": "^3.6.0",
"@astrojs/starlight": "^0.28.2", "@astrojs/starlight": "^0.28.2",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@lorenzo_lewis/starlight-utils": "^0.1.1",
"@monaco-editor/react": "^4.6.0",
"@mui/icons-material": "^6.1.1",
"@mui/material": "^6.1.1",
"@shikijs/monaco": "^1.7.0",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"astro": "^4.15.9", "astro": "^4.15.9",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.2.1",
"sharp": "^0.33.5", "sharp": "^0.33.5",
"shiki": "^1.14.1", "shiki": "^1.7.0",
"starlight-links-validator": "^0.12.1", "starlight-links-validator": "^0.12.1",
"typescript": "^5.4.5" "tm-themes": "^1.4.3",
"typescript": "^5.4.5",
"use-immer": "^0.10.0",
"vite-plugin-wasm": "^3.3.0"
},
"devDependencies": {
"sass": "^1.77.6"
}, },
"packageManager": "pnpm@9.7.0+" "packageManager": "pnpm@9.7.0+"
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
namespace "my-shulkerscript-pack";
#[tick]
fn main() {
// Change this
/say Hello World!
}

View File

@ -0,0 +1,5 @@
[pack]
name = "my-shulkerscript-pack"
description = "A Minecraft datapack created with shulkerscript"
format = 48
version = "0.1.0"

View File

@ -0,0 +1,493 @@
import { useEffect, useState } from "react";
import { useMonaco, type Monaco } from "@monaco-editor/react";
import { useImmer, type Updater } from "use-immer";
import ThemeProvider from "@mui/material/styles/ThemeProvider";
import ErrorDisplay from "./playground/ErrorDisplay";
import FileView from "./playground/FileView";
import Editor from "./playground/Editor";
import Header from "./playground/Header";
import {
compile,
compileZip,
} from "@wasm/webcompiler/pkg/webcompiler";
import type { Directory, File, PlaygroundLang } from "@utils/playground";
import { customTheme } from "@utils/material-ui-theme";
import "@styles/playground.scss";
import mainFileContent from "@assets/playground/main.shu?raw";
import packTomlContent from "@assets/playground/pack.toml?raw";
const FILE_STORAGE_KEY = "playground-files";
const DEFAULT_FILES = {
dirs: {
src: {
files: {
"main.shu": {
content: mainFileContent,
language: "shulkerscript",
},
},
},
},
files: {
"pack.toml": {
content: packTomlContent,
language: "toml",
},
},
};
export default function Playground({ lang }: { lang: PlaygroundLang }) {
const [errorMsg, setErrorMsg] = useState<string | null>(null);
useEffect(() => {
(window as any).playground = {
showError: (message: string) => {
if (message.length > 0) {
setErrorMsg(message);
}
},
};
}, []);
const [rootDir, updateRootDir] = useImmer(
getStorageOrDefault(FILE_STORAGE_KEY, DEFAULT_FILES) as Directory
);
const [theme, setTheme] = useState<"light" | "dark">("dark");
const [fileName, setFileName] = useState("src/main.shu");
const file = getFile(rootDir, fileName);
const onBuild = () => {
if (monaco) {
const compiled = compile(getFiles(monaco));
if (compiled) {
const dist = JSON.parse(JSON.stringify(compiled, jsonReplacer));
const withRoot = {
dirs: {
dist: dist,
},
} as Directory;
loadFiles(monaco, updateRootDir, withRoot);
}
} else {
console.error("monaco has not loaded");
}
};
const onZip = () => {
if (monaco) {
const zipped = compileZip(getFiles(monaco));
if (zipped) {
const data = "data:application/zip;base64," + zipped;
const a = document.createElement("a");
a.href = data;
a.download = "shulkerscript-pack.zip";
a.click();
}
} else {
console.error("monaco has not loaded");
}
};
const onSave = () => {
if (monaco) {
const currentFiles = getFiles(monaco);
updateRootDir((dir) => {
dir.dirs = currentFiles.dirs;
dir.files = currentFiles.files;
});
window.localStorage.setItem(
FILE_STORAGE_KEY,
JSON.stringify(currentFiles)
);
}
};
const onReset = () => {
if (monaco) {
monaco.editor.getModels().forEach((model) => {
if (model.uri.path != "/src/main.shu") {
model.dispose();
} else {
model.setValue(mainFileContent);
}
});
updateRootDir((dir) => {
dir.dirs = DEFAULT_FILES.dirs;
dir.files = DEFAULT_FILES.files;
});
loadFiles(monaco, updateRootDir, DEFAULT_FILES);
setFileName("src/main.shu");
}
};
const monaco = useMonaco();
useEffect(() => {
if (monaco) {
loadFiles(monaco, updateRootDir, rootDir);
}
}, [monaco]);
useEffect(() => {
if (monaco) {
let isReadOnly = fileName.startsWith("dist/");
monaco.editor.getEditors().forEach((e) =>
e.updateOptions({
readOnly: isReadOnly,
readOnlyMessage: {
value: "Generated files are read-only",
},
})
);
}
}, [fileName]);
useEffect(() => {
const root = document.querySelector(":root") as HTMLElement;
if (root) {
function reactToThemeChange() {
const selectedTheme = root.getAttribute("data-theme");
if (selectedTheme !== theme && selectedTheme !== null) {
setTheme(selectedTheme as "light" | "dark");
}
}
reactToThemeChange();
root.onchange = () => {
reactToThemeChange();
};
}
});
return (
<ThemeProvider theme={customTheme(theme)}>
<main
className="playground not-content"
style={{
maxWidth: "95vw",
marginInline: "auto",
marginTop: "0.5cm",
}}
>
<ErrorDisplay
lang={lang.errorDisplay}
error={errorMsg}
setError={setErrorMsg}
/>
<Header
lang={lang.header}
onSave={onSave}
onReset={onReset}
onBuild={onBuild}
onZip={onZip}
/>
<FileView
className="file-view"
root={rootDir}
selectedFileName={fileName}
setSelectedFileName={setFileName}
addFile={(name) => {
if (monaco) {
loadFile(
monaco,
updateRootDir,
{ content: "" },
name
);
}
}}
deleteFile={(name) => {
if (monaco) {
if (name.endsWith("/")) {
deleteDir(monaco, updateRootDir, name);
} else {
deleteFile(monaco, updateRootDir, name);
if (name === fileName) {
const newFile = monaco.editor
.getModels()[0]
?.uri.path.slice(1);
if (newFile) {
setFileName(newFile);
} else {
setFileName("");
}
}
}
}
}}
renameFile={(oldName, newName) => {
if (monaco) {
renameFile(monaco, updateRootDir, oldName, newName);
if (oldName === fileName) {
setFileName(newName);
}
}
}}
lang={lang.explorer}
/>
<Editor
fileName={fileName}
file={file ?? undefined}
theme={theme}
/>
</main>
</ThemeProvider>
);
}
function getFiles(monaco: Monaco): Directory {
const files: Directory = {};
for (const model of monaco.editor.getModels()) {
const parts = model.uri.path.slice(1).split("/");
const name = parts.pop()!;
let dir = files;
for (const part of parts) {
if (!dir.dirs) {
dir.dirs = {};
}
if (!dir.dirs[part]) {
dir.dirs[part] = {};
}
dir = dir.dirs[part];
}
if (!dir.files) {
dir.files = {};
}
dir.files[name] = {
content: model.getValue(),
language: model.getLanguageId(),
};
}
return files;
}
function getFile(root: Directory, path: string): File | null {
if (path.includes("/")) {
let dir = root;
const split = path.split("/");
let last = split.pop()!;
for (const dirName of split) {
if (dir && dir.dirs) {
dir = dir.dirs[dirName];
} else {
return null;
}
}
return dir.files?.[last] ?? null;
}
return root.files?.[path] ?? null;
}
function loadFiles(
monaco: Monaco,
updater: Updater<Directory>,
dir: Directory,
prefix = ""
) {
for (const [name, d] of Object.entries(dir.dirs ?? {})) {
loadFiles(monaco, updater, d, prefix + name + "/");
updater((dir) => {
let current = dir;
for (const part of [
...prefix.split("/").filter((s) => s !== ""),
name,
]) {
if (!current.dirs) {
current.dirs = {};
}
current = current.dirs[part];
}
});
}
for (const [name, file] of Object.entries(dir.files ?? {})) {
loadFile(monaco, updater, file, prefix + name);
}
}
function loadFile(
monaco: Monaco,
updater: Updater<Directory>,
file: File,
name: string
) {
let extension = name.split(".").pop()!;
let lang = undefined;
if (extension === "shu") {
lang = "shulkerscript";
} else if (extension === "toml") {
lang = "toml";
} else if (extension === "mcfunction") {
lang = "mcfunction";
} else if (extension === "json") {
lang = "json";
}
const uri = monaco.Uri.parse(name);
let prevModel = monaco.editor.getModel(uri);
if (prevModel) {
prevModel.setValue(file.content);
} else {
monaco.editor.createModel(file.content, lang, uri);
}
updater((dir) => {
if (dir) {
let current = dir;
const parts = name.split("/").filter((s) => s !== "");
const last = parts.pop()!;
for (const part of parts) {
if (!current.dirs) {
current.dirs = {};
}
if (!current.dirs[part]) {
current.dirs[part] = {};
}
current = current.dirs[part];
}
if (!current.files) {
current.files = {};
}
current.files[last] = {
content: file.content,
language: lang,
};
}
});
}
function getStorageOrDefault(key: string, def: any) {
const item = window.localStorage.getItem(key);
if (item) {
return JSON.parse(item);
} else {
return def;
}
}
function jsonReplacer(_key: any, value: any): any {
if (value instanceof Map) {
const res: { [key: string]: any } = {};
for (const [k, v] of value.entries()) {
res[k] = v;
}
return res;
} else {
return value;
}
}
function deleteFile(monaco: Monaco, updater: Updater<Directory>, name: string) {
const uri = monaco.Uri.parse(name);
const model = monaco.editor.getModel(uri);
if (model) {
model.dispose();
} else {
console.error("Model not found: ", name);
}
updater((dir) => {
let current = dir;
const parts = name.split("/").filter((s) => s !== "");
const last = parts.pop()!;
for (const part of parts) {
if (!current.dirs) {
current.dirs = {};
}
if (!current.dirs[part]) {
current.dirs[part] = {};
}
current = current.dirs[part];
}
if (current.files) {
delete current.files[last];
}
});
}
function deleteDir(monaco: Monaco, updater: Updater<Directory>, path: string) {
const parts = path.split("/");
const firstCheck = parts.at(0);
if (firstCheck != undefined && firstCheck == "") {
parts.splice(0, 1);
}
const lastCheck = parts.at(-1);
if (lastCheck != undefined && lastCheck == "") {
parts.pop();
}
let current = getFiles(monaco) as Directory | null;
for (let part of parts) {
if (current?.dirs) {
current = current.dirs[part];
}
}
if (current) {
destroyModels(monaco, path, current);
}
updater((dir) => {
const last = parts.pop();
if (!last) return;
let current = dir;
for (const part of parts) {
if (current.dirs) {
current = current.dirs[part];
if (!current) return;
} else {
return;
}
}
if (current.dirs) {
delete current.dirs[last];
}
});
}
function destroyModels(monaco: Monaco, path: string, dir: Directory) {
if (!path.endsWith("/")) {
path += "/";
}
if (dir.files) {
for (const file of Object.keys(dir.files)) {
const filepath = path + file;
const uri = monaco.Uri.parse(filepath);
const model = monaco.editor.getModel(uri);
if (model) {
model.dispose();
} else {
console.error("Model not found: ", filepath);
}
}
}
if (dir.dirs) {
for (const dirname of Object.keys(dir.dirs)) {
const dirpath = path + dirname;
const subdir = dir.dirs[dirname];
destroyModels(monaco, dirpath, subdir);
}
}
}
function renameFile(
monaco: Monaco,
updater: Updater<Directory>,
oldName: string,
newName: string
) {
const file = getFile(getFiles(monaco), oldName);
if (file) {
deleteFile(monaco, updater, oldName);
loadFile(monaco, updater, file, newName);
}
}

View File

@ -0,0 +1,17 @@
---
import type { Props } from "@astrojs/starlight/props";
import Default from "@astrojs/starlight/components/ContentPanel.astro";
import { isPlaygroundPage } from '@utils/playground';
const isPlayground = isPlaygroundPage(Astro.props.slug, Astro.currentLocale);
---
{
isPlayground ? (
<slot />
) : (
<Default {...Astro.props}>
<slot />
</Default>
)
}

View File

@ -0,0 +1,10 @@
---
import type { Props } from "@astrojs/starlight/props";
import Default from "@astrojs/starlight/components/PageTitle.astro";
import { isPlaygroundPage } from '@utils/playground';
const isPlayground = isPlaygroundPage(Astro.props.slug, Astro.currentLocale);
---
{isPlayground ? <></> : <Default {...Astro.props}><slot /></Default>}

View File

@ -0,0 +1,10 @@
---
import type { Props } from "@astrojs/starlight/props";
import Default from "@astrojs/starlight/components/Pagination.astro";
import { isPlaygroundPage } from '@utils/playground';
const isPlayground = isPlaygroundPage(Astro.props.slug, Astro.currentLocale);
---
{isPlayground ? <></> : <Default {...Astro.props}><slot /></Default>}

View File

@ -0,0 +1,117 @@
import * as React from "react";
import Button from "@mui/material/Button";
import ButtonGroup from "@mui/material/ButtonGroup";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import ClickAwayListener from "@mui/material/ClickAwayListener";
import Grow from "@mui/material/Grow";
import Paper from "@mui/material/Paper";
import Popper from "@mui/material/Popper";
import MenuItem from "@mui/material/MenuItem";
import MenuList from "@mui/material/MenuList";
export default function DropdownButton({
options,
visible,
style,
}: {
options: [string, React.MouseEventHandler<HTMLLIElement>][];
visible: [React.ReactNode, React.MouseEventHandler<HTMLButtonElement>][];
style?: React.CSSProperties;
}) {
const [open, setOpen] = React.useState(false);
const anchorRef = React.useRef<HTMLDivElement>(null);
const handleMenuItemClick = (
event: React.MouseEvent<HTMLLIElement, MouseEvent>,
index: number
) => {
options[index][1](event);
setOpen(false);
};
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
const handleClose = (event: Event) => {
if (
anchorRef.current &&
anchorRef.current.contains(event.target as HTMLElement)
) {
return;
}
setOpen(false);
};
return (
<>
<ButtonGroup
variant="contained"
ref={anchorRef}
aria-label="Button group with a nested menu"
style={style}
>
{visible.map(([children, onClick], index) => {
return (
<Button key={index} onClick={onClick}>
{children}
</Button>
);
})}
<Button
size="small"
aria-controls={open ? "split-button-menu" : undefined}
aria-expanded={open ? "true" : undefined}
aria-label="select merge strategy"
aria-haspopup="menu"
onClick={handleToggle}
>
<ArrowDropDownIcon />
</Button>
</ButtonGroup>
<Popper
sx={{
zIndex: 1,
}}
open={open}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === "bottom"
? "center top"
: "center bottom",
}}
>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MenuList id="split-button-menu" autoFocusItem>
{options.map((option, index) => (
<MenuItem
key={option[0]}
onClick={(event) =>
handleMenuItemClick(
event,
index
)
}
>
{option[0]}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</>
);
}

View File

@ -0,0 +1,61 @@
import type { File } from "@utils/playground";
import MonacoEditor, { useMonaco } from "@monaco-editor/react";
import { getHighlighter, type Highlighter } from "shiki";
import { shikiToMonaco } from "@shikijs/monaco";
import { useEffect, useState } from "react";
import darkPlus from "tm-themes/themes/dark-plus.json";
import lightPlus from "tm-themes/themes/light-plus.json";
import { shulkerscriptGrammar } from "@utils/shulkerscript-grammar";
import { mcfunctionGrammar } from "@utils/mcfunction-grammar";
export default function Editor({
theme,
fileName,
file,
}: {
theme: "light" | "dark";
fileName: string;
file?: File;
}) {
const [highlighter, setHighlighter] = useState<Highlighter | null>(null);
const monaco = useMonaco();
useEffect(() => {
if (monaco) {
if (highlighter == null) {
getHighlighter({
themes: [darkPlus as any, lightPlus],
langs: ["toml", shulkerscriptGrammar, mcfunctionGrammar],
}).then((highlighter) => {
highlighter.setTheme(
theme === "dark" ? "dark-plus" : "light-plus"
);
setHighlighter(highlighter);
});
} else {
shikiToMonaco(highlighter, monaco);
}
monaco.languages.register({ id: "toml" });
monaco.languages.register({ id: "shulkerscript" });
monaco.languages.register({ id: "mcfunction" });
}
}, [monaco]);
useEffect(() => {
if (highlighter != null) {
shikiToMonaco(highlighter, monaco);
}
}, [highlighter]);
return (
<MonacoEditor
height="70vh"
theme={theme === "dark" ? "dark-plus" : "light-plus"}
path={fileName}
defaultLanguage={file?.language}
defaultValue={file?.content}
wrapperProps={{ className: "editor" }}
/>
);
}

View File

@ -0,0 +1,34 @@
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogTitle from "@mui/material/DialogTitle";
import type { PlaygroundErrorDisplayLang } from "@utils/playground";
export default function ErrorDisplay({
lang,
error,
setError,
}: {
lang: PlaygroundErrorDisplayLang;
error: string | null;
setError: (error: string | null) => void;
}) {
return (
<Dialog open={error !== null} onClose={() => setError(null)}>
<DialogTitle>{lang.title}</DialogTitle>
<DialogContent>
<div className="error-terminal-display">
<code
dangerouslySetInnerHTML={{ __html: error ?? "" }}
></code>
</div>
</DialogContent>
<DialogActions>
<Button onClick={() => setError(null)}>
{lang.buttons.close}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@ -0,0 +1,67 @@
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import TextField from "@mui/material/TextField";
import type { PlaygroundExplorerLang } from "@utils/playground";
export default function AddFileDialog({
open,
handleClose,
addFile,
lang,
defaultPath,
}: {
open: boolean;
defaultPath: string;
lang: PlaygroundExplorerLang;
handleClose: () => void;
addFile: (filepath: string) => void;
}) {
return (
<Dialog
open={open}
onClose={handleClose}
PaperProps={{
component: "form",
onSubmit: (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const formJson = Object.fromEntries(
(formData as any).entries()
);
const filepath = formJson.filepath;
if (addFile) {
addFile(filepath);
}
handleClose();
},
}}
>
<DialogTitle>{lang.menu.add}</DialogTitle>
<DialogContent>
<DialogContentText>
{lang.menu.addPrompt.message}
</DialogContentText>
<TextField
autoFocus
required
margin="dense"
id="filepath"
name="filepath"
label={lang.menu.addPrompt.label}
type="text"
fullWidth
variant="filled"
defaultValue={defaultPath}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>{lang.menu.cancel}</Button>
<Button type="submit">{lang.menu.add}</Button>
</DialogActions>
</Dialog>
);
}

View File

@ -0,0 +1,166 @@
import type {
Directory,
PlaygroundExplorerLang,
SetState,
} from "@utils/playground";
import React, { useState } from "react";
import {
GoChevronDown as ChevDown,
GoChevronRight as ChevRight,
} from "react-icons/go";
import FileElement from "./FileElement";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import AddFileDialog from "./AddFileDialog";
export default function DirElement({
name,
fullPath,
dir: currentDir,
collapsed: pCollapsed,
selectedFileName,
lang,
setSelectedFileName,
addFile,
deleteFile,
renameFile,
}: {
name: string;
fullPath: string;
dir: Directory;
collapsed?: boolean;
selectedFileName: string;
lang: PlaygroundExplorerLang;
setSelectedFileName: SetState<string>;
addFile: (name: string) => void;
deleteFile: (name: string) => void;
renameFile: (oldName: string, newName: string) => void;
}) {
const [collapsed, setCollapsed] = useState(pCollapsed ?? false);
const chevStyles: React.CSSProperties = {
marginBottom: "-2px",
};
const hasChildren =
Object.keys(currentDir.dirs ?? {}).length > 0 ||
Object.keys(currentDir.files ?? {}).length > 0;
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const contextOpen = Boolean(anchorEl);
const handleContext = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
setAnchorEl(event.currentTarget);
};
const handleContextClose = () => {
setAnchorEl(null);
};
const [addOpen, setAddOpen] = React.useState(false);
const handleAddClose = () => {
setAddOpen(false);
};
const onDelete = () => {
deleteFile(fullPath + "/");
};
return (
<div key={name} className="dir">
<button
style={{ display: "block" }}
onClick={() => setCollapsed(!collapsed)}
onContextMenu={handleContext}
>
{collapsed ? (
<ChevRight
aria-description="collapsed"
style={chevStyles}
/>
) : (
<ChevDown aria-description="expanded" style={chevStyles} />
)}{" "}
{name + "/" + (collapsed && hasChildren ? "..." : "")}
</button>
<Menu
anchorEl={anchorEl}
open={contextOpen}
onClose={handleContextClose}
>
<MenuItem
disabled={(fullPath + "/").startsWith("dist/")}
onClick={() => {
handleContextClose();
setAddOpen(true);
}}
>
{lang.menu.add}
</MenuItem>
<MenuItem
onClick={() => {
handleContextClose();
onDelete?.();
}}
>
{lang.menu.delete}
</MenuItem>
</Menu>
<AddFileDialog
lang={lang}
defaultPath={fullPath + "/"}
open={addOpen}
addFile={addFile}
handleClose={handleAddClose}
/>
<div style={{ marginLeft: "0.5cm" }} className="dirChildren">
{collapsed ? null : (
<div>
{Object.entries(currentDir.dirs ?? {}).map(
([dirname, dir]) => {
return (
<DirElement
key={dirname}
name={dirname}
fullPath={fullPath + "/" + dirname}
dir={dir}
selectedFileName={selectedFileName}
lang={lang}
addFile={addFile}
setSelectedFileName={
setSelectedFileName
}
deleteFile={deleteFile}
renameFile={renameFile}
/>
);
}
)}
{Object.entries(currentDir.files ?? {}).map(
([currentName, _]) => {
const currentPath =
fullPath + "/" + currentName;
return (
<FileElement
key={currentName}
fullPath={currentPath}
name={currentName}
isSelected={
selectedFileName == currentPath
}
lang={lang}
onClick={() =>
setSelectedFileName(currentPath)
}
onDelete={() => deleteFile(currentPath)}
renameFile={renameFile}
/>
);
}
)}
</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,126 @@
import Button from "@mui/material/Button";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import TextField from "@mui/material/TextField";
import type { PlaygroundExplorerLang } from "@utils/playground";
import React from "react";
import { useState } from "react";
export default function FileElement({
name,
fullPath,
isSelected,
lang,
onClick,
renameFile,
onDelete,
}: {
name: string;
fullPath: string;
isSelected: boolean;
lang: PlaygroundExplorerLang;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
renameFile?: (oldName: string, newName: string) => void;
onDelete?: React.MouseEventHandler<HTMLLIElement>;
}) {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const contextOpen = Boolean(anchorEl);
const handleContext = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
setAnchorEl(event.currentTarget);
};
const handleContextClose = () => {
setAnchorEl(null);
};
const [renameOpen, setRenameOpen] = React.useState(false);
const handleRenameClose = () => {
setRenameOpen(false);
};
return (
<div className="file">
<button
onClick={onClick}
onContextMenu={handleContext}
className={"file" + (isSelected ? " selected" : "")}
>
{name}
</button>
<Menu
anchorEl={anchorEl}
open={contextOpen}
onClose={handleContextClose}
>
<MenuItem
onClick={() => {
handleContextClose();
setRenameOpen(true);
}}
disabled={fullPath.startsWith("dist/")}
>
{lang.menu.rename}
</MenuItem>
<MenuItem
onClick={(ev) => {
handleContextClose();
onDelete?.(ev);
}}
>
{lang.menu.delete}
</MenuItem>
</Menu>
<Dialog
open={renameOpen}
onClose={handleRenameClose}
PaperProps={{
component: "form",
onSubmit: (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const formJson = Object.fromEntries(
(formData as any).entries()
);
const filepath = formJson.filepath;
if (renameFile) {
renameFile(fullPath, filepath);
}
handleRenameClose();
},
}}
>
<DialogTitle>
{lang.menu.rename} - {fullPath}
</DialogTitle>
<DialogContent>
<DialogContentText>
{lang.menu.renamePrompt.message}
</DialogContentText>
<TextField
autoFocus
required
margin="dense"
id="filepath"
name="filepath"
label={lang.menu.renamePrompt.label}
type="text"
fullWidth
variant="filled"
defaultValue={fullPath}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleRenameClose}>
{lang.menu.cancel}
</Button>
<Button type="submit">{lang.menu.rename}</Button>
</DialogActions>
</Dialog>
</div>
);
}

View File

@ -0,0 +1,109 @@
import type {
Directory,
PlaygroundExplorerLang,
SetState,
} from "@utils/playground";
import DirElement from "./DirElement";
import FileElement from "./FileElement";
import MenuItem from "@mui/material/MenuItem";
import Menu from "@mui/material/Menu";
import React, { useState } from "react";
import AddFileDialog from "./AddFileDialog";
export default function FileView({
lang,
root,
selectedFileName,
setSelectedFileName,
addFile,
deleteFile,
renameFile,
className,
}: {
lang: PlaygroundExplorerLang;
root: Directory;
selectedFileName: string;
setSelectedFileName: SetState<string>;
addFile: (name: string) => void;
deleteFile: (name: string) => void;
renameFile: (oldName: string, newName: string) => void;
className?: string;
}) {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const contextOpen = Boolean(anchorEl);
const handleContext = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
setAnchorEl(event.currentTarget);
};
const handleContextClose = () => {
setAnchorEl(null);
};
const [addOpen, setAddOpen] = React.useState(false);
const handleAddClose = () => {
setAddOpen(false);
};
return (
<div className={className}>
<h3 onContextMenu={handleContext}>{lang.title}</h3>
<Menu
anchorEl={anchorEl}
open={contextOpen}
onClose={handleContextClose}
>
<MenuItem
onClick={() => {
handleContextClose();
setAddOpen(true);
}}
>
{lang.menu.add}
</MenuItem>
</Menu>
<AddFileDialog lang={lang} defaultPath="" addFile={addFile} handleClose={handleAddClose} open={addOpen} />
<div className="entries">
{Object.entries(root.dirs ?? {}).map(([name, dir]) => {
return (
<DirElement
key={name}
name={name}
fullPath={name}
dir={dir}
selectedFileName={selectedFileName}
lang={lang}
setSelectedFileName={setSelectedFileName}
addFile={addFile}
deleteFile={deleteFile}
renameFile={renameFile}
/>
);
})}
{Object.entries(root.files ?? {}).map(([name, _]) => {
const isSelected = selectedFileName == name;
return (
<span key={name}>
<FileElement
name={name}
fullPath={name}
isSelected={isSelected}
lang={lang}
onClick={
isSelected
? () => {}
: () => setSelectedFileName(name)
}
onDelete={() => deleteFile(name)}
renameFile={renameFile}
/>
</span>
);
})}
</div>
</div>
);
}

View File

@ -0,0 +1,34 @@
import type { PlaygroundHeaderLang } from "@utils/playground";
import DropdownButton from "./DropdownButton";
export default function Header({
lang,
onSave,
onReset,
onBuild,
onZip,
}: {
lang: PlaygroundHeaderLang;
onSave: () => void;
onReset: () => void;
onBuild: () => void;
onZip: () => void;
}) {
return (
<header>
<h1 id="_top">{lang.title}</h1>
<div className="buttons" style={{ height: "100%" }}>
<DropdownButton
style={{ height: "100%", marginRight: "0.5cm" }}
visible={[[lang.buttons.save, onSave]]}
options={[[lang.buttons.reset, onReset]]}
/>
<DropdownButton
style={{ height: "100%" }}
visible={[[lang.buttons.build, onBuild]]}
options={[[lang.buttons.zip, onZip]]}
/>
</div>
</header>
);
}

View File

@ -6,12 +6,13 @@ import { Steps, FileTree, Tabs, TabItem } from '@astrojs/starlight/components';
## Installation ## Installation
To get started with Shulkerscript, you need to install the Shulkerscript CLI. To get started with Shulkerscript, you need to install the Shulkerscript CLI.
You can either [download](#download-from-github) the latest release from the GitHub releases page or [build it from source](#building-from-source). You can either [download](#download-from-github) the latest release from the GitHub releases page or [build it from source](#building-from-source).
{/* :::tip :::tip
Before you install the CLI, you can try it out in your browser by using the [online playground](/playground). If you want to try out Shulkerscript without installing anything, you can use the [online playground](../../playground) right in your browser.
::: */} :::
### Quickinstall script *(recommended)* ### Quickinstall script *(recommended)*
<Steps> <Steps>

View File

@ -16,8 +16,7 @@ hero:
icon: external icon: external
--- ---
import { Card, CardGrid } from '@astrojs/starlight/components'; import { Card, LinkCard, CardGrid } from "@astrojs/starlight/components";
<CardGrid stagger> <CardGrid stagger>
<Card title="Simple Syntax" icon="seti:html"> <Card title="Simple Syntax" icon="seti:html">
@ -33,6 +32,5 @@ import { Card, CardGrid } from '@astrojs/starlight/components';
<Card title="Contribute to this project" icon="github"> <Card title="Contribute to this project" icon="github">
Contribute to [the CLI](https://github.com/moritz-hoelting/shulkerscript-cli) or [the compiler](https://github.com/moritz-hoelting/shulkerscript-lang). Contribute to [the CLI](https://github.com/moritz-hoelting/shulkerscript-cli) or [the compiler](https://github.com/moritz-hoelting/shulkerscript-lang).
</Card> </Card>
<LinkCard title="Online Playground" href="./playground" description="Get started without downloading anything" />
</CardGrid> </CardGrid>

View File

@ -0,0 +1,50 @@
---
import PlaygroundComponent from "@components/Playground";
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
import type { PlaygroundLang } from "@utils/playground";
const lang: PlaygroundLang = {
header: {
title: "Spielplatz",
buttons: {
build: "Bauen",
zip: "Als Zip herunterladen",
save: "Speichern",
reset: "Zurücksetzen",
},
},
explorer: {
title: "Dateien",
menu: {
add: "Datei hinzufügen",
addPrompt: {
label: "Dateipfad",
message: "Pfad eingeben, an dem die Datei erstellt werden soll."
},
delete: "Löschen",
rename: "Umbenennen",
renamePrompt: {
label: "Neuer Dateipfad",
message: "Pfad eingeben, zu dem die Datei umbenannt/verschoben werden soll."
},
cancel: "Abbrechen",
}
},
errorDisplay: {
title: "Fehler beim kompilieren!",
buttons: {
close: "Schließen",
},
}
};
---
<StarlightPage frontmatter={{ title: "Playground", template: "splash" }}>
<PlaygroundComponent client:only="react" {lang} />
</StarlightPage>
<style is:global>
.pagination-links {
display: none;
}
</style>

View File

@ -0,0 +1,52 @@
---
import PlaygroundComponent from "@components/Playground";
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
import type { PlaygroundLang } from "@utils/playground";
const lang: PlaygroundLang = {
header: {
title: "Playground",
buttons: {
build: "Build",
zip: "Download zip",
save: "Save",
reset: "Reset",
},
},
explorer: {
title: "Explorer",
menu: {
add: "Add file",
addPrompt: {
label: "File path",
message:
"Enter the path you want the new file to be created at.",
},
delete: "Delete",
rename: "Rename",
renamePrompt: {
label: "New file path",
message:
"Enter the path you want the file to be renamed/moved to.",
},
cancel: "Cancel",
},
},
errorDisplay: {
title: "Error during compilation!",
buttons: {
close: "Close",
},
},
};
---
<StarlightPage frontmatter={{ title: "Playground", template: "splash" }}>
<PlaygroundComponent client:only="react" {lang} />
</StarlightPage>
<style is:global>
.pagination-links {
display: none;
}
</style>

106
src/styles/playground.scss Normal file
View File

@ -0,0 +1,106 @@
.playground {
display: grid;
grid-template-columns: 1fr;
grid-template-areas:
"header"
"files"
"editor";
@media only screen and (min-width: 640px) {
grid-template-columns: clamp(200px, 15%, 500px) auto;
grid-template-areas:
"header header"
"files editor";
}
> header {
grid-area: header;
display: flex;
margin-bottom: 0.5cm;
flex-direction: column;
max-width: 95dvw;
@media only screen and (min-width: 450px) {
justify-content: space-between;
flex-direction: row;
}
}
> .file-view {
grid-area: files;
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: hidden;
max-height: 70vh;
@media only screen and (min-width: 640px) {
margin-right: 0.5cm;
}
> h3 {
cursor: pointer;
}
.entries {
overflow-y: auto;
overflow-x: hidden;
word-wrap: break-word;
button {
border: none;
width: 100%;
text-align: left;
background-color: transparent;
cursor: pointer;
&:hover {
background-color: var(--sl-color-gray-5);
}
&.selected {
color: var(--sl-color-text);
background-color: var(--sl-color-gray-6);
}
}
}
}
> .editor {
grid-area: editor;
@media only screen and (max-width: 640px) {
max-width: 100%;
}
.monaco-editor {
padding-block: 10px;
}
}
}
.error-terminal-display {
background-color: black;
padding: 15px;
border-radius: 15px;
font-size: 1.2em;
line-height: 0.8em;
--red: #ff0000;
--cyan: #00d6d6;
code {
white-space: break-spaces;
font-family: monospace;
}
}
:root[data-theme="light"] {
.error-terminal-display {
background-color: lightgray;
--red: #ff3f3f;
--cyan: #00a6a6;
--black: lightgray;
--white: #4f4f4f;
}
}

View File

@ -0,0 +1,34 @@
import { createTheme } from "@mui/material/styles";
export function customTheme(mode: "light" | "dark") {
return createTheme({
palette: {
mode,
primary: {
light: "#a700c3",
main: "#a700c3",
dark: "#a700c3",
contrastText: "#fff",
},
secondary: {
light: "#ff7961",
main: "#f44336",
dark: "#ba000d",
contrastText: "#000",
},
},
typography: {
fontFamily: [
"-apple-system",
"BlinkMacSystemFont",
'"Segoe UI"',
'"Helvetica Neue"',
"Arial",
"sans-serif",
'"Apple Color Emoji"',
'"Segoe UI Emoji"',
'"Segoe UI Symbol"',
].join(","),
},
});
}

54
src/utils/playground.ts Normal file
View File

@ -0,0 +1,54 @@
export type PlaygroundLang = {
header: PlaygroundHeaderLang;
explorer: PlaygroundExplorerLang;
errorDisplay: PlaygroundErrorDisplayLang;
};
export type PlaygroundHeaderLang = {
title: string;
buttons: {
save: string;
reset: string;
build: string;
zip: string;
};
};
export type PlaygroundExplorerLang = {
title: string;
menu: {
add: string;
addPrompt: {
message: string;
label: string;
}
rename: string;
renamePrompt: {
message: string;
label: string;
}
delete: string;
cancel: string;
}
};
export type PlaygroundErrorDisplayLang = {
title: string;
buttons: {
close: string;
}
}
export type File = {
language?: string;
content: string;
};
export type Directory = {
dirs?: { [key: string]: Directory };
files?: { [key: string]: File };
};
export type SetState<T> = React.Dispatch<React.SetStateAction<T>>;
export function isPlaygroundPage(slug: string, lang?: string): boolean {
return (
slug === (!lang || lang == "en" ? "playground" : `${lang}/playground`)
);
}

View File

@ -0,0 +1,3 @@
[build]
target = "wasm32-unknown-unknown"
target-dir = "target"

2
src/wasm/webcompiler/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/pkg

879
src/wasm/webcompiler/Cargo.lock generated Normal file
View File

@ -0,0 +1,879 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "ansi-to-html"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d73c455ae09fa2223a75114789f30ad605e9e297f79537953523366c05995f5f"
dependencies = [
"regex",
"thiserror",
]
[[package]]
name = "anyhow"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
[[package]]
name = "arbitrary"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chksum-core"
version = "0.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6db20071fdeca52ed6a7745519fb2d343fddcb93af81448373b851f072aaec5"
dependencies = [
"chksum-hash-core",
"thiserror",
]
[[package]]
name = "chksum-hash-core"
version = "0.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "221456234d441c788a2c51a27b91c4380f499de560670a67d3303e621d37b3bd"
[[package]]
name = "chksum-hash-md5"
version = "0.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80c33d01c33c9e193fe33e719a29a7eb900c08583375dd1d3269991aacbe434a"
dependencies = [
"chksum-hash-core",
"thiserror",
]
[[package]]
name = "chksum-md5"
version = "0.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95dda0f76fbb6069e042c370a928457086e1b4eabc7e75f5f49fe1b913634351"
dependencies = [
"chksum-core",
"chksum-hash-md5",
]
[[package]]
name = "colored"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8"
dependencies = [
"lazy_static",
"windows-sys",
]
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if 1.0.0",
"wasm-bindgen",
]
[[package]]
name = "crc32fast"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "derive_arbitrary"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "derive_more"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "either"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "enum-as-inner"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "flate2"
version = "1.0.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "getset"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f636605b743120a8d32ed92fc27b6cde1a769f8f936c065151eb66f88ded513c"
dependencies = [
"proc-macro-error2",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "js-sys"
version = "0.3.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.159"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
[[package]]
name = "lockfree-object-pool"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e"
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memory_units"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3"
[[package]]
name = "miniz_oxide"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
dependencies = [
"adler2",
]
[[package]]
name = "once_cell"
version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1"
dependencies = [
"portable-atomic",
]
[[package]]
name = "path-absolutize"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5"
dependencies = [
"path-dedot",
]
[[package]]
name = "path-dedot"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397"
dependencies = [
"once_cell",
]
[[package]]
name = "pathdiff"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]]
name = "pin-project-lite"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "portable-atomic"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2"
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro-error2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rustversion"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "serde_derive"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
[[package]]
name = "shulkerbox"
version = "0.1.0"
source = "git+https://github.com/moritz-hoelting/shulkerbox?rev=6e956fbe7438158c6a29c1a92d057f0f3093405a#6e956fbe7438158c6a29c1a92d057f0f3093405a"
dependencies = [
"chksum-md5",
"getset",
"serde",
"serde_json",
"tracing",
]
[[package]]
name = "shulkerscript"
version = "0.1.0"
source = "git+https://github.com/moritz-hoelting/shulkerscript-lang.git?rev=a9a8aff13b0ad0986ee1bdf3d44b74676385dfcd#a9a8aff13b0ad0986ee1bdf3d44b74676385dfcd"
dependencies = [
"chksum-md5",
"colored",
"derive_more",
"enum-as-inner",
"getset",
"itertools",
"path-absolutize",
"pathdiff",
"serde",
"shulkerbox",
"strsim",
"strum",
"strum_macros",
"thiserror",
"tracing",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "syn"
version = "2.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "toml"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "tracing"
version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
"once_cell",
]
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "wasm-bindgen"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
dependencies = [
"cfg-if 1.0.0",
"once_cell",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
[[package]]
name = "webcompiler"
version = "0.1.0"
dependencies = [
"ansi-to-html",
"anyhow",
"base64",
"cfg-if 1.0.0",
"colored",
"console_error_panic_hook",
"serde",
"serde-wasm-bindgen",
"shulkerscript",
"toml",
"wasm-bindgen",
"wee_alloc",
"zip",
]
[[package]]
name = "wee_alloc"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e"
dependencies = [
"cfg-if 0.1.10",
"libc",
"memory_units",
"winapi",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "winnow"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
dependencies = [
"memchr",
]
[[package]]
name = "zip"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494"
dependencies = [
"arbitrary",
"crc32fast",
"crossbeam-utils",
"displaydoc",
"flate2",
"indexmap",
"memchr",
"thiserror",
"zopfli",
]
[[package]]
name = "zopfli"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946"
dependencies = [
"bumpalo",
"crc32fast",
"lockfree-object-pool",
"log",
"once_cell",
"simd-adler32",
]

View File

@ -0,0 +1,30 @@
[package]
name = "webcompiler"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[profile.release]
opt-level = "z"
[features]
wee_alloc = ["dep:wee_alloc"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ansi-to-html = "0.2.1"
anyhow = "1.0.86"
base64 = "0.22.1"
cfg-if = "1.0.0"
colored = "2.1.0"
console_error_panic_hook = "0.1.7"
serde = "1.0"
serde-wasm-bindgen = "0.6.5"
shulkerscript = { git = "https://github.com/moritz-hoelting/shulkerscript-lang.git", default-features = false, features = ["serde", "shulkerbox"], rev = "a9a8aff13b0ad0986ee1bdf3d44b74676385dfcd" }
toml = "0.8.19"
wasm-bindgen = "0.2.93"
wee_alloc = { version = "0.4.5", optional = true }
zip = { version = "2.1.3", default-features = false, features = ["deflate"] }

View File

@ -0,0 +1,102 @@
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use shulkerscript::shulkerbox::virtual_fs::{VFile, VFolder};
#[allow(dead_code)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct File {
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) language: Option<String>,
pub(crate) content: String,
}
impl File {
pub fn with_lang(self, lang: String) -> Self {
Self {
language: Some(lang),
..self
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Directory {
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) dirs: Option<BTreeMap<String, Directory>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) files: Option<BTreeMap<String, File>>,
}
impl From<Directory> for VFolder {
fn from(value: Directory) -> Self {
let mut folder = VFolder::new();
if let Some(dirs) = value.dirs {
for (name, dir) in dirs {
folder.add_existing_folder(&name, dir.into())
}
}
if let Some(files) = value.files {
for (name, file) in files {
folder.add_file(&name, file.into());
}
}
folder
}
}
impl From<File> for VFile {
fn from(value: File) -> Self {
VFile::Text(value.content)
}
}
impl From<VFolder> for Directory {
fn from(value: VFolder) -> Self {
let mut dirs = BTreeMap::new();
let mut files = BTreeMap::new();
for (name, item) in value.get_folders() {
dirs.insert(name.to_string(), item.clone().into());
}
for (name, item) in value.get_files() {
files.insert(
name.to_string(),
File::from(item.clone()).correct_lang(name),
);
}
Self {
dirs: Some(dirs),
files: Some(files),
}
}
}
impl From<VFile> for File {
fn from(value: VFile) -> Self {
let content = match value {
VFile::Text(content) => content,
VFile::Binary(bin) => String::from_utf8_lossy(&bin).to_string(),
};
Self {
content,
language: None,
}
}
}
impl File {
pub fn correct_lang(self, name: &str) -> Self {
let language = match name.split('.').last() {
Some("shu") => Some("shulkerscript".to_string()),
Some("mcfunction") => Some("mcfunction".to_string()),
Some("json" | "mcmeta") => Some("json".to_string()),
_ => None,
};
Self { language, ..self }
}
}

View File

@ -0,0 +1,165 @@
use std::{
fmt::Display,
io::{Cursor, Write},
path::PathBuf,
sync::Mutex,
};
use anyhow::Result;
use base64::prelude::*;
use fs::Directory;
use shulkerscript::{
base::Handler,
shulkerbox::virtual_fs::{VFile, VFolder},
};
use wasm_bindgen::prelude::*;
use zip::{write::SimpleFileOptions, ZipWriter};
mod fs;
mod pack_toml;
cfg_if::cfg_if! {
if #[cfg(feature = "wee_alloc")] {
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
}
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
#[wasm_bindgen(js_namespace = console, js_name = error)]
fn log_err(s: &str);
#[wasm_bindgen(js_namespace = ["window", "playground"], js_name = showError)]
fn show_err(s: &str);
}
/// Compiles the given directory into datapack files.
#[wasm_bindgen]
pub fn compile(root_dir: JsValue) -> JsValue {
console_error_panic_hook::set_once();
let root_dir = VFolder::from(serde_wasm_bindgen::from_value::<Directory>(root_dir).unwrap());
log("Compiling...");
if let Ok(folder) = _compile(&root_dir) {
let folder = Directory::from(folder);
serde_wasm_bindgen::to_value(&folder).unwrap()
} else {
JsValue::null()
}
}
/// Returns a base64 encoded zip file containing the compiled datapack.
#[wasm_bindgen(js_name = compileZip)]
pub fn compile_zip(root_dir: JsValue) -> Option<String> {
console_error_panic_hook::set_once();
let root_dir = VFolder::from(serde_wasm_bindgen::from_value::<Directory>(root_dir).unwrap());
let datapack = _compile(&root_dir).ok()?;
let mut buffer = Cursor::new(Vec::new());
let mut writer = ZipWriter::new(&mut buffer);
let virtual_files = datapack.flatten();
// write each file to the zip archive
for (path, file) in virtual_files {
writer.start_file(path, SimpleFileOptions::default()).ok()?;
match file {
VFile::Text(text) => {
writer.write_all(text.as_bytes()).ok()?;
}
VFile::Binary(data) => {
writer.write_all(data).ok()?;
}
}
}
writer.set_comment("Data pack created with Shulkerscript web compiler");
writer.finish().ok()?;
Some(BASE64_STANDARD.encode(buffer.into_inner()))
}
fn _compile(root_dir: &VFolder) -> Result<VFolder> {
colored::control::set_override(true);
let printer = Printer::new();
let pack_format = {
let pack_toml = root_dir.get_file("pack.toml").ok_or_else(|| {
printer.receive_str("Could not find pack.toml. Make sure it is in the root directory.");
anyhow::anyhow!("Could not find pack.toml. Make sure it is in the root directory.")
})?;
toml::from_str::<pack_toml::PackToml>(pack_toml.as_text().unwrap())
.map_err(|e| {
printer.receive_str(&format!("Error parsing pack.toml: {}", e));
anyhow::anyhow!("Error parsing pack.toml: {}", e)
})
.map(|toml| toml.pack.format)
};
let res = pack_format.and_then(|pack_format| {
shulkerscript::compile(&printer, root_dir, pack_format, &get_script_paths(root_dir))
.map_err(|e| e.into())
});
printer.display();
res
}
#[derive(Debug)]
struct Printer {
queue: Mutex<Vec<String>>,
}
impl<T: Display> Handler<T> for Printer {
fn receive<E: Into<T>>(&self, error: E) {
self.queue.lock().unwrap().push(format!("{}", error.into()));
}
fn has_received(&self) -> bool {
self.has_printed()
}
}
impl Printer {
/// Creates a new [`Printer`].
fn new() -> Self {
Self {
queue: Mutex::new(Vec::new()),
}
}
fn display(self) {
let queue = self
.queue
.into_inner()
.unwrap()
.into_iter()
.map(|el| ansi_to_html::convert(&el).unwrap())
.collect::<Vec<_>>();
show_err(&queue.join("\n\n"));
}
fn has_printed(&self) -> bool {
!self.queue.lock().unwrap().is_empty()
}
fn receive_str(&self, error: &str) {
<Printer as Handler<&str>>::receive::<&str>(self, error);
}
}
fn get_script_paths(root: &VFolder) -> Vec<(String, PathBuf)> {
root.flatten()
.into_iter()
.filter_map(|(p, _)| {
p.strip_suffix(".shu")
.and_then(|p| p.strip_prefix("src/"))
.map(|ident| (ident.to_string(), PathBuf::from(&p)))
})
.collect()
}

View File

@ -0,0 +1,15 @@
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub struct PackToml {
pub pack: Pack,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Deserialize)]
pub struct Pack {
pub name: String,
pub description: Option<String>,
pub version: Option<String>,
pub format: u8,
}