diff --git a/src/livecodes/i18n/locales/en/language-info.lokalise.json b/src/livecodes/i18n/locales/en/language-info.lokalise.json
index d5dc3b9561..7e520df184 100644
--- a/src/livecodes/i18n/locales/en/language-info.lokalise.json
+++ b/src/livecodes/i18n/locales/en/language-info.lokalise.json
@@ -228,6 +228,18 @@
"notes": "",
"translation": "EJS"
},
+ "elm.desc": {
+ "notes": "",
+ "translation": "Elm is compiled to JavaScript in the browser by a WebAssembly build of the Elm compiler."
+ },
+ "elm.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",
+ "translation": "Elm official website Elm guide LiveCodes Documentation Load starter template "
+ },
+ "elm.name": {
+ "notes": "",
+ "translation": "Elm"
+ },
"eta.desc": {
"notes": "",
"translation": "Embedded JS template engine for Node, Deno, and the browser. Lightweight, fast, and pluggable. Written in TypeScript."
diff --git a/src/livecodes/i18n/locales/en/language-info.ts b/src/livecodes/i18n/locales/en/language-info.ts
index bd1c1f5bd5..3e347c3958 100644
--- a/src/livecodes/i18n/locales/en/language-info.ts
+++ b/src/livecodes/i18n/locales/en/language-info.ts
@@ -105,6 +105,11 @@ const languageInfo = {
link: '<1><2>Official website2>1> <3> <4>LiveCodes Documentations4> 3>',
name: 'EJS',
},
+ elm: {
+ desc: 'Elm is compiled to JavaScript in the browser by a WebAssembly build of the Elm compiler.',
+ link: '<1><2>Elm official website2>1> <3><4>Elm guide4>3> <5> <6>LiveCodes Documentation6> 5> <7> <8>Load starter template8> 7>',
+ name: 'Elm',
+ },
eta: {
desc: 'Embedded JS template engine for Node, Deno, and the browser. Lightweight, fast, and pluggable. Written in TypeScript.',
link: '<1><2>Official website2>1> <3> <4>Documentation4> 3> <5> <6>LiveCodes Documentations6> 5>',
diff --git a/src/livecodes/i18n/locales/en/translation.lokalise.json b/src/livecodes/i18n/locales/en/translation.lokalise.json
index 85fa70786b..96b07a3de6 100644
--- a/src/livecodes/i18n/locales/en/translation.lokalise.json
+++ b/src/livecodes/i18n/locales/en/translation.lokalise.json
@@ -2616,6 +2616,10 @@
"notes": "",
"translation": "Diagrams Starter"
},
+ "templates.starter.elm": {
+ "notes": "",
+ "translation": "Elm Starter"
+ },
"templates.starter.fennel": {
"notes": "",
"translation": "Fennel Starter"
diff --git a/src/livecodes/i18n/locales/en/translation.ts b/src/livecodes/i18n/locales/en/translation.ts
index 74d078ece0..f4156980e9 100644
--- a/src/livecodes/i18n/locales/en/translation.ts
+++ b/src/livecodes/i18n/locales/en/translation.ts
@@ -1000,6 +1000,7 @@ const translation = {
d3: 'D3 Starter',
daisyui: 'daisyUI Starter',
diagrams: 'Diagrams Starter',
+ elm: 'Elm Starter',
fennel: 'Fennel Starter',
fsharp: 'F# Starter',
'fsharp-wasm': 'F# (Wasm) Starter',
diff --git a/src/livecodes/languages/elm/index.ts b/src/livecodes/languages/elm/index.ts
new file mode 100644
index 0000000000..c72456c3c6
--- /dev/null
+++ b/src/livecodes/languages/elm/index.ts
@@ -0,0 +1 @@
+export * from './lang-elm';
diff --git a/src/livecodes/languages/elm/lang-elm-compiler.ts b/src/livecodes/languages/elm/lang-elm-compiler.ts
new file mode 100644
index 0000000000..b9897c1fd6
--- /dev/null
+++ b/src/livecodes/languages/elm/lang-elm-compiler.ts
@@ -0,0 +1,65 @@
+import type { CompilerFunction } from '../../models';
+import { elmWasmBaseUrl } from '../../vendors';
+
+interface ElmCompiler {
+ compile: (source: string) => Promise<{ js: string; name: string }>;
+}
+
+declare const self: {
+ importScripts: (...args: string[]) => void;
+ ElmWasm: {
+ createCompiler: (options: { baseUrl: string }) => Promise;
+ /** renders the structured Elm problems as readable text */
+ formatError: (error: unknown) => string;
+ };
+ createElmCompiler: () => CompilerFunction;
+};
+
+/**
+ * Runs the compiled Elm program in the result page.
+ * Elm's output is a classic script that defines the global `Elm`, so the program is
+ * initialized here (instead of by the user's code) into a node appended to the page.
+ */
+const getMountCode = (name: string) => `
+;(function () {
+ const node =
+ document.querySelector('#livecodes-app') ||
+ document.body.appendChild(document.createElement('div'));
+ try {
+ Elm[${JSON.stringify(name)}].init({ node });
+ } catch (err) {
+ console.error('Failed to initialize Elm:', err);
+ }
+})();
+`;
+
+self.createElmCompiler = (): CompilerFunction => {
+ let compiler: Promise | undefined;
+
+ const getCompiler = () => {
+ if (!compiler) {
+ compiler = (async () => {
+ self.importScripts(elmWasmBaseUrl + 'index.umd.js');
+ return self.ElmWasm.createCompiler({ baseUrl: elmWasmBaseUrl });
+ })();
+ // do not keep a failed boot (e.g. a transient network error)
+ compiler.catch(() => {
+ compiler = undefined;
+ });
+ }
+ return compiler;
+ };
+
+ // the compiler (a ~10 MB WASM module plus the Elm package data) is loaded once and kept warm
+ getCompiler().catch(() => undefined);
+
+ return async (code) => {
+ if (!code.trim()) return '';
+ try {
+ const { js, name } = await (await getCompiler()).compile(code);
+ return `${js.trimEnd()}\n${getMountCode(name)}`;
+ } catch (err) {
+ throw new Error(self.ElmWasm.formatError(err));
+ }
+ };
+};
diff --git a/src/livecodes/languages/elm/lang-elm.ts b/src/livecodes/languages/elm/lang-elm.ts
new file mode 100644
index 0000000000..9f44e0016a
--- /dev/null
+++ b/src/livecodes/languages/elm/lang-elm.ts
@@ -0,0 +1,24 @@
+import { codemirrorLegacy } from '../../editor/codemirror/utils';
+import type { LanguageSpecs } from '../../models';
+import { codeMirrorBaseUrl, monacoLanguagesBaseUrl } from '../../vendors';
+
+export const elm: LanguageSpecs = {
+ name: 'elm',
+ title: 'Elm',
+ compiler: {
+ factory: (_config, baseUrl) => {
+ (self as any).importScripts(baseUrl + '{{hash:lang-elm-compiler.js}}');
+ return (self as any).createElmCompiler();
+ },
+ },
+ extensions: ['elm'],
+ editor: 'script',
+ editorSupport: {
+ monaco: { languageSupport: monacoLanguagesBaseUrl + 'elm.js' },
+ codemirror: {
+ languageSupport: async () =>
+ codemirrorLegacy((await import(codeMirrorBaseUrl + 'codemirror-lang-elm.js')).elm),
+ },
+ },
+ largeDownload: true,
+};
diff --git a/src/livecodes/languages/languages.ts b/src/livecodes/languages/languages.ts
index f87667b480..6fdc306b85 100644
--- a/src/livecodes/languages/languages.ts
+++ b/src/livecodes/languages/languages.ts
@@ -18,6 +18,7 @@ import { css } from './css';
import { diagrams } from './diagrams';
import { dot } from './dot';
import { ejs } from './ejs';
+import { elm } from './elm';
import { eta } from './eta';
import { fennel } from './fennel';
import { flow } from './flow';
@@ -142,6 +143,7 @@ export const languages: LanguageSpecs[] = [
civet,
clio,
imba,
+ elm,
rescript,
reason,
ocaml,
diff --git a/src/livecodes/templates/starter/elm-starter.ts b/src/livecodes/templates/starter/elm-starter.ts
new file mode 100644
index 0000000000..d24c469a34
--- /dev/null
+++ b/src/livecodes/templates/starter/elm-starter.ts
@@ -0,0 +1,75 @@
+import type { Template } from '../../models';
+
+export const elmStarter: Template = {
+ name: 'elm',
+ title: window.deps.translateString('templates.starter.elm', 'Elm Starter'),
+ thumbnail: 'assets/templates/elm.svg',
+ activeEditor: 'script',
+ markup: {
+ language: 'html',
+ content: '',
+ },
+ style: {
+ language: 'css',
+ content: `
+.container,
+.container button {
+ text-align: center;
+ font: 1em sans-serif;
+}
+.logo {
+ width: 100px;
+}
+`.trimStart(),
+ },
+ script: {
+ language: 'elm',
+ content: `
+module Main exposing (main)
+
+import Browser
+import Html exposing (Html, button, div, h1, img, p, span, text)
+import Html.Attributes exposing (alt, class, id, src)
+import Html.Events exposing (onClick)
+
+
+type alias Model =
+ Int
+
+
+type Msg
+ = Increment
+
+
+update : Msg -> Model -> Model
+update msg model =
+ case msg of
+ Increment ->
+ model + 1
+
+
+view : Model -> Html Msg
+view model =
+ div [ class "container" ]
+ [ h1 []
+ [ text "Hello, "
+ , span [ id "name" ] [ text "Elm" ]
+ , text "!"
+ ]
+ , img [ class "logo", alt "logo", src "{{ __livecodes_baseUrl__ }}assets/templates/elm.svg" ] []
+ , p []
+ [ text "You clicked "
+ , span [ id "counter" ] [ text (String.fromInt model) ]
+ , text " times."
+ ]
+ , button [ id "counter-button", onClick Increment ]
+ [ text "Click me" ]
+ ]
+
+
+main : Program () Model Msg
+main =
+ Browser.sandbox { init = 0, update = update, view = view }
+`.trimStart(),
+ },
+};
diff --git a/src/livecodes/templates/starter/index.ts b/src/livecodes/templates/starter/index.ts
index 4f806cc056..73a63e716d 100644
--- a/src/livecodes/templates/starter/index.ts
+++ b/src/livecodes/templates/starter/index.ts
@@ -18,6 +18,7 @@ import { csharpWasmStarter } from './csharp-wasm-starter';
import { d3Starter } from './d3-starter';
import { daisyuiStarter } from './daisyui-starter';
import { diagramsStarter } from './diagrams-starter';
+import { elmStarter } from './elm-starter';
import { fennelStarter } from './fennel-starter';
import { fsharpStarter } from './fsharp-starter';
import { fsharpWasmStarter } from './fsharp-wasm-starter';
@@ -111,6 +112,7 @@ export const starterTemplates = [
civetStarter,
clioStarter,
imbaStarter,
+ elmStarter,
rescriptStarter,
reasonStarter,
ocamlStarter,
diff --git a/src/livecodes/vendors.ts b/src/livecodes/vendors.ts
index 6ddc4a68d9..a565d2d713 100644
--- a/src/livecodes/vendors.ts
+++ b/src/livecodes/vendors.ts
@@ -134,6 +134,8 @@ export const ejsUrl = /* @__PURE__ */ getUrl('ejs@4.0.1/ejs.js');
export const elkjsBaseUrl = /* @__PURE__ */ getUrl('elkjs@0.8.2/lib/');
+export const elmWasmBaseUrl = /* @__PURE__ */ getUrl('@live-codes/elm-wasm@0.2.1/dist/');
+
export const emmetMonacoUrl = /* @__PURE__ */ getUrl('emmet-monaco-es@5.5.0/dist/emmet-monaco.js');
export const esModuleShimsPath = 'es-module-shims@1.10.0/dist/es-module-shims.js';
@@ -334,7 +336,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.2/dist/',
+ '@live-codes/monaco-languages@0.3.4/dist/',
);
export const monacoThemesBaseUrl = /* @__PURE__ */ getUrl('monaco-themes@0.4.4/themes/');
diff --git a/src/sdk/models.ts b/src/sdk/models.ts
index 079506f515..528762d247 100644
--- a/src/sdk/models.ts
+++ b/src/sdk/models.ts
@@ -128,6 +128,7 @@ export type Language =
| 'civet'
| 'clio'
| 'imba'
+ | 'elm'
| 'assemblyscript'
| 'as'
| 'python'
@@ -433,6 +434,7 @@ export type TemplateName =
| 'commonlisp'
| 'clojurescript'
| 'gleam'
+ | 'elm'
| 'tcl'
| 'markdown'
| 'assemblyscript'
diff --git a/storybook/_stories/EmbedOptions/template.ts b/storybook/_stories/EmbedOptions/template.ts
index 8cb755bbd8..28b968c83f 100644
--- a/storybook/_stories/EmbedOptions/template.ts
+++ b/storybook/_stories/EmbedOptions/template.ts
@@ -35,6 +35,7 @@ const storyDef: StoryDef = {
Civet: { props: { template: 'civet' } },
Clio: { props: { template: 'clio' } },
Imba: { props: { template: 'imba' } },
+ Elm: { props: { template: 'elm' } },
ReScript: { props: { template: 'rescript' }, storyName: 'ReScript' },
Reason: { props: { template: 'reason' } },
Ocaml: { props: { template: 'ocaml' } },
diff --git a/storybook/preact/stories/EmbedOptions/template.stories.ts b/storybook/preact/stories/EmbedOptions/template.stories.ts
index 59039bec2c..a5206392b6 100644
--- a/storybook/preact/stories/EmbedOptions/template.stories.ts
+++ b/storybook/preact/stories/EmbedOptions/template.stories.ts
@@ -74,6 +74,7 @@ export const Scheme = livecodesStory({ template: 'scheme' });
export const CommonLisp = livecodesStory({ template: 'commonlisp' });
export const ClojureScript = livecodesStory({ template: 'clojurescript' });
ClojureScript.storyName = 'ClojureScript';
+export const Elm = livecodesStory({ template: 'elm' });
export const Perl = livecodesStory({ template: 'perl' });
export const Lua = livecodesStory({ template: 'lua' });
export const LuaWasm = livecodesStory({ template: 'lua-wasm' });
diff --git a/storybook/react/stories/EmbedOptions/template.stories.ts b/storybook/react/stories/EmbedOptions/template.stories.ts
index 59039bec2c..a5206392b6 100644
--- a/storybook/react/stories/EmbedOptions/template.stories.ts
+++ b/storybook/react/stories/EmbedOptions/template.stories.ts
@@ -74,6 +74,7 @@ export const Scheme = livecodesStory({ template: 'scheme' });
export const CommonLisp = livecodesStory({ template: 'commonlisp' });
export const ClojureScript = livecodesStory({ template: 'clojurescript' });
ClojureScript.storyName = 'ClojureScript';
+export const Elm = livecodesStory({ template: 'elm' });
export const Perl = livecodesStory({ template: 'perl' });
export const Lua = livecodesStory({ template: 'lua' });
export const LuaWasm = livecodesStory({ template: 'lua-wasm' });
diff --git a/storybook/solid/src/solid.ts b/storybook/solid/src/solid.ts
index b196750b3e..8319c10a3e 100644
--- a/storybook/solid/src/solid.ts
+++ b/storybook/solid/src/solid.ts
@@ -8,10 +8,9 @@
*/
import { createComponent } from '@live-codes/solid-sdk';
-import type { Component, JSX } from 'solid-js';
-import { createPlayground } from 'livecodes';
-
import type { EmbedOptions, Playground } from 'livecodes';
+import { createPlayground } from 'livecodes';
+import type { Component, JSX } from 'solid-js';
export type { Code, Config, EmbedOptions, Language, Playground } from 'livecodes';
/**
diff --git a/storybook/solid/stories/EmbedOptions/template.stories.ts b/storybook/solid/stories/EmbedOptions/template.stories.ts
index 59039bec2c..a5206392b6 100644
--- a/storybook/solid/stories/EmbedOptions/template.stories.ts
+++ b/storybook/solid/stories/EmbedOptions/template.stories.ts
@@ -74,6 +74,7 @@ export const Scheme = livecodesStory({ template: 'scheme' });
export const CommonLisp = livecodesStory({ template: 'commonlisp' });
export const ClojureScript = livecodesStory({ template: 'clojurescript' });
ClojureScript.storyName = 'ClojureScript';
+export const Elm = livecodesStory({ template: 'elm' });
export const Perl = livecodesStory({ template: 'perl' });
export const Lua = livecodesStory({ template: 'lua' });
export const LuaWasm = livecodesStory({ template: 'lua-wasm' });
diff --git a/storybook/svelte/stories/EmbedOptions/template.stories.ts b/storybook/svelte/stories/EmbedOptions/template.stories.ts
index 59039bec2c..a5206392b6 100644
--- a/storybook/svelte/stories/EmbedOptions/template.stories.ts
+++ b/storybook/svelte/stories/EmbedOptions/template.stories.ts
@@ -74,6 +74,7 @@ export const Scheme = livecodesStory({ template: 'scheme' });
export const CommonLisp = livecodesStory({ template: 'commonlisp' });
export const ClojureScript = livecodesStory({ template: 'clojurescript' });
ClojureScript.storyName = 'ClojureScript';
+export const Elm = livecodesStory({ template: 'elm' });
export const Perl = livecodesStory({ template: 'perl' });
export const Lua = livecodesStory({ template: 'lua' });
export const LuaWasm = livecodesStory({ template: 'lua-wasm' });
diff --git a/storybook/vue/stories/EmbedOptions/template.stories.ts b/storybook/vue/stories/EmbedOptions/template.stories.ts
index 59039bec2c..a5206392b6 100644
--- a/storybook/vue/stories/EmbedOptions/template.stories.ts
+++ b/storybook/vue/stories/EmbedOptions/template.stories.ts
@@ -74,6 +74,7 @@ export const Scheme = livecodesStory({ template: 'scheme' });
export const CommonLisp = livecodesStory({ template: 'commonlisp' });
export const ClojureScript = livecodesStory({ template: 'clojurescript' });
ClojureScript.storyName = 'ClojureScript';
+export const Elm = livecodesStory({ template: 'elm' });
export const Perl = livecodesStory({ template: 'perl' });
export const Lua = livecodesStory({ template: 'lua' });
export const LuaWasm = livecodesStory({ template: 'lua-wasm' });
diff --git a/storybook/web-components/stories/EmbedOptions/template.stories.ts b/storybook/web-components/stories/EmbedOptions/template.stories.ts
index 59039bec2c..a5206392b6 100644
--- a/storybook/web-components/stories/EmbedOptions/template.stories.ts
+++ b/storybook/web-components/stories/EmbedOptions/template.stories.ts
@@ -74,6 +74,7 @@ export const Scheme = livecodesStory({ template: 'scheme' });
export const CommonLisp = livecodesStory({ template: 'commonlisp' });
export const ClojureScript = livecodesStory({ template: 'clojurescript' });
ClojureScript.storyName = 'ClojureScript';
+export const Elm = livecodesStory({ template: 'elm' });
export const Perl = livecodesStory({ template: 'perl' });
export const Lua = livecodesStory({ template: 'lua' });
export const LuaWasm = livecodesStory({ template: 'lua-wasm' });
diff --git a/vendor-licenses.md b/vendor-licenses.md
index 1cd543c5b5..3ff02207b3 100644
--- a/vendor-licenses.md
+++ b/vendor-licenses.md
@@ -74,6 +74,8 @@ EJS: [Apache License 2.0](https://github.com/mde/ejs/blob/f47d7aedd51a983e4f7304
elkjs-svg: [MIT License](https://github.com/EmilStenstrom/elkjs-svg/blob/4a8709b2407d23218904f523552c565a37764e40/LICENSE)
+Elm: [BSD-3-Clause License](https://github.com/elm/compiler/blob/1bd5b36915a38335195ca7792fe3995f53d84d5e/LICENSE)
+
emmet-monaco-es: [MIT License](https://github.com/troy351/emmet-monaco-es/blob/b578210738fb591c65fb34530d28c0d0341fbb7d/LICENSE)
ES Module Shims: [MIT License](https://github.com/guybedford/es-module-shims/blob/986733009f4c9374decd4c0348740134ff499157/LICENSE)