diff --git a/src/livecodes/i18n/locales/en/language-info.lokalise.json b/src/livecodes/i18n/locales/en/language-info.lokalise.json
index 7fe8c02dc..8db6f3dc8 100644
--- a/src/livecodes/i18n/locales/en/language-info.lokalise.json
+++ b/src/livecodes/i18n/locales/en/language-info.lokalise.json
@@ -836,6 +836,18 @@
"notes": "",
"translation": "Ruby (WASM)"
},
+ "rustWasm.desc": {
+ "notes": "###
###\n\n\n",
+ "translation": "Rust is interpreted by Miri (the Rust mid-level IR interpreter) compiled to WebAssembly, running entirely in the browser."
+ },
+ "rustWasm.link": {
+ "notes": "### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n### ###\n\n\n",
+ "translation": " Rust official website The Rust Book Learn X in Y minutes, where X=Rust LiveCodes Documentation Load starter template "
+ },
+ "rustWasm.name": {
+ "notes": "",
+ "translation": "Rust (Wasm)"
+ },
"sass.desc": {
"notes": "",
"translation": "Syntactically Awesome Style Sheets."
diff --git a/src/livecodes/i18n/locales/en/language-info.ts b/src/livecodes/i18n/locales/en/language-info.ts
index a17ee49be..d273855ed 100644
--- a/src/livecodes/i18n/locales/en/language-info.ts
+++ b/src/livecodes/i18n/locales/en/language-info.ts
@@ -360,6 +360,11 @@ const languageInfo = {
link: '<1> <2>Ruby official website2> 1> <3> <4>Ruby documentation4> 3> <5> <6>ruby.wasm website6> 5> <7><8>CRuby8>7> <9> <10>Learn X in Y minutes, where X=ruby10> 9> <11> <12>LiveCodes Documentations12> 11> <13> <14>Load starter template14> 13>',
name: 'Ruby (WASM)',
},
+ rustWasm: {
+ desc: 'Rust is interpreted by <1>Miri1> (the Rust mid-level IR interpreter) compiled to WebAssembly, running entirely in the browser.',
+ link: '<1> <2>Rust official website2> 1> <3> <4>The Rust Book4> 3> <5> <6>Learn X in Y minutes, where X=Rust6> 5> <7> <8>LiveCodes Documentation8> 7> <9> <10>Load starter template10> 9>',
+ name: 'Rust (Wasm)',
+ },
sass: {
desc: 'Syntactically Awesome Style Sheets.',
link: '<1> <2>Sass official website2> 1> <3> <4>Sass documentation4> 3> <5> <6>Sass (the indented) syntax6> 5> <7> <8>Learn X in Y minutes, where X=sass8> 7>',
diff --git a/src/livecodes/i18n/locales/en/translation.lokalise.json b/src/livecodes/i18n/locales/en/translation.lokalise.json
index a9bc5f2ba..ab7991964 100644
--- a/src/livecodes/i18n/locales/en/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/en/translation.lokalise.json
@@ -2784,6 +2784,10 @@
"notes": "",
"translation": "Ruby (Wasm) Starter"
},
+ "templates.starter.rust-wasm": {
+ "notes": "",
+ "translation": "Rust (Wasm) Starter"
+ },
"templates.starter.scheme": {
"notes": "",
"translation": "Scheme Starter"
diff --git a/src/livecodes/i18n/locales/en/translation.ts b/src/livecodes/i18n/locales/en/translation.ts
index 085d53a53..3786a4b68 100644
--- a/src/livecodes/i18n/locales/en/translation.ts
+++ b/src/livecodes/i18n/locales/en/translation.ts
@@ -1042,6 +1042,7 @@ const translation = {
riot: 'Riot.js Starter',
ruby: 'Ruby Starter',
'ruby-wasm': 'Ruby (Wasm) Starter',
+ 'rust-wasm': 'Rust (Wasm) Starter',
scheme: 'Scheme Starter',
shadcnui: 'shadcn/ui Starter',
solid: 'Solid Starter',
diff --git a/src/livecodes/languages/languages.ts b/src/livecodes/languages/languages.ts
index 2cdfec143..7ae04b272 100644
--- a/src/livecodes/languages/languages.ts
+++ b/src/livecodes/languages/languages.ts
@@ -65,6 +65,7 @@ import { richtext } from './richtext';
import { riot } from './riot';
import { ruby } from './ruby';
import { rubyWasm } from './ruby-wasm';
+import { rustWasm } from './rust-wasm';
import { scheme } from './scheme';
import { sass, scss } from './scss';
import { solid, solidTsx } from './solid';
@@ -153,6 +154,7 @@ export const languages: LanguageSpecs[] = [
phpWasm,
cpp,
cppWasm,
+ rustWasm,
zigWasm,
java,
csharpWasm,
diff --git a/src/livecodes/languages/prettier.ts b/src/livecodes/languages/prettier.ts
index 45bcc30e1..1807517b0 100644
--- a/src/livecodes/languages/prettier.ts
+++ b/src/livecodes/languages/prettier.ts
@@ -1,4 +1,10 @@
-import { prettierBaseUrl, prettierMinizincUrl, prettierPhpUrl, vendorsBaseUrl } from '../vendors';
+import {
+ prettierBaseUrl,
+ prettierMinizincUrl,
+ prettierPhpUrl,
+ prettierRustUrl,
+ vendorsBaseUrl,
+} from '../vendors';
export const prettierUrl = prettierBaseUrl + 'standalone.js';
export const parserPlugins = {
@@ -12,4 +18,5 @@ export const parserPlugins = {
minizinc: prettierMinizincUrl,
pug: vendorsBaseUrl + 'prettier/parser-pug.js',
java: vendorsBaseUrl + 'prettier/parser-java.js',
+ rust: prettierRustUrl,
};
diff --git a/src/livecodes/languages/rust-wasm/index.ts b/src/livecodes/languages/rust-wasm/index.ts
new file mode 100644
index 000000000..c813e7b57
--- /dev/null
+++ b/src/livecodes/languages/rust-wasm/index.ts
@@ -0,0 +1 @@
+export * from './lang-rust-wasm';
diff --git a/src/livecodes/languages/rust-wasm/lang-rust-wasm-script.ts b/src/livecodes/languages/rust-wasm/lang-rust-wasm-script.ts
new file mode 100644
index 000000000..c7eb7ceff
--- /dev/null
+++ b/src/livecodes/languages/rust-wasm/lang-rust-wasm-script.ts
@@ -0,0 +1,260 @@
+import { getWorkerDataURL } from '../../utils';
+import { rustWasmUrl, wasmRustcBaseUrl } from '../../vendors';
+
+// Miri cannot be interrupted once it is running, so a run that exceeds this
+// budget means the worker is discarded and a fresh one is spawned next time.
+const RUN_TIMEOUT_MS = 30_000;
+// The first run downloads ~55 MB of toolchain, so boot gets a much larger budget.
+const BOOT_TIMEOUT_MS = 5 * 60_000;
+
+interface RunResult {
+ stdout: string;
+ stderr: string;
+ exitCode: number;
+ trap?: string | null;
+ durationMs: number;
+}
+
+interface Pending {
+ resolve: (result: RunResult) => void;
+ reject: (error: Error) => void;
+ timer: ReturnType;
+}
+
+declare const window: Window & {
+ livecodes: {
+ rust: {
+ ready?: boolean;
+ init?: Promise | null;
+ run?: (
+ input?: string,
+ ) => Promise<{ output: string | null; error: string | null; exitCode: number }>;
+ loaded?: Promise;
+ input?: string;
+ output?: string | null;
+ stderr?: string | null;
+ exitCode?: number | null;
+ runner?: ReturnType;
+ };
+ };
+};
+
+function createRunner() {
+ let worker: Worker | null = null;
+ let ready: Promise | null = null;
+ let settleReady: ((error?: Error) => void) | null = null;
+ let bootTimer: ReturnType | null = null;
+ let pending: Record = {};
+ let nextId = 1;
+
+ const clearBootTimer = () => {
+ if (bootTimer !== null) {
+ clearTimeout(bootTimer);
+ bootTimer = null;
+ }
+ };
+
+ const teardown = () => {
+ clearBootTimer();
+ worker?.terminate();
+ worker = null;
+ ready = null;
+ settleReady = null;
+ };
+
+ const failAll = (error: Error) => {
+ for (const id of Object.keys(pending)) {
+ const p = pending[Number(id)];
+ clearTimeout(p.timer);
+ p.reject(error);
+ }
+ pending = {};
+ };
+
+ function onMessage(e: MessageEvent) {
+ const msg = e.data ?? {};
+ if (msg.type === 'ready') {
+ clearBootTimer();
+ settleReady?.();
+ settleReady = null;
+ } else if (msg.type === 'result') {
+ const p = pending[msg.id];
+ if (p) {
+ delete pending[msg.id];
+ clearTimeout(p.timer);
+ p.resolve(msg);
+ }
+ } else if (msg.type === 'run-error') {
+ const p = pending[msg.id];
+ if (p) {
+ delete pending[msg.id];
+ clearTimeout(p.timer);
+ p.reject(new Error(msg.message));
+ }
+ } else if (msg.type === 'error') {
+ // The runtime failed to load (network, or the browser is unsupported).
+ clearBootTimer();
+ settleReady?.(new Error(msg.message));
+ settleReady = null;
+ failAll(new Error(msg.message));
+ }
+ }
+
+ function onError(err: ErrorEvent) {
+ const error = new Error(`Rust worker crashed: ${err?.message ?? 'unknown error'}`);
+ clearBootTimer();
+ settleReady?.(error);
+ settleReady = null;
+ failAll(error);
+ teardown();
+ }
+
+ async function spawn() {
+ ready = new Promise((resolve, reject) => {
+ settleReady = (error?: Error) => (error ? reject(error) : resolve());
+ });
+ const workerUrl = getWorkerDataURL(rustWasmUrl);
+ worker = new Worker(workerUrl);
+ worker.onmessage = onMessage;
+ worker.onerror = onError;
+ // `gzipped` is true because the published package stores the artifacts with
+ // a `.gz`suffix; unpkg serves them as-is and the worker inflates them.
+ worker.postMessage({
+ type: 'init',
+ toolchain: { baseUrl: wasmRustcBaseUrl, gzipped: true },
+ });
+
+ bootTimer = setTimeout(() => {
+ teardown();
+ failAll(new Error('Timed out while loading the Rust toolchain.'));
+ }, BOOT_TIMEOUT_MS);
+ }
+
+ /** Spawn the worker if needed and resolve once the toolchain is loaded. */
+ async function ensureReady() {
+ if (!ready) await spawn();
+ await ready;
+ }
+
+ function run(code: string, input: string): Promise {
+ return ensureReady().then(
+ () =>
+ new Promise((resolve, reject) => {
+ const id = nextId++;
+ const timer = setTimeout(() => {
+ delete pending[id];
+ // Miri has no way to be preempted, so abandon the worker entirely.
+ teardown();
+ failAll(new Error('Rust execution timed out; the interpreter was restarted.'));
+ }, RUN_TIMEOUT_MS);
+ pending[id] = { resolve, reject, timer };
+ worker?.postMessage({ type: 'run', id, code, input });
+ }),
+ );
+ }
+
+ return { ensureReady, run };
+}
+
+window.livecodes.rust ??= {};
+
+const rust = window.livecodes.rust;
+rust.ready = false;
+rust.runner ??= createRunner();
+
+/** Start (once) loading the interpreter and sysroot. */
+const ensureLoaded = (): Promise => {
+ let init = rust.init;
+ if (!init) {
+ init = (async () => {
+ parent.postMessage({ type: 'loading', payload: true }, '*');
+ try {
+ await rust.runner?.ensureReady();
+ } finally {
+ parent.postMessage({ type: 'loading', payload: false }, '*');
+ }
+ })().catch((err: Error) => {
+ // Reset so a later run can retry the download.
+ rust.init = null;
+ throw err;
+ });
+ // The failure is surfaced through `run`; do not also report it unhandled.
+ init.catch(() => undefined);
+ rust.init = init;
+ }
+ return init;
+};
+
+const setResult = (output: string | null, error: string | null, exitCode: number) => {
+ rust.output = output;
+ rust.stderr = error;
+ rust.exitCode = exitCode;
+ rust.ready = true;
+
+ if (error != null) {
+ // eslint-disable-next-line no-console
+ console.error(error);
+ } else if (output != null) {
+ // eslint-disable-next-line no-console
+ console.log(output);
+ }
+ return { output, error, exitCode };
+};
+
+rust.run = async (input?: string) => {
+ let code = '';
+ document.querySelectorAll('script[type="text/rust-wasm"]').forEach((script) => {
+ code += `${script.innerHTML}\n`;
+ });
+ rust.input = input;
+
+ if (!code.trim()) return setResult(null, null, 0);
+
+ try {
+ await ensureLoaded();
+ } catch (err) {
+ return setResult(null, `Error: ${(err as Error).message}`, 1);
+ }
+
+ try {
+ const result = await rust.runner!.run(code, `${input ?? ''}`);
+ const stdout = result.stdout ?? '';
+ // Compiler diagnostics and a program's own stderr both arrive here, which is
+ // how rustc behaves natively, so they share one stream.
+ const stderr = (result.stderr ?? '').trim();
+
+ if (result.exitCode !== 0) {
+ return setResult(
+ stdout || null,
+ stderr || `Exited with code ${result.exitCode}`,
+ result.exitCode,
+ );
+ }
+
+ if (stderr) {
+ // Warnings on a successful run are worth surfacing, but are not errors.
+ // eslint-disable-next-line no-console
+ console.warn(stderr);
+ }
+ return setResult(stdout, null, 0);
+ } catch (err) {
+ return setResult(null, `Error: ${(err as Error).message}`, 1);
+ }
+};
+
+ensureLoaded();
+
+rust.loaded = new Promise((resolve) => {
+ const interval = setInterval(() => {
+ if (rust.ready) {
+ clearInterval(interval);
+ resolve();
+ }
+ }, 50);
+});
+
+window.addEventListener('load', async () => {
+ parent.postMessage({ type: 'loading', payload: true }, '*');
+ await rust.run?.(rust.input);
+ parent.postMessage({ type: 'loading', payload: false }, '*');
+});
diff --git a/src/livecodes/languages/rust-wasm/lang-rust-wasm.ts b/src/livecodes/languages/rust-wasm/lang-rust-wasm.ts
new file mode 100644
index 000000000..3dcb42cd6
--- /dev/null
+++ b/src/livecodes/languages/rust-wasm/lang-rust-wasm.ts
@@ -0,0 +1,33 @@
+import { codemirrorLegacy } from '../../editor/codemirror/utils';
+import type { LanguageSpecs } from '../../models';
+import { codeMirrorBaseUrl, monacoLanguagesBaseUrl } from '../../vendors';
+import { parserPlugins } from '../prettier';
+
+export const rustWasm: LanguageSpecs = {
+ name: 'rust-wasm',
+ title: 'Rust (Wasm)',
+ formatter: {
+ prettier: {
+ name: 'rust',
+ pluginUrls: [parserPlugins.rust],
+ },
+ },
+ compiler: {
+ factory: () => async (code) => code,
+ scripts: ({ baseUrl }) => [baseUrl + '{{hash:lang-rust-wasm-script.js}}'],
+ scriptType: 'text/rust-wasm',
+ compiledCodeLanguage: 'rust',
+ liveReload: true,
+ },
+ extensions: ['rs', 'rust', 'wasm.rs', 'rs-wasm'],
+ editor: 'script',
+ editorSupport: {
+ monaco: { languageSupport: monacoLanguagesBaseUrl + 'rust.js', language: 'rust' },
+ codemirror: {
+ languageSupport: async () =>
+ codemirrorLegacy((await import(codeMirrorBaseUrl + 'codemirror-lang-rust.js')).rust),
+ },
+ codejar: { language: 'rust' },
+ },
+ largeDownload: true,
+};
diff --git a/src/livecodes/models.ts b/src/livecodes/models.ts
index 33b9fad0c..d22c84c47 100644
--- a/src/livecodes/models.ts
+++ b/src/livecodes/models.ts
@@ -99,6 +99,7 @@ export type ParserName =
| 'php'
| 'pug'
| 'java'
+ | 'rust'
| 'minizinc';
export interface PrettierParser {
@@ -193,6 +194,8 @@ export interface Compiler {
| 'text/x-uniter-php'
| 'text/php-wasm'
| 'text/cpp'
+ | 'text/rust-wasm'
+ | 'text/zig-wasm'
| 'text/java'
| 'text/csharp-wasm'
| 'text/fsharp-wasm'
@@ -204,7 +207,6 @@ export interface Compiler {
| 'text/prolog'
| 'text/minizinc'
| 'text/go-wasm'
- | 'text/zig-wasm'
| 'application/json'
| 'application/lua'
| 'text/fennel'
@@ -247,6 +249,8 @@ export type TemplateAlias =
| 'fs'
| 'f#-wasm'
| 'fs-wasm'
+ | 'rust'
+ | 'rs'
| 'pl'
| 'lisp'
| 'cljs'
diff --git a/src/livecodes/templates/starter/index.ts b/src/livecodes/templates/starter/index.ts
index 7ffaa6b4a..dd6711e6c 100644
--- a/src/livecodes/templates/starter/index.ts
+++ b/src/livecodes/templates/starter/index.ts
@@ -58,6 +58,7 @@ import { rescriptStarter } from './rescript-starter';
import { riotStarter } from './riot-starter';
import { rubyStarter } from './ruby-starter';
import { rubyWasmStarter } from './ruby-wasm-starter';
+import { rustWasmStarter } from './rust-wasm-starter';
import { schemeStarter } from './scheme-starter';
import { shadcnuiStarter } from './shadcn-ui-starter';
import { solidStarter } from './solid-starter';
@@ -121,6 +122,7 @@ export const starterTemplates = [
phpWasmStarter,
cppStarter,
cppWasmStarter,
+ rustWasmStarter,
zigWasmStarter,
javaStarter,
csharpWasmStarter,
diff --git a/src/livecodes/templates/starter/rust-wasm-starter.ts b/src/livecodes/templates/starter/rust-wasm-starter.ts
new file mode 100644
index 000000000..38c648bcd
--- /dev/null
+++ b/src/livecodes/templates/starter/rust-wasm-starter.ts
@@ -0,0 +1,93 @@
+import type { Template } from '../../models';
+
+export const rustWasmStarter: Template = {
+ name: 'rust-wasm',
+ aliases: ['rust', 'rs'],
+ title: window.deps.translateString('templates.starter.rust-wasm', 'Rust (Wasm) Starter'),
+ thumbnail: 'assets/templates/rust.svg',
+ activeEditor: 'script',
+ markup: {
+ language: 'html',
+ content: `
+
+
Hello, World!
+

+
You clicked 0 times.
+
+
+
+
+`.trimStart(),
+ },
+ style: {
+ language: 'css',
+ content: `
+.container,
+.container button {
+ text-align: center;
+ font: 1em sans-serif;
+}
+.logo {
+ width: 150px;
+}
+`.trimStart(),
+ },
+ script: {
+ language: 'rust-wasm',
+ content: `
+use std::io::BufRead;
+
+fn main() {
+ let title = "Rust";
+ println!("{title}");
+
+ let mut input = String::new();
+ let bytes = std::io::stdin().lock().read_line(&mut input).unwrap();
+ let count: i64 = if bytes == 0 {
+ 0
+ } else {
+ input.trim().parse().unwrap_or(0)
+ };
+
+ println!("{}", count + 1);
+}
+`.trimStart(),
+ },
+};
diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts
index 7aeab4c9c..1556615cf 100644
--- a/src/livecodes/vendors.ts
+++ b/src/livecodes/vendors.ts
@@ -324,7 +324,7 @@ export const monacoBaseUrl = /* @__PURE__ */ getUrl('@live-codes/monaco-editor@0
export const monacoEmacsUrl = /* @__PURE__ */ getUrl('monaco-emacs@0.3.0/dist/monaco-emacs.js');
export const monacoLanguagesBaseUrl = /* @__PURE__ */ getUrl(
- '@live-codes/monaco-languages@0.3.1/dist/',
+ '@live-codes/monaco-languages@0.3.2/dist/',
);
export const monacoThemesBaseUrl = /* @__PURE__ */ getUrl('monaco-themes@0.4.4/themes/');
@@ -369,6 +369,10 @@ export const prettierMinizincUrl = /* @__PURE__ */ getUrl(
'@live-codes/prettier-plugin-minizinc@0.2.0/dist/standalone.js',
);
+export const prettierRustUrl = /* @__PURE__ */ getUrl(
+ '@live-codes/prettier-plugin-rust@0.2.0/index.global.js',
+);
+
export const prettierPhpUrl = /* @__PURE__ */ getUrl('@prettier/plugin-php@0.22.2/standalone.js');
export const prismBaseUrl = /* @__PURE__ */ getUrl('prismjs@1.29.0/components/');
@@ -429,6 +433,11 @@ export const rubyWasmScriptUrl = /* @__PURE__ */ getUrl(
'@ruby/wasm-wasi@2.7.2/dist/browser.umd.js',
);
+export const rustWasmUrl = /* @__PURE__ */ getUrl(
+ '@live-codes/rust-wasm@0.3.0/dist/worker.iife.js',
+);
+export const wasmRustcBaseUrl = /* @__PURE__ */ getUrl('@live-codes/wasm-rustc@0.2.0/');
+
export const snackbarUrl = /* @__PURE__ */ getUrl('@snackbar/core@1.7.0/dist/snackbar.css');
export const spacingJsUrl = /* @__PURE__ */ getUrl('spacingjs@1.0.7/dist/bundle.js');
diff --git a/src/sdk/models.ts b/src/sdk/models.ts
index 1ac43e0ab..3fbce38be 100644
--- a/src/sdk/models.ts
+++ b/src/sdk/models.ts
@@ -188,6 +188,11 @@ export type Language =
| 'fs-wasm'
| 'wasm.fs'
| 'wasm.fsx'
+ | 'rust'
+ | 'rs'
+ | 'rust-wasm'
+ | 'rs-wasm'
+ | 'wasm.rs'
| 'perl'
| 'pl'
| 'pm'
@@ -407,6 +412,7 @@ export type TemplateName =
| 'csharp-wasm'
| 'fsharp'
| 'fsharp-wasm'
+ | 'rust-wasm'
| 'zig-wasm'
| 'perl'
| 'lua'
diff --git a/storybook/_stories/EmbedOptions/template.ts b/storybook/_stories/EmbedOptions/template.ts
index fe77280d5..2f6d51305 100644
--- a/storybook/_stories/EmbedOptions/template.ts
+++ b/storybook/_stories/EmbedOptions/template.ts
@@ -47,6 +47,8 @@ const storyDef: StoryDef = {
PHPWasm: { props: { template: 'php-wasm' }, storyName: 'PHP Wasm' },
Cpp: { props: { template: 'cpp' } },
CppWasm: { props: { template: 'cpp-wasm' } },
+ RustWasm: { props: { template: 'rust-wasm' } },
+ ZigWasm: { props: { template: 'zig-wasm' } },
Java: { props: { template: 'java' } },
CSharpWasm: { props: { template: 'csharp-wasm' } },
FSharp: { props: { template: 'fsharp' } },
diff --git a/vendor-licenses.md b/vendor-licenses.md
index b12fd8267..faae2c537 100644
--- a/vendor-licenses.md
+++ b/vendor-licenses.md
@@ -28,6 +28,8 @@ BiwaScheme: [MIT License](https://github.com/biwascheme/biwascheme/blob/7a95e757
Blockly: [Apache-2.0 License](https://github.com/google/blockly/blob/3ae4a618429c87fc002e512e5a2504af382325fd/LICENSE)
+browser_wasi_shim: [MIT License](https://github.com/bjorn3/browser_wasi_shim/blob/b068ec2c22d68581c48f2592f8cca1681bf71a98/LICENSE-MIT) OR [Apache License 2.0](https://github.com/bjorn3/browser_wasi_shim/blob/b068ec2c22d68581c48f2592f8cca1681bf71a98/LICENSE-APACHE)
+
BrowserFS: [MIT License](https://github.com/jvilk/BrowserFS/blob/76fd5122fcf3ad6bff3315550aafb041cfb6a72e/license.md)
brython: [BSD-3-Clause license](https://github.com/brython-dev/brython/blob/c579e26d7e24c37c77f00fc345af0248ca6be8eb/LICENCE.txt)
@@ -156,6 +158,8 @@ MDX: [MIT License](https://github.com/mdx-js/mdx/blob/7fd1d9a4272754951e70dbaecf
minizinc-js: [MPL-2.0 License](https://github.com/MiniZinc/minizinc-js/blob/3f7c34f0549195e5a66cf0f2d6f34cb5bce867f4/LICENSE)
+Miri: [MIT License](https://github.com/rust-lang/miri/blob/92ea60bf0effd37aea533cd19c9702c43b90abe7/LICENSE-MIT) OR [Apache License 2.0](https://github.com/rust-lang/miri/blob/92ea60bf0effd37aea533cd19c9702c43b90abe7/LICENSE-APACHE)
+
MJML: [MIT License](https://github.com/mjmlio/mjml/blob/988819de3375867c09585d28f555166b97415200/LICENSE.md)
Monaco-editor: [MIT License](https://github.com/microsoft/monaco-editor/blob/f849d3f2653d1097652a7d9e1d01d242cc225da8/LICENSE.md)
@@ -246,6 +250,8 @@ Riot: [MIT License](https://github.com/riot/riot/blob/2b08ebf8c7fa3f338d24b7320e
ruby.wasm: [MIT License](https://github.com/ruby/ruby.wasm/blob/097b7ca8d2ed2a98eea4dcf2da8089bb8ff06e07/LICENSE)
+Rust (compiler and standard library): [MIT License](https://github.com/rust-lang/rust/blob/018018e881e2db0956f229dbb543e21f058d1ce7/LICENSE-MIT) OR [Apache License 2.0](https://github.com/rust-lang/rust/blob/018018e881e2db0956f229dbb543e21f058d1ce7/LICENSE-APACHE)
+
Sass.js: [MIT License](https://github.com/medialize/sass.js/blob/71d9bed2cad10969efda9905aa1bddacc480f372/LICENSE)
SnackBar: [MIT License](https://github.com/egoist/snackbar/blob/4bc2fb7afd32d53a39661418fa5189dbb6e4aa86/LICENSE)