commit 0c60a5877949c168801eda602487895703bf7296 Author: Martijn Remmen Date: Sat Jul 13 11:28:36 2024 +0200 initial commit diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..da7701a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: test + +on: + push: + branches: + - master + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: "26.0.2" + gleam-version: "1.3.2" + rebar3-version: "3" + # elixir-version: "1.15.4" + - run: gleam deps download + - run: gleam test + - run: gleam format --check src test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..599be4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/README.md b/README.md new file mode 100644 index 0000000..02b0bef --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# app + +[![Package Version](https://img.shields.io/hexpm/v/app)](https://hex.pm/packages/app) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/app/) + +```sh +gleam add app@1 +``` +```gleam +import app + +pub fn main() { + // TODO: An example of the project in use +} +``` + +Further documentation can be found at . + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +``` diff --git a/gleam.toml b/gleam.toml new file mode 100644 index 0000000..83c4701 --- /dev/null +++ b/gleam.toml @@ -0,0 +1,24 @@ +name = "app" +version = "1.0.0" +target = "javascript" + +# Fill out these fields if you intend to generate HTML documentation or publish +# your project to the Hex package manager. +# +# description = "" +# licences = ["Apache-2.0"] +# repository = { type = "github", user = "", repo = "" } +# links = [{ title = "Website", href = "" }] +# +# For a full reference of all the available options, you can have a look at +# https://gleam.run/writing-gleam/gleam-toml/. + +[dependencies] +gleam_stdlib = ">= 0.34.0 and < 2.0.0" +gleam_json = "1.0.1" +lustre = ">= 4.3.0 and < 5.0.0" +lustre_http = ">= 0.5.2 and < 1.0.0" + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" +lustre_dev_tools = ">= 1.3.4 and < 2.0.0" diff --git a/index.html b/index.html new file mode 100644 index 0000000..860c03b --- /dev/null +++ b/index.html @@ -0,0 +1,17 @@ + + + + + + + 🚧 app + + + + + + + +
+ + diff --git a/manifest.toml b/manifest.toml new file mode 100644 index 0000000..3f2efe8 --- /dev/null +++ b/manifest.toml @@ -0,0 +1,51 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, + { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, + { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, + { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, + { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" }, + { name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" }, + { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" }, + { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, + { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, + { name = "gleam_fetch", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "7446410A44A1D1328F5BC1FF4FC9CBD1570479EA69349237B3F82E34521CCC10" }, + { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, + { name = "gleam_httpc", version = "2.2.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF76C71002DEECF6DC5D9CA83D962728FAE166B57926BE442D827004D3C7DF1B" }, + { name = "gleam_javascript", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "483631D3001FCE8EB12ADEAD5E1B808440038E96F93DA7A32D326C82F480C0B2" }, + { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, + { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, + { name = "gleam_package_interface", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "CF3BFC5D0997750D9550D8D73A90F4B8D71C6C081B20ED4E70FFBE1E99AFC3C2" }, + { name = "gleam_stdlib", version = "0.39.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "2D7DE885A6EA7F1D5015D1698920C9BAF7241102836CE0C3837A4F160128A9C4" }, + { name = "glearray", version = "0.2.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "9C207E05F38D724F464FA921378DB3ABC2B0A2F5821116D8BC8B2CACC68930D5" }, + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, + { name = "glint", version = "0.18.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "5FB54D7732B4105E4AF4D89A7EE6D5E8CF33DA13A3575D0C6ECE470B97958454" }, + { name = "glisten", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], otp_app = "glisten", source = "hex", outer_checksum = "CF3A9383E9BA4A8CBAF2F7B799716290D02F2AC34E7A77556B49376B662B9314" }, + { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, + { name = "logging", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "FCB111401BDB4703A440A94FF8CC7DA521112269C065F219C2766998333E7738" }, + { name = "lustre", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "43642C0602D3E2D6FEC3E24173D68A1F8E646969B53A2B0A5EB61238DDA739C4" }, + { name = "lustre_dev_tools", version = "1.3.4", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "glisten", "mist", "simplifile", "spinner", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "FB056F18870EA7FE2A070264A598CFCDB8F6F24D65FF989F18F3F46C9ABEEE31" }, + { name = "lustre_http", version = "0.5.2", build_tools = ["gleam"], requirements = ["gleam_fetch", "gleam_http", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_http", source = "hex", outer_checksum = "FB0478CBFA6B16DBE8ECA326DAE2EC15645E04900595EF2C4F039ABFA0512ABA" }, + { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" }, + { name = "mist", version = "1.2.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "109B4D64E68C104CC23BB3CC5441ECD479DD7444889DA01113B75C6AF0F0E17B" }, + { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, + { name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" }, + { name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" }, + { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" }, + { name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" }, + { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, + { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, + { name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" }, + { name = "wisp", version = "0.14.0", build_tools = ["gleam"], requirements = ["exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "9F5453AF1F9275E6F8707BC815D6A6A9DF41551921B16FBDBA52883773BAE684" }, +] + +[requirements] +gleam_json = { version = "1.0.1" } +gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +lustre = { version = ">= 4.3.0 and < 5.0.0" } +lustre_dev_tools = { version = ">= 1.3.4 and < 2.0.0" } +lustre_http = { version = ">= 0.5.2 and < 1.0.0"} diff --git a/priv/static/app.mjs b/priv/static/app.mjs new file mode 100644 index 0000000..ee9e0db --- /dev/null +++ b/priv/static/app.mjs @@ -0,0 +1,771 @@ +// build/dev/javascript/prelude.mjs +var CustomType = class { + withFields(fields) { + let properties = Object.keys(this).map( + (label) => label in fields ? fields[label] : this[label] + ); + return new this.constructor(...properties); + } +}; +var List = class { + static fromArray(array3, tail) { + let t = tail || new Empty(); + for (let i = array3.length - 1; i >= 0; --i) { + t = new NonEmpty(array3[i], t); + } + return t; + } + [Symbol.iterator]() { + return new ListIterator(this); + } + toArray() { + return [...this]; + } + // @internal + atLeastLength(desired) { + for (let _ of this) { + if (desired <= 0) + return true; + desired--; + } + return desired <= 0; + } + // @internal + hasLength(desired) { + for (let _ of this) { + if (desired <= 0) + return false; + desired--; + } + return desired === 0; + } + countLength() { + let length4 = 0; + for (let _ of this) + length4++; + return length4; + } +}; +function toList(elements, tail) { + return List.fromArray(elements, tail); +} +var ListIterator = class { + #current; + constructor(current) { + this.#current = current; + } + next() { + if (this.#current instanceof Empty) { + return { done: true }; + } else { + let { head, tail } = this.#current; + this.#current = tail; + return { value: head, done: false }; + } + } +}; +var Empty = class extends List { +}; +var NonEmpty = class extends List { + constructor(head, tail) { + super(); + this.head = head; + this.tail = tail; + } +}; +var Result = class _Result extends CustomType { + // @internal + static isResult(data) { + return data instanceof _Result; + } +}; +var Ok = class extends Result { + constructor(value) { + super(); + this[0] = value; + } + // @internal + isOk() { + return true; + } +}; +var Error = class extends Result { + constructor(detail) { + super(); + this[0] = detail; + } + // @internal + isOk() { + return false; + } +}; +function makeError(variant, module, line, fn, message, extra) { + let error = new globalThis.Error(message); + error.gleam_error = variant; + error.module = module; + error.line = line; + error.fn = fn; + for (let k in extra) + error[k] = extra[k]; + return error; +} + +// build/dev/javascript/gleam_stdlib/gleam/option.mjs +var None = class extends CustomType { +}; + +// build/dev/javascript/gleam_stdlib/dict.mjs +var tempDataView = new DataView(new ArrayBuffer(8)); +var SHIFT = 5; +var BUCKET_SIZE = Math.pow(2, SHIFT); +var MASK = BUCKET_SIZE - 1; +var MAX_INDEX_NODE = BUCKET_SIZE / 2; +var MIN_ARRAY_NODE = BUCKET_SIZE / 4; + +// build/dev/javascript/gleam_stdlib/gleam_stdlib.mjs +function to_string3(term) { + return term.toString(); +} +var unicode_whitespaces = [ + " ", + // Space + " ", + // Horizontal tab + "\n", + // Line feed + "\v", + // Vertical tab + "\f", + // Form feed + "\r", + // Carriage return + "\x85", + // Next line + "\u2028", + // Line separator + "\u2029" + // Paragraph separator +].join(); +var left_trim_regex = new RegExp(`^([${unicode_whitespaces}]*)`, "g"); +var right_trim_regex = new RegExp(`([${unicode_whitespaces}]*)$`, "g"); + +// build/dev/javascript/gleam_stdlib/gleam/int.mjs +function to_string2(x) { + return to_string3(x); +} + +// build/dev/javascript/gleam_stdlib/gleam/bool.mjs +function guard(requirement, consequence, alternative) { + if (requirement) { + return consequence; + } else { + return alternative(); + } +} + +// build/dev/javascript/lustre/lustre/effect.mjs +var Effect = class extends CustomType { + constructor(all) { + super(); + this.all = all; + } +}; +function none() { + return new Effect(toList([])); +} + +// build/dev/javascript/lustre/lustre/internals/vdom.mjs +var Text = class extends CustomType { + constructor(content) { + super(); + this.content = content; + } +}; +var Element = class extends CustomType { + constructor(key, namespace, tag, attrs, children, self_closing, void$) { + super(); + this.key = key; + this.namespace = namespace; + this.tag = tag; + this.attrs = attrs; + this.children = children; + this.self_closing = self_closing; + this.void = void$; + } +}; +var Event = class extends CustomType { + constructor(x0, x1) { + super(); + this[0] = x0; + this[1] = x1; + } +}; + +// build/dev/javascript/lustre/lustre/attribute.mjs +function on(name, handler) { + return new Event("on" + name, handler); +} + +// build/dev/javascript/lustre/lustre/element.mjs +function element(tag, attrs, children) { + if (tag === "area") { + return new Element("", "", tag, attrs, toList([]), false, true); + } else if (tag === "base") { + return new Element("", "", tag, attrs, toList([]), false, true); + } else if (tag === "br") { + return new Element("", "", tag, attrs, toList([]), false, true); + } else if (tag === "col") { + return new Element("", "", tag, attrs, toList([]), false, true); + } else if (tag === "embed") { + return new Element("", "", tag, attrs, toList([]), false, true); + } else if (tag === "hr") { + return new Element("", "", tag, attrs, toList([]), false, true); + } else if (tag === "img") { + return new Element("", "", tag, attrs, toList([]), false, true); + } else if (tag === "input") { + return new Element("", "", tag, attrs, toList([]), false, true); + } else if (tag === "link") { + return new Element("", "", tag, attrs, toList([]), false, true); + } else if (tag === "meta") { + return new Element("", "", tag, attrs, toList([]), false, true); + } else if (tag === "param") { + return new Element("", "", tag, attrs, toList([]), false, true); + } else if (tag === "source") { + return new Element("", "", tag, attrs, toList([]), false, true); + } else if (tag === "track") { + return new Element("", "", tag, attrs, toList([]), false, true); + } else if (tag === "wbr") { + return new Element("", "", tag, attrs, toList([]), false, true); + } else { + return new Element("", "", tag, attrs, children, false, false); + } +} +function text(content) { + return new Text(content); +} + +// build/dev/javascript/lustre/lustre/internals/runtime.mjs +var Debug = class extends CustomType { + constructor(x0) { + super(); + this[0] = x0; + } +}; +var Dispatch = class extends CustomType { + constructor(x0) { + super(); + this[0] = x0; + } +}; +var Shutdown = class extends CustomType { +}; +var ForceModel = class extends CustomType { + constructor(x0) { + super(); + this[0] = x0; + } +}; + +// build/dev/javascript/lustre/vdom.ffi.mjs +function morph(prev, next, dispatch, isComponent = false) { + let out; + let stack = [{ prev, next, parent: prev.parentNode }]; + while (stack.length) { + let { prev: prev2, next: next2, parent } = stack.pop(); + if (next2.subtree !== void 0) + next2 = next2.subtree(); + if (next2.content !== void 0) { + if (!prev2) { + const created = document.createTextNode(next2.content); + parent.appendChild(created); + out ??= created; + } else if (prev2.nodeType === Node.TEXT_NODE) { + if (prev2.textContent !== next2.content) + prev2.textContent = next2.content; + out ??= prev2; + } else { + const created = document.createTextNode(next2.content); + parent.replaceChild(created, prev2); + out ??= created; + } + } else if (next2.tag !== void 0) { + const created = createElementNode({ + prev: prev2, + next: next2, + dispatch, + stack, + isComponent + }); + if (!prev2) { + parent.appendChild(created); + } else if (prev2 !== created) { + parent.replaceChild(created, prev2); + } + out ??= created; + } else if (next2.elements !== void 0) { + iterateElement(next2, (fragmentElement) => { + stack.unshift({ prev: prev2, next: fragmentElement, parent }); + prev2 = prev2?.nextSibling; + }); + } else if (next2.subtree !== void 0) { + stack.push({ prev: prev2, next: next2, parent }); + } + } + return out; +} +function createElementNode({ prev, next, dispatch, stack }) { + const namespace = next.namespace || "http://www.w3.org/1999/xhtml"; + const canMorph = prev && prev.nodeType === Node.ELEMENT_NODE && prev.localName === next.tag && prev.namespaceURI === (next.namespace || "http://www.w3.org/1999/xhtml"); + const el2 = canMorph ? prev : namespace ? document.createElementNS(namespace, next.tag) : document.createElement(next.tag); + let handlersForEl; + if (!registeredHandlers.has(el2)) { + const emptyHandlers = /* @__PURE__ */ new Map(); + registeredHandlers.set(el2, emptyHandlers); + handlersForEl = emptyHandlers; + } else { + handlersForEl = registeredHandlers.get(el2); + } + const prevHandlers = canMorph ? new Set(handlersForEl.keys()) : null; + const prevAttributes = canMorph ? new Set(Array.from(prev.attributes, (a) => a.name)) : null; + let className = null; + let style = null; + let innerHTML = null; + for (const attr of next.attrs) { + const name = attr[0]; + const value = attr[1]; + if (attr.as_property) { + if (el2[name] !== value) + el2[name] = value; + if (canMorph) + prevAttributes.delete(name); + } else if (name.startsWith("on")) { + const eventName = name.slice(2); + const callback = dispatch(value); + if (!handlersForEl.has(eventName)) { + el2.addEventListener(eventName, lustreGenericEventHandler); + } + handlersForEl.set(eventName, callback); + if (canMorph) + prevHandlers.delete(eventName); + } else if (name.startsWith("data-lustre-on-")) { + const eventName = name.slice(15); + const callback = dispatch(lustreServerEventHandler); + if (!handlersForEl.has(eventName)) { + el2.addEventListener(eventName, lustreGenericEventHandler); + } + handlersForEl.set(eventName, callback); + el2.setAttribute(name, value); + } else if (name === "class") { + className = className === null ? value : className + " " + value; + } else if (name === "style") { + style = style === null ? value : style + value; + } else if (name === "dangerous-unescaped-html") { + innerHTML = value; + } else { + if (el2.getAttribute(name) !== value) + el2.setAttribute(name, value); + if (name === "value" || name === "selected") + el2[name] = value; + if (canMorph) + prevAttributes.delete(name); + } + } + if (className !== null) { + el2.setAttribute("class", className); + if (canMorph) + prevAttributes.delete("class"); + } + if (style !== null) { + el2.setAttribute("style", style); + if (canMorph) + prevAttributes.delete("style"); + } + if (canMorph) { + for (const attr of prevAttributes) { + el2.removeAttribute(attr); + } + for (const eventName of prevHandlers) { + handlersForEl.delete(eventName); + el2.removeEventListener(eventName, lustreGenericEventHandler); + } + } + if (next.key !== void 0 && next.key !== "") { + el2.setAttribute("data-lustre-key", next.key); + } else if (innerHTML !== null) { + el2.innerHTML = innerHTML; + return el2; + } + let prevChild = el2.firstChild; + let seenKeys = null; + let keyedChildren = null; + let incomingKeyedChildren = null; + let firstChild = next.children[Symbol.iterator]().next().value; + if (canMorph && firstChild !== void 0 && // Explicit checks are more verbose but truthy checks force a bunch of comparisons + // we don't care about: it's never gonna be a number etc. + firstChild.key !== void 0 && firstChild.key !== "") { + seenKeys = /* @__PURE__ */ new Set(); + keyedChildren = getKeyedChildren(prev); + incomingKeyedChildren = getKeyedChildren(next); + } + for (const child of next.children) { + iterateElement(child, (currElement) => { + if (currElement.key !== void 0 && seenKeys !== null) { + prevChild = diffKeyedChild( + prevChild, + currElement, + el2, + stack, + incomingKeyedChildren, + keyedChildren, + seenKeys + ); + } else { + stack.unshift({ prev: prevChild, next: currElement, parent: el2 }); + prevChild = prevChild?.nextSibling; + } + }); + } + while (prevChild) { + const next2 = prevChild.nextSibling; + el2.removeChild(prevChild); + prevChild = next2; + } + return el2; +} +var registeredHandlers = /* @__PURE__ */ new WeakMap(); +function lustreGenericEventHandler(event2) { + const target = event2.currentTarget; + if (!registeredHandlers.has(target)) { + target.removeEventListener(event2.type, lustreGenericEventHandler); + return; + } + const handlersForEventTarget = registeredHandlers.get(target); + if (!handlersForEventTarget.has(event2.type)) { + target.removeEventListener(event2.type, lustreGenericEventHandler); + return; + } + handlersForEventTarget.get(event2.type)(event2); +} +function lustreServerEventHandler(event2) { + const el2 = event2.currentTarget; + const tag = el2.getAttribute(`data-lustre-on-${event2.type}`); + const data = JSON.parse(el2.getAttribute("data-lustre-data") || "{}"); + const include = JSON.parse(el2.getAttribute("data-lustre-include") || "[]"); + switch (event2.type) { + case "input": + case "change": + include.push("target.value"); + break; + } + return { + tag, + data: include.reduce( + (data2, property) => { + const path = property.split("."); + for (let i = 0, o = data2, e = event2; i < path.length; i++) { + if (i === path.length - 1) { + o[path[i]] = e[path[i]]; + } else { + o[path[i]] ??= {}; + e = e[path[i]]; + o = o[path[i]]; + } + } + return data2; + }, + { data } + ) + }; +} +function getKeyedChildren(el2) { + const keyedChildren = /* @__PURE__ */ new Map(); + if (el2) { + for (const child of el2.children) { + iterateElement(child, (currElement) => { + const key = currElement?.key || currElement?.getAttribute?.("data-lustre-key"); + if (key) + keyedChildren.set(key, currElement); + }); + } + } + return keyedChildren; +} +function diffKeyedChild(prevChild, child, el2, stack, incomingKeyedChildren, keyedChildren, seenKeys) { + while (prevChild && !incomingKeyedChildren.has(prevChild.getAttribute("data-lustre-key"))) { + const nextChild = prevChild.nextSibling; + el2.removeChild(prevChild); + prevChild = nextChild; + } + if (keyedChildren.size === 0) { + iterateElement(child, (currChild) => { + stack.unshift({ prev: prevChild, next: currChild, parent: el2 }); + prevChild = prevChild?.nextSibling; + }); + return prevChild; + } + if (seenKeys.has(child.key)) { + console.warn(`Duplicate key found in Lustre vnode: ${child.key}`); + stack.unshift({ prev: null, next: child, parent: el2 }); + return prevChild; + } + seenKeys.add(child.key); + const keyedChild = keyedChildren.get(child.key); + if (!keyedChild && !prevChild) { + stack.unshift({ prev: null, next: child, parent: el2 }); + return prevChild; + } + if (!keyedChild && prevChild !== null) { + const placeholder = document.createTextNode(""); + el2.insertBefore(placeholder, prevChild); + stack.unshift({ prev: placeholder, next: child, parent: el2 }); + return prevChild; + } + if (!keyedChild || keyedChild === prevChild) { + stack.unshift({ prev: prevChild, next: child, parent: el2 }); + prevChild = prevChild?.nextSibling; + return prevChild; + } + el2.insertBefore(keyedChild, prevChild); + stack.unshift({ prev: keyedChild, next: child, parent: el2 }); + return prevChild; +} +function iterateElement(element2, processElement) { + if (element2.elements !== void 0) { + for (const currElement of element2.elements) { + processElement(currElement); + } + } else { + processElement(element2); + } +} + +// build/dev/javascript/lustre/client-runtime.ffi.mjs +var LustreClientApplication2 = class _LustreClientApplication { + #root = null; + #queue = []; + #effects = []; + #didUpdate = false; + #isComponent = false; + #model = null; + #update = null; + #view = null; + static start(flags, selector, init3, update2, view2) { + if (!is_browser()) + return new Error(new NotABrowser()); + const root2 = selector instanceof HTMLElement ? selector : document.querySelector(selector); + if (!root2) + return new Error(new ElementNotFound(selector)); + const app = new _LustreClientApplication(init3(flags), update2, view2, root2); + return new Ok((msg) => app.send(msg)); + } + constructor([model, effects], update2, view2, root2 = document.body, isComponent = false) { + this.#model = model; + this.#update = update2; + this.#view = view2; + this.#root = root2; + this.#effects = effects.all.toArray(); + this.#didUpdate = true; + this.#isComponent = isComponent; + window.requestAnimationFrame(() => this.#tick()); + } + send(action) { + switch (true) { + case action instanceof Dispatch: { + this.#queue.push(action[0]); + this.#tick(); + return; + } + case action instanceof Shutdown: { + this.#shutdown(); + return; + } + case action instanceof Debug: { + this.#debug(action[0]); + return; + } + default: + return; + } + } + emit(event2, data) { + this.#root.dispatchEvent( + new CustomEvent(event2, { + bubbles: true, + detail: data, + composed: true + }) + ); + } + #tick() { + this.#flush_queue(); + if (this.#didUpdate) { + const vdom = this.#view(this.#model); + const dispatch = (handler) => (e) => { + const result = handler(e); + if (result instanceof Ok) { + this.send(new Dispatch(result[0])); + } + }; + this.#didUpdate = false; + this.#root = morph(this.#root, vdom, dispatch, this.#isComponent); + } + } + #flush_queue(iterations = 0) { + while (this.#queue.length) { + const [next, effects] = this.#update(this.#model, this.#queue.shift()); + this.#didUpdate ||= this.#model !== next; + this.#model = next; + this.#effects = this.#effects.concat(effects.all.toArray()); + } + while (this.#effects.length) { + this.#effects.shift()( + (msg) => this.send(new Dispatch(msg)), + (event2, data) => this.emit(event2, data) + ); + } + if (this.#queue.length) { + if (iterations < 5) { + this.#flush_queue(++iterations); + } else { + window.requestAnimationFrame(() => this.#tick()); + } + } + } + #debug(action) { + switch (true) { + case action instanceof ForceModel: { + const vdom = this.#view(action[0]); + const dispatch = (handler) => (e) => { + const result = handler(e); + if (result instanceof Ok) { + this.send(new Dispatch(result[0])); + } + }; + this.#queue = []; + this.#effects = []; + this.#didUpdate = false; + this.#root = morph(this.#root, vdom, dispatch, this.#isComponent); + } + } + } + #shutdown() { + this.#root.remove(); + this.#root = null; + this.#model = null; + this.#queue = []; + this.#effects = []; + this.#didUpdate = false; + this.#update = () => { + }; + this.#view = () => { + }; + } +}; +var start = (app, selector, flags) => LustreClientApplication2.start( + flags, + selector, + app.init, + app.update, + app.view +); +var is_browser = () => globalThis.window && window.document; + +// build/dev/javascript/lustre/lustre.mjs +var App = class extends CustomType { + constructor(init3, update2, view2, on_attribute_change) { + super(); + this.init = init3; + this.update = update2; + this.view = view2; + this.on_attribute_change = on_attribute_change; + } +}; +var ElementNotFound = class extends CustomType { + constructor(selector) { + super(); + this.selector = selector; + } +}; +var NotABrowser = class extends CustomType { +}; +function application(init3, update2, view2) { + return new App(init3, update2, view2, new None()); +} +function start3(app, selector, flags) { + return guard( + !is_browser(), + new Error(new NotABrowser()), + () => { + return start(app, selector, flags); + } + ); +} + +// build/dev/javascript/lustre/lustre/element/html.mjs +function div(attrs, children) { + return element("div", attrs, children); +} +function button(attrs, children) { + return element("button", attrs, children); +} + +// build/dev/javascript/lustre/lustre/event.mjs +function on2(name, handler) { + return on(name, handler); +} +function on_click(msg) { + return on2("click", (_) => { + return new Ok(msg); + }); +} + +// build/dev/javascript/app/app.mjs +var Increment = class extends CustomType { +}; +var Decrement = class extends CustomType { +}; +function init2(_) { + return [0, none()]; +} +function update(model, msg) { + if (msg instanceof Increment) { + return [model + 1, none()]; + } else { + return [model - 1, none()]; + } +} +function view(model) { + let count = to_string2(model); + return div( + toList([]), + toList([ + button( + toList([on_click(new Increment())]), + toList([text("+")]) + ), + text(count), + button( + toList([on_click(new Decrement())]), + toList([text("-")]) + ) + ]) + ); +} +function main() { + let app = application(init2, update, view); + let $ = start3(app, "#app", void 0); + if (!$.isOk()) { + throw makeError( + "assignment_no_match", + "app", + 12, + "main", + "Assignment pattern did not match", + { value: $ } + ); + } + return void 0; +} + +// build/.lustre/entry.mjs +main(); diff --git a/src/app.gleam b/src/app.gleam new file mode 100644 index 0000000..55fd6e6 --- /dev/null +++ b/src/app.gleam @@ -0,0 +1,54 @@ +import gleam/int +import gleam/io +import lustre +import lustre/effect +import lustre/element +import lustre/element/html +import lustre/event +import lustre_http + +pub fn main() { + let app = lustre.application(init, update, view) + let assert Ok(_) = lustre.start(app, "#app", Nil) + + Nil +} + +pub type Incident { + Incident(number: String, brief_description: String) +} + +pub type Model = + List(Incident) + +pub fn init(_flags) -> #(Model, effect.Effect(Msg)) { + #(0, effect.none()) +} + +pub type Msg { + TopdeskReturnedTickets(Result(List(Incident), lustre_http.HttpError)) + UserRefreshedTickets +} + +pub fn update(model: Model, msg: Msg) -> #(Model, effect.Effect(Msg)) { + case msg { + UserRefreshedTickets -> #(model, get_tickets()) + TopdeskReturnedTickets(Ok(incidents)) -> #(incidents, effect.none()) + TopdeskReturnedTickets(Error(_)) -> #(model, effect.none()) + } +} + +pub fn get_tickets() -> effect.Effect(Msg) { + let url = "https://support.kembit.nl/" + let decoder = dynamic +} + +pub fn view(model: Model) -> element.Element(Msg) { + let count = int.to_string(model) + + html.div([], [ + html.button([event.on_click(Increment)], [element.text("+")]), + element.text(count), + html.button([event.on_click(Decrement)], [element.text("-")]), + ]) +} diff --git a/test/app_test.gleam b/test/app_test.gleam new file mode 100644 index 0000000..3831e7a --- /dev/null +++ b/test/app_test.gleam @@ -0,0 +1,12 @@ +import gleeunit +import gleeunit/should + +pub fn main() { + gleeunit.main() +} + +// gleeunit test functions end in `_test` +pub fn hello_world_test() { + 1 + |> should.equal(1) +}