-
\ No newline at end of file
diff --git a/cynthia_websites_mini_client/client.test.ts b/cynthia_websites_mini_client/client.test.ts
deleted file mode 100644
index 8aac633..0000000
--- a/cynthia_websites_mini_client/client.test.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { expect, test } from "bun:test";
-
-process.chdir(__dirname + "/..");
-
-test("client gleeunit tests (best to be ran with `bun run test client`)", () => {
- expect(
- Bun.spawnSync({
- cmd: [process.argv0, "run", "test", "client"],
- }).success,
- ).toBe(true);
-});
diff --git a/cynthia_websites_mini_client/gleam.toml b/cynthia_websites_mini_client/gleam.toml
index 08132f9..f8b203a 100644
--- a/cynthia_websites_mini_client/gleam.toml
+++ b/cynthia_websites_mini_client/gleam.toml
@@ -1,41 +1,28 @@
name = "cynthia_websites_mini_client"
+version = "1.0.0"
target = "javascript"
-gleam = ">= 1.9.0"
-version = "1.3.0"
-description = "The Cynthia Mini client."
-licences = ["AGPL-3.0"]
-repository = { type = "github", user = "CynthiaWebsiteEngine", repo = "Mini", path = "cynthia_websites_mini_client" }
-links = [
- { title = "NPM", href = "https://www.npmjs.com/package/@cynthiaweb/cynthiaweb-mini" },
- { title = "GitHub releases", href = "https://github.com/CynthiaWebsiteEngine/Mini/releases" }
-]
-
-[javascript]
-typescript_declarations = true
+# 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]
-# Shared dependencies with the server
-gleam_stdlib = "0.59.0"
-plinth = ">= 0.5.9 and < 1.0.0"
-gleam_javascript = "1.0.0"
-gleam_community_colour = "1.4.1"
-gleam_json = "2.3.0"
-gleam_http = "3.7.2"
-gleam_fetch = "1.3.0"
-
-# Lustre specific dependencies
-lustre = ">= 5.0.2 and < 6.0.0"
-rsvp = ">= 1.0.0 and < 2.0.0"
-modem = "2.0.2"
-
-
-# Other dependencies
-odysseus = ">= 1.0.0 and < 2.0.0"
-houdini = ">= 1.1.0 and < 2.0.0"
-jot = ">= 5.0.0 and < 6.0.0"
-gleam_time = ">= 1.2.0 and < 2.0.0"
-
+gleam_stdlib = ">= 0.44.0 and < 2.0.0"
+gleam_json = ">= 3.1.0 and < 4.0.0"
+plinth = ">= 0.9.2 and < 1.0.0"
+tom = "1.1.1"
+lustre = ">= 5.5.2 and < 6.0.0"
+modem = ">= 2.1.2 and < 3.0.0"
+rsvp = ">= 1.2.0 and < 2.0.0"
+gleam_fetch = ">= 1.3.0 and < 2.0.0"
+gleam_http = ">= 4.3.0 and < 5.0.0"
+chilp = { git = "https://forge.strawmelonjuice.com/strawmelonjuice/chilp.git", ref = "b43d01e6c39d12121a5cbfbf53b6b1dec8bc49f1" }
[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"
-birdie = ">= 1.2.7 and < 2.0.0"
diff --git a/cynthia_websites_mini_client/manifest.toml b/cynthia_websites_mini_client/manifest.toml
index 906711e..fe52e34 100644
--- a/cynthia_websites_mini_client/manifest.toml
+++ b/cynthia_websites_mini_client/manifest.toml
@@ -2,54 +2,34 @@
# 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 = "birdie", version = "1.3.1", build_tools = ["gleam"], requirements = ["argv", "edit_distance", "filepath", "glance", "gleam_community_ansi", "gleam_stdlib", "justin", "rank", "simplifile", "term_size", "trie_again"], otp_app = "birdie", source = "hex", outer_checksum = "F811C9EDAF920EF48597A26E788907AAF80D9239A5E8C8CCFBD0DD1BB10184D7" },
- { name = "edit_distance", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "edit_distance", source = "hex", outer_checksum = "A1E485C69A70210223E46E63985FA1008B8B2DDA9848B7897469171B29020C05" },
- { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" },
- { name = "glance", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "FAA3DAC74AF71D47C67D88EB32CE629075169F878D148BB1FF225439BE30070A" },
- { name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" },
- { name = "gleam_community_colour", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "386CB9B01B33371538672EEA8A6375A0A0ADEF41F17C86DDCB81C92AD00DA610" },
- { name = "gleam_erlang", version = "0.34.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "0C38F2A128BAA0CEF17C3000BD2097EB80634E239CE31A86400C4416A5D0FDCC" },
+ { name = "chilp", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "gleam_time", "lustre", "rsvp"], source = "git", repo = "https://forge.strawmelonjuice.com/strawmelonjuice/chilp.git", commit = "b43d01e6c39d12121a5cbfbf53b6b1dec8bc49f1" },
+ { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" },
{ name = "gleam_fetch", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "2CBF9F2E1C71AEBBFB13A9D5720CD8DB4263EB02FE60C5A7A1C6E17B0151C20C" },
- { name = "gleam_http", version = "3.7.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8A70D2F70BB7CFEB5DF048A2183FFBA91AF6D4CF5798504841744A16999E33D2" },
- { name = "gleam_httpc", version = "4.2.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "224BF35B2091502921D1623F35E6FA52815B75D99D18AEFB9DAEA0B8AEADD7A1" },
+ { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" },
+ { name = "gleam_httpc", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C545172618D07811494E97AAA4A0FB34DA6F6D0061FDC8041C2F8E3BE2B2E48F" },
{ name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" },
- { name = "gleam_json", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C55C5C2B318533A8072D221C5E06E5A75711C129E420DD1CE463342106012E5D" },
- { name = "gleam_otp", version = "0.16.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "50DA1539FC8E8FA09924EB36A67A2BBB0AD6B27BCDED5A7EF627057CF69D035E" },
- { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" },
- { name = "gleam_stdlib", version = "0.59.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F8FEE9B35797301994B81AF75508CF87C328FE1585558B0FFD188DC2B32EAA95" },
- { name = "gleam_time", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "F9AB61CE910F3071B136E1C8E214A46C406734F710D3AF75C99B00DA785902A2" },
- { name = "gleeunit", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "A7DD6C07B7DA49A6E28796058AA89E651D233B357D5607006D70619CD89DAAAB" },
- { name = "glexer", version = "2.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "5C235CBDF4DA5203AD5EAB1D6D8B456ED8162C5424FE2309CFFB7EF438B7C269" },
- { name = "houdini", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "houdini", source = "hex", outer_checksum = "5BA517E5179F132F0471CB314F27FE210A10407387DA1EA4F6FD084F74469FC2" },
- { name = "jot", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "houdini", "splitter"], otp_app = "jot", source = "hex", outer_checksum = "2C1B30CC00B0D79F904028F48229C0BB354F3C1BC05EE99D4F3D423E223D85BF" },
- { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" },
- { name = "lustre", version = "5.0.3", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "0BB69D69A9E75E675AA2C32A4A0E5086041D037829FC8AD385BA6A59E45A60A2" },
- { name = "modem", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib", "lustre"], otp_app = "modem", source = "hex", outer_checksum = "EF6B6B187E9D6425DFADA3A1AC212C01C4F34913A135DA2FF9B963EEF324C1F7" },
- { name = "odysseus", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "odysseus", source = "hex", outer_checksum = "6A97DA1075BDDEA8B60F47B1DFFAD49309FA27E73843F13A0AF32EA7087BA11C" },
- { name = "plinth", version = "0.7.1", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "63BB36AACCCCB82FBE46A862CF85CB88EBE4EF280ECDBAC4B6CB042340B9E1D8" },
- { name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" },
- { name = "rsvp", version = "1.0.4", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_fetch", "gleam_http", "gleam_httpc", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "rsvp", source = "hex", outer_checksum = "EFCA7CD53B0A8738C06E136422D1FF080DBB657C89E077F7B9DD20BFACE0A77A" },
- { name = "simplifile", version = "2.3.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0A868DAC6063D9E983477981839810DC2E553285AB4588B87E3E9C96A7FB4CB4" },
- { name = "splitter", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "128FC521EE33B0012E3E64D5B55168586BC1B9C8D7B0D0CA223B68B0D770A547" },
- { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" },
- { name = "trie_again", version = "1.1.3", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "365FE609649F3A098D1D7FC7EA5222EE422F0B3745587BF2AB03352357CA70BB" },
+ { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" },
+ { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" },
+ { name = "gleam_stdlib", version = "0.68.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F7FAEBD8EF260664E86A46C8DBA23508D1D11BB3BCC6EE1B89B3BC3E5C83FF1E" },
+ { name = "gleam_time", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "56DB0EF9433826D3B99DB0B4AF7A2BFED13D09755EC64B1DAAB46F804A9AD47D" },
+ { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" },
+ { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" },
+ { name = "lustre", version = "5.5.2", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "2DC2973D81C12E63251B636773217B8E09C5C84590A729750F6BCF009420B38E" },
+ { name = "modem", version = "2.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib", "lustre"], otp_app = "modem", source = "hex", outer_checksum = "3F9682EBCBF4D26045F1038A7507E8C7967E49D43F9CA6BA68EF0C971B195A7F" },
+ { name = "plinth", version = "0.9.2", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "A5A14C590F9820F8447E989B66C73C9DE077FAAB75618D639DFA1F3BCA45F946" },
+ { name = "rsvp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_fetch", "gleam_http", "gleam_httpc", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "rsvp", source = "hex", outer_checksum = "40F9E0E662FF258E10C7041A9591261FE802D56625FB444B91510969644F7722" },
+ { name = "tom", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0910EE688A713994515ACAF1F486A4F05752E585B9E3209D8F35A85B234C2719" },
]
[requirements]
-birdie = { version = ">= 1.2.7 and < 2.0.0" }
-gleam_community_colour = { version = "1.4.1" }
-gleam_fetch = { version = "1.3.0" }
-gleam_http = { version = "3.7.2" }
-gleam_javascript = { version = "1.0.0" }
-gleam_json = { version = "2.3.0" }
-gleam_stdlib = { version = "0.59.0" }
-gleam_time = { version = ">= 1.2.0 and < 2.0.0" }
+chilp = { git = "https://forge.strawmelonjuice.com/strawmelonjuice/chilp.git", ref = "b43d01e6c39d12121a5cbfbf53b6b1dec8bc49f1" }
+gleam_fetch = { version = ">= 1.3.0 and < 2.0.0" }
+gleam_http = { version = ">= 4.3.0 and < 5.0.0" }
+gleam_json = { version = ">= 3.1.0 and < 4.0.0" }
+gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
-houdini = { version = ">= 1.1.0 and < 2.0.0" }
-jot = { version = ">= 5.0.0 and < 6.0.0" }
-lustre = { version = ">= 5.0.2 and < 6.0.0" }
-modem = { version = "2.0.2" }
-odysseus = { version = ">= 1.0.0 and < 2.0.0" }
-plinth = { version = ">= 0.5.9 and < 1.0.0" }
-rsvp = { version = ">= 1.0.0 and < 2.0.0" }
+lustre = { version = ">= 5.5.2 and < 6.0.0" }
+modem = { version = ">= 2.1.2 and < 3.0.0" }
+plinth = { version = ">= 0.9.2 and < 1.0.0" }
+rsvp = { version = ">= 1.2.0 and < 2.0.0" }
+tom = { version = "1.1.1" }
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client.gleam
index 4a765a8..91c0b07 100644
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client.gleam
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client.gleam
@@ -1,621 +1,225 @@
+import chilp/widget
+import cynthia_websites_mini_shared/config/site_json
+import cynthia_websites_mini_shared/config/v4_1
+import cynthia_websites_mini_shared/ffi
+import gleam/dynamic/decode
+import gleam/fetch
+import gleam/http/request
+import gleam/http/response
+import gleam/javascript/promise
+import gleam/option.{None}
+import gleam/result
+import gleam/string
+import plinth/browser/location
+import plinth/browser/window
+import rsvp
+
+pub const version = ffi.version
+
// IMPORTS ---------------------------------------------------------------------
-import cynthia_websites_mini_client/configtype
-import cynthia_websites_mini_client/configurable_variables
-import cynthia_websites_mini_client/contenttypes
-import cynthia_websites_mini_client/dom
-import cynthia_websites_mini_client/messages.{
- type Msg, ApiReturnedData, SafeTimePassed, TriggerCheckForHashChange,
- UserNavigateTo,
-}
-import cynthia_websites_mini_client/model_type.{type Model, Model}
-import cynthia_websites_mini_client/pottery
-import cynthia_websites_mini_client/utils
-import cynthia_websites_mini_client/view
-import gleam/bit_array
-import gleam/bool
-import gleam/dict
-import gleam/dynamic
-import gleam/float
+import gleam/dict.{type Dict}
import gleam/int
import gleam/list
-import gleam/option.{None, Some}
-import gleam/order
-import gleam/result
-import gleam/string
import gleam/uri.{type Uri}
-import houdini
import lustre
+import lustre/attribute.{type Attribute}
import lustre/effect.{type Effect}
+import lustre/element.{type Element}
+import lustre/element/html
+
+// Modem is a package providing effects and functionality for routing in SPAs.
+// This means instead of links taking you to a new page and reloading everything,
+// they are intercepted and your `update` function gets told about the new URL.
import modem
-import odysseus
-import plinth/browser/window
-import plinth/javascript/console
-import plinth/javascript/global
-import plinth/javascript/storage
-import rsvp
// MAIN ------------------------------------------------------------------------
pub fn main() {
- let app = lustre.application(init, update, view.main)
- let assert Ok(_) = lustre.start(app, "#viewable", Nil)
+ let app = lustre.application(init, update, view)
+ let assert Ok(sitejsonuri) = rsvp.parse_relative_uri("/site.json")
+ let assert Ok(req) = request.to(sitejsonuri |> uri.to_string())
+ use resp <- promise.try_await(fetch.send(req))
+ use resp <- promise.try_await(fetch.read_json_body(resp))
+ let result = decode.run(resp.body, site_json.site_json_decoder())
+ case resp.status, result {
+ 200, Ok(sitejson) -> {
+ let assert Ok(_) = lustre.start(app, "#viewable", sitejson)
+ Nil
+ }
+ // Failure is okay, we just don't activate and hope the server served well enough pregenerations.
+ _, _ -> Nil
+ }
- Nil
+ promise.resolve(Ok(Nil))
}
-fn await_safe_time() {
- let set_timeout_nilled = fn(delay: Int, cb: fn() -> a) -> Nil {
- global.set_timeout(delay, cb)
- Nil
- }
- use dispatch <- effect.from
- use <- set_timeout_nilled(200)
- dispatch(SafeTimePassed)
+// MODEL -----------------------------------------------------------------------
+
+type Model {
+ Model(
+ data: site_json.SiteJSON,
+ route: Route,
+ chilp_model: widget.ChilpDataInYourModel(Msg),
+ )
}
-fn check_for_hash_change_every_50ms() -> Effect(Msg) {
- let set_timeout_nilled = fn(delay: Int, cb: fn() -> a) -> Nil {
- global.set_timeout(delay, cb)
- Nil
- }
- use dispatch <- effect.from
- // Every 50ms, check for hash changes until 200ms have passed
- set_timeout_nilled(50, fn() { dispatch(TriggerCheckForHashChange) })
- set_timeout_nilled(100, fn() { dispatch(TriggerCheckForHashChange) })
- set_timeout_nilled(150, fn() { dispatch(TriggerCheckForHashChange) })
- set_timeout_nilled(200, fn() { dispatch(TriggerCheckForHashChange) })
- Nil
+type PostFilter {
+ ByCategory(String)
+ ByTag(String)
+ AnyFieldContains(String)
}
-/// Slightly more assertive way of finding url changes. This because sometimes a page change outside of the visibility of modem is undetected, mixing up the hashes and pages. This effect kicks in once they should have had their time and attempts to fix it.
-fn check_for_hash_change(model: Model) -> Effect(Msg) {
- use dispatch <- effect.from
- case model.safetimepassed {
- True -> {
- Nil
- }
- False -> {
- let assert Ok(session) = storage.local()
- as "Browser is expected to have a localstorage."
+type Route {
+ Index
+ PostsList(PostFilter)
+ Content(slug: String)
+ NotFound(uri: Uri)
+}
- case window.get_hash() {
- Ok(f) -> {
- let h = case f {
- "" -> {
- "/"
- }
- d -> {
- d
- }
- }
- case h == model.path {
- True -> Nil
- False -> {
- console.log("[assertive] Hash changed to: " <> h)
- let assert Ok(..) = storage.set_item(session, "last", h)
- dispatch(UserNavigateTo(h))
- }
- }
- }
- _ -> {
- // This happens whenever the hash is not found
- // like for example when utterances login just happened.
- // This is not unexpected behaviour, since the storage knows better in those cases.
- Nil
+fn parse_route(uri: Uri) -> Route {
+ case uri.path_segments(uri.path) {
+ [] | [""] -> {
+ case location.hash(window.location(window.self())) {
+ Error(_) -> Index
+
+ Ok("#!/category/" <> cat) -> PostsList(ByCategory(cat))
+ Ok("#!/tag/" <> tag) -> PostsList(ByTag(tag))
+ Ok("#!/search/" <> tag) -> PostsList(AnyFieldContains(tag))
+
+ Ok(c) -> {
+ let d = "Unhandled hashroute: " <> c
+ panic as d
}
}
}
+ ["tagged", tag] -> PostsList(ByCategory(tag))
+ ["category", cat] -> PostsList(ByTag(cat))
+ ["post", slug] | ["page", slug] | ["content", slug] -> Content(slug:)
+
+ _ -> NotFound(uri:)
}
}
-fn init(_) -> #(Model, Effect(Msg)) {
- console.log("Cynthia Client starting up")
- let effects =
- effect.batch([
- fetch_all(ApiReturnedData),
- modem.init(on_url_change),
- await_safe_time(),
- check_for_hash_change_every_50ms(),
- ])
- // Using local storage as session storage because session storage doesn't stay long enough
- let assert Ok(session) = storage.local()
- as "Browser is expected to have a localstorage."
- // .. if the local storage is older than 1 minute though, we clear it
- let val = case storage.get_item(session, "time") {
- Ok(time) -> {
- let now = utils.now()
- let stamp = result.unwrap(int.parse(time), 0)
- let diff = int.subtract(now, stamp) |> int.absolute_value
- // 1 minutes = 60 seconds
- let order = int.compare(diff, 60)
- case order {
- order.Eq | order.Gt -> False
- order.Lt -> True
- }
- }
- Error(..) -> {
- False
- }
+/// We also need a way to turn a Route back into a an `href` attribute that we
+/// can then use on `html.a` elements. It is important to keep this function in
+/// sync with the parsing, but once you do, all links are guaranteed to work!
+///
+fn href(route: Route, model: Model) -> Attribute(msg) {
+ let url = case route {
+ Index -> "/"
+ Content(c) -> {
+ dict.get(model.data.content, c)
+ |> result.map(fn(content) {
+ case content {
+ site_json.Post(..) -> {
+ "/post/" <> c
+ }
+ site_json.Page(..) -> {
+ "/page/" <> c
+ }
+ }
+ })
+ |> result.unwrap("/content/" <> c)
+ }
+ NotFound(_) -> "/404"
+ PostsList(ByCategory(cat)) -> "/category/" <> cat
+ PostsList(ByTag(tag)) -> "/tagged/" <> tag
+ PostsList(AnyFieldContains(q)) -> "/#!/search/" <> q
}
- case val {
- False -> {
- console.log("Clearing local storage")
- storage.clear(session)
- }
- True -> {
- // Keeping local storage, updating time
- let now = utils.now() |> int.to_string
- case storage.set_item(session, "time", now) {
- Ok(_) -> {
- console.log("Updated local storage time")
- }
- Error(e) -> {
- console.error(
- "Error updating local storage time: " <> string.inspect(e),
- )
- }
- }
- }
- }
- let initial_path = case storage.get_item(session, "last"), window.get_hash() {
- Ok(path), _ -> {
- // We have a last path in local storage. Return it. Hash will be set to it later.
- path
- }
- Error(..), Ok("") | Error(..), Error(..) -> {
- // No last path in local storage, so we set the hash to "/"
- dom.set_hash("/")
- let assert Ok(..) = storage.set_item(session, "last", "/")
+ attribute.href(url)
+}
- "/"
- }
- Error(..), Ok(f) -> {
- // From the hash, we set the last path in local storage
- let assert Ok(..) = storage.set_item(session, "last", f)
- // and return the hash
- f
+fn init(appdata: site_json.SiteJSON) -> #(Model, Effect(Msg)) {
+ let route = case modem.initial_uri() {
+ Ok(uri) -> parse_route(uri)
+ Error(_) -> Index
+ }
+ let chilp_model = widget.init(Chilp)
+ let effect =
+ modem.init(fn(uri) {
+ uri
+ |> parse_route
+ |> UserNavigatedTo
+ })
+ let model = Model(appdata, route:, chilp_model:)
+ let effect = case appdata.config.posts.comments {
+ v4_1.CommentsGithubStored(..) -> effect
+ v4_1.CommentsDisabled -> effect
+ // This site uses Chilp! Let's smoothen the UX by prefetching some of the posts in the background!
+ v4_1.CommentsMastodonStored -> {
+ appdata.content
+ |> dict.values
+ |> list.shuffle
+ |> list.filter(keeping: fn(c) {
+ case c {
+ site_json.Post(mastodon_comments:, ..) -> {
+ case mastodon_comments {
+ option.Some(..) -> True
+ _ -> False
+ }
+ }
+ _ -> False
+ }
+ })
+ |> list.map(fn(post) {
+ let assert site_json.Post(mastodon_comments: option.Some(status), ..) =
+ post
+ let widget_ =
+ widget.new(
+ instance: status.instance,
+ post_id: status.id,
+ chilp_model:,
+ )
+ widget.force(chilp_model:, on: widget_)
+ })
+ |> list.shuffle
+ |> list.append([effect], _)
+ |> effect.batch
}
}
- console.log("Initial path: " <> initial_path)
- let model =
- Model(initial_path, None, dict.new(), Ok(Nil), dict.new(), session, False)
- #(model, effects)
+ #(model, effect)
}
-// Effect handlers --------------------------------------------------------------
-/// On url change: (Obviously) is triggered on url change, this is useful for intercepting the url hash change on in-site-navigation, that Cynthia uses.
-fn on_url_change(uri: Uri) -> Msg {
- pottery.destroy_comment_box()
- console.log("URL changed to: " <> uri.to_string(uri))
- let assert Ok(#(_, d)) =
- uri
- |> uri.to_string
- |> string.split_once("#")
- messages.UserNavigateTo(d)
-}
+// UPDATE ----------------------------------------------------------------------
-/// Fetches data from server side
-fn fetch_all(
- on_response handle_response: fn(Result(configtype.CompleteData, rsvp.Error)) ->
- msg,
-) -> Effect(msg) {
- let url = utils.phone_home_url() <> "site.json"
- let decoder = configtype.complete_data_decoder()
- let handler = rsvp.expect_json(decoder, handle_response)
- console.log("Fetching site.json...")
- rsvp.get(url, handler)
+type Msg {
+ UserNavigatedTo(route: Route)
+ Chilp(widget.ChilpMsg)
}
-// UPDATE ----------------------------------------------------------------------
-
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
- // Update session storage with the current time
- let now = utils.now() |> int.to_string
- case storage.set_item(model.sessionstore, "time", now) {
- Ok(_) -> {
- Nil
- }
- Error(e) -> {
- console.error(
- "Error updating session storage time: " <> string.inspect(e),
- )
- }
- }
case msg {
- TriggerCheckForHashChange -> {
- #(model, check_for_hash_change(model))
- }
- SafeTimePassed -> {
- #(Model(..model, safetimepassed: True), check_for_hash_change(model))
- }
- UserNavigateTo(path) -> {
- dom.set_hash(path)
- let other =
- model.other
- |> dict.delete("search_term")
- case storage.set_item(model.sessionstore, "last", path) {
- Ok(_) -> {
- console.log("Stored last path: " <> path)
- }
- Error(e) -> {
- console.error("Error storing last path: " <> string.inspect(e))
- }
- }
- #(Model(..model, path:, other:), effect.none())
- }
- messages.UserSearchTerm(search_term) -> {
- let path = "!/search/" <> search_term
- let computed_menus = model.computed_menus
- let complete_data = model.complete_data
- let status = model.status
- let safetimepassed = model.safetimepassed
- let other =
- model.other
- |> dict.insert("search_term", dynamic.from(search_term))
- dom.set_hash(path)
- let sessionstore = model.sessionstore
- #(
- Model(
- path:,
- complete_data:,
- computed_menus:,
- status:,
- other:,
- sessionstore:,
- safetimepassed:,
- ),
- effect.none(),
- )
- }
- ApiReturnedData(data) -> {
- case data {
- Error(e) -> {
- let error_message =
- "Cynthia Client failed "
- <> case e {
- rsvp.UnhandledResponse(s) ->
- "to handle the server's response: '" <> string.inspect(s) <> "'"
- rsvp.HttpError(_) | rsvp.NetworkError -> "to connect to server."
- rsvp.JsonError(s) ->
- "to decode response: '" <> string.inspect(s) <> "'"
- _ -> " to load this site."
- }
- #(Model(..model, status: Error(error_message)), effect.none())
- }
- Ok(new) -> {
- console.log("Succesfully decoded new data, parsing into model...")
- case new.comment_repo {
- Some(..) ->
- global.set_interval(300, pottery.comment_box_forced_styles)
- None -> global.set_timeout(30_000, fn() { Nil })
- }
- let computed_menus = compute_menus(new.content, model)
- case convert_configurable(new.other_vars) {
- Ok(dict_of_configurables) -> {
- console.log("Succesfully unjsonified configurable variables.")
- // I thought this was already done, but I see what is going on here, still gonna commit. This _should_ be a dynamic, not a fucken string.
- let other = dict.merge(model.other, dict_of_configurables)
- let status = Ok(Nil)
- let complete_data =
- Some(configtype.CompleteData(..new, other_vars: []))
- console.log("Updated model.")
- #(
- Model(..model, complete_data:, computed_menus:, status:, other:),
- effect.none(),
- )
- }
- Error(mess) -> {
- #(Model(..model, status: Error(mess)), effect.none())
- }
- }
- }
- }
- }
- // This also shows pretty well how to store booleans in the model.other dict: Use results.
- messages.UserOnGitHubLayoutToggleMenu -> {
- let other = case dict.get(model.other, "github-layout menu open") {
- Ok(..) -> {
- // is open, so close it
- dict.delete(model.other, "github-layout menu open")
- }
- Error(..) -> {
- // is closed, so open it
- dict.insert(
- model.other,
- "github-layout menu open",
- dynamic.from(None),
- )
- }
- }
- #(Model(..model, other:), effect.none())
- }
- messages.CindyToggleMenu1 -> {
- let other = case dict.get(model.other, "cindy menu 1 open") {
- Ok(..) -> {
- // is open, so close it
- dict.delete(model.other, "cindy menu 1 open")
- }
- Error(..) -> {
- // is closed, so open it
- dict.insert(model.other, "cindy menu 1 open", dynamic.from(None))
- }
- }
- #(Model(..model, other:), effect.none())
+ UserNavigatedTo(route:) -> {
+ let model = Model(..model, route:)
+ #(model, effect.none())
}
- messages.UserOnDocumentationLayoutToggleSidebar -> {
- let other = case dict.get(model.other, "documentation-sidebar-open") {
- Ok(..) -> {
- // is open, so close it
- dict.delete(model.other, "documentation-sidebar-open")
- }
- Error(..) -> {
- // is closed, so open it
- dict.insert(
- model.other,
- "documentation-sidebar-open",
- dynamic.from(None),
- )
- }
- }
- #(Model(..model, other:), effect.none())
+ Chilp(chilp_msg) -> {
+ let #(chilp_model, chilp_effects) =
+ widget.update(chilp_msg, model.chilp_model, browse_to)
+ #(Model(..model, chilp_model:), chilp_effects)
}
}
}
-/// -----------------------------------------------------------------------------------------
-/// Helper function to convert configurable variables into `Result(Dict(String,Dynamic))`'s,
-/// allowing usage in `model.other`
-/// -----------------------------------------------------------------------------------------
-fn convert_configurable(from: List(#(String, List(String)))) {
- let defined = configurable_variables.typecontrolled
- let res =
- result.all(
- list.map(from, fn(item) {
- let #(keyname, probable_value): #(String, List(String)) = item
- use found_type <- result.try(
- list.last(probable_value)
- |> result.replace_error(
- "Invalid value at "
- <> keyname
- <> ", something might have gone wrong encoding this value at the server side.",
- ),
- )
- let defined_type = case list.key_find(defined, keyname) {
- Ok(m) -> m
- Error(_) -> found_type
- }
- // Check if a convertible is found
- let might_rewrite_the_story: Result(#(String, List(String)), String) = case
- bool.or(
- bool.and(
- bool.or(
- defined_type == configurable_variables.var_bitstring,
- defined_type == configurable_variables.var_string,
- ),
- bool.or(
- defined_type == configurable_variables.var_bitstring,
- defined_type == configurable_variables.var_string,
- ),
- ),
- bool.and(
- bool.or(
- defined_type == configurable_variables.var_int,
- defined_type == configurable_variables.var_float,
- ),
- bool.or(
- defined_type == configurable_variables.var_int,
- defined_type == configurable_variables.var_float,
- ),
- ),
- ),
- found_type,
- defined_type,
- probable_value
- {
- False, _, _, _ -> {
- // Type is not found to be convertible, return as-is
- Ok(#(found_type, probable_value))
- }
- _, "integer", "float", [intstr, ..] -> {
- use in <- result.try(result.replace_error(
- int.parse(intstr),
- "Could not parse number in " <> keyname,
- ))
- let flstr = in |> int.to_float |> float.to_string
- Ok(#("float", [flstr, "float"]))
- }
- _, "float", "integer", [flstr, ..] -> {
- // This is a convertible something, and the conversion required is from float to integer, we can just do that.
- use fl <- result.try(result.replace_error(
- float.parse(flstr),
- "Could not parse number in " <> keyname,
- ))
- let in = int.to_string(float.truncate(fl))
- Ok(#("integer", [in, "integer"]))
- }
- _, "string", "bits", [text, ..] -> {
- // This is a convertible something, and the conversion required is from string to bitstring, we can just do that.
- Ok(
- #(configurable_variables.var_bitstring, [
- bit_array.base64_encode(bit_array.from_string(text), True),
- configurable_variables.var_bitstring,
- ]),
- )
- }
-
- _, "bits", "string", [bits64base, ..] -> {
- // This is a convertible something, and the conversion required is from bitstring to string, we can do that if the bitstring is correct.
- use bits <- result.try(result.replace_error(
- bit_array.base64_decode(bits64base),
- "Failed to decode base64 to bitstring for " <> keyname,
- ))
- use str <- result.try(result.replace_error(
- bit_array.to_string(bits),
- "Failed to convert bitstring to string for " <> keyname,
- ))
- Ok(
- #(configurable_variables.var_string, [
- str,
- configurable_variables.var_string,
- ]),
- )
- }
- // For the other kinds, we don't know how to convert, so again, return as-is
- // We won't realistically reach here.
- _, _, _, _ -> Ok(#(found_type, probable_value))
- }
-
- use might_rewrite_the_story <- result.try(might_rewrite_the_story)
- // and this is why it _might_ rewrite the story
- let #(found_type, probable_value) = might_rewrite_the_story
- use <- bool.guard(
- { found_type != defined_type },
- Error(
- "Expected a "
- <> defined_type
- <> " at "
- <> keyname
- <> " but found a "
- <> found_type
- <> " instead!",
- ),
- )
-
- // Rename keynames
- let or_keyname = keyname
- let keyname = "config_" <> keyname
-
- case found_type, probable_value {
- "integer", [num, ..] -> {
- use integer <- result.try(result.replace_error(
- int.parse(num),
- "Could not parse number in " <> or_keyname,
- ))
-
- Ok(#(keyname, dynamic.from(integer)))
- }
-
- "float", [num, ..] -> {
- use number <- result.try(result.replace_error(
- float.parse(num),
- "Could not parse number in " <> or_keyname,
- ))
-
- Ok(#(keyname, dynamic.from(number)))
- }
-
- "boolean", [wether, ..] -> {
- let b = case wether {
- "True" -> Ok(True)
- "False" -> Ok(False)
- _ -> Error("Could not parse boolean value in " <> or_keyname)
- }
- use b <- result.try(b)
- Ok(#(keyname, dynamic.from(b)))
- }
-
- "bits", [base64, ..] -> {
- use bits <- result.try(result.replace_error(
- bit_array.base64_decode(base64),
- "Could not decode base64 in " <> or_keyname,
- ))
- Ok(#(keyname, dynamic.from(bits)))
- }
-
- "string", [text, ..] -> {
- // Strings or base64 strings are easiest, since they're verbatim
- Ok(#(keyname, dynamic.from(text)))
- }
-
- "datetime", _values -> {
- // TODO: Implement datetime parsing later (non-blocking for releases)
- Error("Datetime decoding not yet implemented in " <> or_keyname)
- }
-
- "date", _values -> {
- // TODO: Implement date parsing later (non-blocking for releases)
- Error("Date decoding not yet implemented in " <> or_keyname)
- }
-
- "time", values -> {
- use new_values <- result.try(result.replace_error(
- result.all(list.map(values, int.parse)),
- "Could not parse times in " <> or_keyname,
- ))
- case new_values {
- [hours, minutes, seconds, milis] -> {
- let c =
- dynamic.from(model_type.Time(
- hours:,
- minutes:,
- seconds:,
- milis:,
- ))
- Ok(#(keyname, c))
- }
- _ -> Error("Could not parse times in " <> or_keyname)
- }
- }
- _, _ ->
- Error("Could not decode configurable variable '" <> or_keyname)
- }
- }),
- )
- use res <- result.try(res)
- Ok(dict.from_list(res))
-}
-
-/// Helper function to compute menus --------------------------------------------------------
-fn compute_menus(content: List(contenttypes.Content), model: Model) {
- let menu_s_available =
- content
- |> list.filter_map(fn(alls) {
- case alls.data {
- contenttypes.PageData(soms, _) -> Ok(soms)
- _ -> Error(Nil)
+fn browse_to(url: String) {
+ use dispatch <- effect.from
+ case url |> string.starts_with("/") {
+ // Local! Weird that it'd use this function but glad to catch!
+ True -> {
+ case rsvp.parse_relative_uri(url) {
+ Ok(d) -> dispatch(UserNavigatedTo(d |> parse_route))
+ _ -> ffi.browse(url)
}
- })
- |> list.flatten()
- |> list.unique()
- |> list.sort(int.compare)
- add_each_menu(menu_s_available, model.computed_menus, content)
-}
-
-// This is actually where the real magic happens
-fn add_each_menu(
- next: List(Int),
- gotten: dict.Dict(Int, List(model_type.MenuItem)),
- items: List(contenttypes.Content),
-) -> dict.Dict(Int, List(model_type.MenuItem)) {
- case next {
- [] -> gotten
- [current_menu, ..rest] -> {
- let hits: List(model_type.MenuItem) =
- list.filter_map(items, fn(item) -> Result(model_type.MenuItem, Nil) {
- case item.data {
- contenttypes.PageData(m, _) -> {
- case m |> list.contains(current_menu) {
- True -> {
- Ok(model_type.MenuItem(name: item.title, to: item.permalink))
- }
- False -> Error(Nil)
- }
- }
- _ -> Error(Nil)
- }
- })
- |> list.sort(fn(itema, itemb) {
- let a = houdini.escape(utils.js_trim(odysseus.unescape(itema.name)))
- let b = houdini.escape(utils.js_trim(odysseus.unescape(itemb.name)))
- utils.compare_so_natural(a, b)
- })
- dict.insert(gotten, current_menu, hits)
- |> add_each_menu(rest, _, items)
+ }
+ False -> {
+ ffi.browse_prompt(url)
}
}
}
-@external(javascript, "./cynthia_websites_mini_client/version_ffi.ts", "my_own_version")
-pub fn version() -> String
+fn view(model: Model) -> Element(Msg) {
+ let href = href(_, model)
+ html.a([href(Index)], [element.text("house?")])
+}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/configtype.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/configtype.gleam
deleted file mode 100644
index 47757fc..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/configtype.gleam
+++ /dev/null
@@ -1,246 +0,0 @@
-import cynthia_websites_mini_client/contenttypes.{type Content}
-import gleam/dict
-import gleam/dynamic/decode
-import gleam/json
-import gleam/list
-import gleam/option.{type Option, None, Some}
-
-pub type CompleteData {
- CompleteData(
- global_theme: String,
- global_theme_dark: String,
- global_colour: String,
- global_site_name: String,
- global_site_description: String,
- server_port: Option(Int),
- server_host: Option(String),
- comment_repo: Option(String),
- git_integration: Bool,
- crawlable_context: Bool,
- sitemap: Option(String),
- other_vars: List(#(String, List(String))),
- content: List(Content),
- )
-}
-
-pub fn encode_complete_data_for_client(complete_data: CompleteData) -> json.Json {
- let CompleteData(
- global_theme:,
- global_theme_dark:,
- global_colour:,
- global_site_name:,
- global_site_description:,
- server_port: _,
- server_host: _,
- comment_repo:,
- git_integration:,
- other_vars:,
- content:,
- crawlable_context:,
- sitemap:,
- ) = complete_data
- json.object([
- #("global_theme", json.string(global_theme)),
- #("global_theme_dark", json.string(global_theme_dark)),
- #("global_colour", json.string(global_colour)),
- #("global_site_name", json.string(global_site_name)),
- #("global_site_description", json.string(global_site_description)),
- #("git_integration", json.bool(git_integration)),
- #("crawlable_context", json.bool(crawlable_context)),
- #("sitemap", case sitemap {
- None -> json.null()
- Some(value) -> json.string(value)
- }),
- #("comment_repo", case comment_repo {
- None -> json.null()
- Some(value) -> json.string(value)
- }),
- #(
- "configurable_variables",
- json.array(other_vars, fn(item) -> json.Json {
- json.object([#(item.0, json.array(item.1, json.string))])
- }),
- ),
- #("content", json.array(content, contenttypes.encode_content)),
- ])
-}
-
-pub fn complete_data_decoder() -> decode.Decoder(CompleteData) {
- use global_theme <- decode.field("global_theme", decode.string)
- use global_theme_dark <- decode.field("global_theme_dark", decode.string)
- use global_colour <- decode.field("global_colour", decode.string)
- use global_site_name <- decode.field("global_site_name", decode.string)
- use git_integration <- decode.optional_field(
- "git_integration",
- default_shared_cynthia_config_global_only.git_integration,
- decode.bool,
- )
- use global_site_description <- decode.field(
- "global_site_description",
- decode.string,
- )
- use server_port <- decode.optional_field(
- "server_port",
- None,
- decode.optional(decode.int),
- )
- use server_host <- decode.optional_field(
- "server_host",
- None,
- decode.optional(decode.string),
- )
- use comment_repo <- decode.field(
- "comment_repo",
- decode.optional(decode.string),
- )
- use content <- decode.field(
- "content",
- decode.list(contenttypes.content_decoder()),
- )
- use other_vars <- decode.field("configurable_variables", {
- decode.list(decode.dict(decode.string, decode.list(decode.string)))
- |> decode.map(list.fold(_, dict.new(), dict.merge))
- })
-
- use crawlable_context <- decode.optional_field(
- "crawlable_context",
- default_shared_cynthia_config_global_only.crawlable_context,
- decode.bool,
- )
- use sitemap <- decode.optional_field(
- "sitemap",
- default_shared_cynthia_config_global_only.sitemap,
- decode.optional(decode.string),
- )
-
- let other_vars = dict.to_list(other_vars)
-
- decode.success(CompleteData(
- global_theme:,
- global_theme_dark:,
- global_colour:,
- global_site_name:,
- global_site_description:,
- server_port:,
- server_host:,
- comment_repo:,
- git_integration:,
- crawlable_context:,
- sitemap:,
- other_vars:,
- content:,
- ))
-}
-
-pub type SharedCynthiaConfigGlobalOnly {
- SharedCynthiaConfigGlobalOnly(
- global_theme: String,
- global_theme_dark: String,
- global_colour: String,
- global_site_name: String,
- global_site_description: String,
- server_port: Option(Int),
- server_host: Option(String),
- comment_repo: Option(String),
- /// [True]
- /// Wether or not to enable git integration for the site.
- git_integration: Bool,
- /// [False]
- /// Wether or not to insert json-ld+context into the HTML
- /// to make the site crawlable by search engines or readable by LLMs.
- crawlable_context: Bool,
- /// [True]
- /// Wether or not to create a sitemap.xml file for the site.
- /// This is useful for search engines to index the site.
- /// This is separate from the crawlable_context setting, as no content needs to be rendered or served for the sitemap.xml file.
- sitemap: Option(String),
- other_vars: List(#(String, List(String))),
- )
-}
-
-pub const default_shared_cynthia_config_global_only: SharedCynthiaConfigGlobalOnly = SharedCynthiaConfigGlobalOnly(
- global_theme: "autumn",
- global_theme_dark: "night",
- global_colour: "#FFFFFF",
- global_site_name: "My Site",
- global_site_description: "A big site on a mini Cynthia!",
- server_port: None,
- server_host: None,
- comment_repo: None,
- git_integration: True,
- crawlable_context: False,
- sitemap: Some("https://example.com"),
- other_vars: [],
-)
-
-pub fn merge(
- orig: SharedCynthiaConfigGlobalOnly,
- content: List(Content),
-) -> CompleteData {
- CompleteData(
- global_theme: orig.global_theme,
- global_theme_dark: orig.global_theme_dark,
- global_colour: orig.global_colour,
- global_site_name: orig.global_site_name,
- global_site_description: orig.global_site_description,
- server_port: orig.server_port,
- server_host: orig.server_host,
- comment_repo: orig.comment_repo,
- git_integration: orig.git_integration,
- crawlable_context: orig.crawlable_context,
- sitemap: orig.sitemap,
- other_vars: orig.other_vars,
- content:,
- )
-}
-
-pub const ootb_index = "{#hello-world}
-# Hello, World
-
-1. Numbered lists
-2. Images: 
-
-{#the-world-is-big}
-## The world is big
-
-{#the-world-is-a-little-smaller}
-### The world is a little smaller
-
-{#the-world-is-tiny}
-#### The world is tiny
-
-{#the-world-is-tinier}
-##### The world is tinier
-
-{#the-world-is-the-tiniest}
-###### The world is the tiniest
-
-> Also quote blocks\\!
-> \\
-> -StrawmelonJuice
-
-
-A task list:
-- [ ] Task 1
-- [x] Task 2
-- [ ] Task 3
-
-A bullet list:
-
-- Point 1
-- Point 2
-
-{.bash}
- ```myfile.bash
- echo \"Code blocks!\"
- // - StrawmelonJuice
- ```
-
-A small table:
-| Column 1 | Column 2 |
-| -------- | -------- |
-| Value 1 | Value 2 |
-| [Github](https://github.com) | [Codeberg](https://codeberg.org) |
-|||
-"
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/configurable_variables.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/configurable_variables.gleam
deleted file mode 100644
index 07129bf..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/configurable_variables.gleam
+++ /dev/null
@@ -1,64 +0,0 @@
-//// Configurable variables module
-////
-//// This module doesn't exist to hold any actual types for configurable variables,
-//// as that is implemented in `cynthia_websites_mini_client/configtype`. Also not
-//// Providing any fucntions for reading, serialising etc. those functions are implemented
-//// in their relative `cynthia_websites_mini_server` and `cynthia_websites_mini_client` modules.
-////
-//// However, these variables are dynamically marked and not typestrongly shipped to the client-side.
-//// This compromises the guarantee of Gleams type safety mechanisms, and might create errors on users'
-//// ends without any valid way of reproducing. This also makes it very hard to do certain optimisations
-////
-//// Luckily, those dynamic markers are developed by yours truly, and of course I keep type information with them.
-//// Even though some values might still be arbitrarily typed and left unchecked, types you add to the below
-//// const typecontrolled variable, WILL be checked in runtime.
-////
-//// Please note: variables in the model are stored under the 'other' type, which means you'll have to decode their values
-//// once more after transfer. However, by setting types beforehand you'll be able to directly decode them, instead of first having to decode them to a
-//// `List(String)` and then manually having to convert their type.
-
-/// Variable names and their pre-defined types.
-pub const typecontrolled = [
- #("examplevar", var_string),
- #(
- // Template for the ownit layout
- "ownit_template",
- var_string,
- ),
-]
-
-/// An unsupported type, this is for example the type of any array or sub-table, as those aren't supported.
-pub const var_unsupported = "unsupported"
-
-/// Now, obviously this isn't a type supported directly in TOML.
-///
-/// This can still be created by using a `{ path = "filename.bin" }` or the `url` equevalent.
-/// Note that bitstrings and strings are interchangeable, if you define a bitstring in typecontrolled, you'll get a
-/// base64 delivered in your layout, wether it's source was a string or file.
-/// If you decide you want a string, bitstrings will be converted for you.
-/// If any of those conversions fail, client will be able to quit quickly, allowing author's to see the error.
-pub const var_bitstring = "bits"
-
-/// A string, also see bitstring to read how this is interchangeable.
-pub const var_string = "string"
-
-/// A boolean
-pub const var_boolean = "boolean"
-
-/// A date with no time attached
-pub const var_date = "date"
-
-/// A date and a time, warning:
-/// Using an offset that implies anything else than 'local', will
-/// change the type to unsupported.
-/// Use an int containing a unix timestamp over this.
-pub const var_datetime = "datetime"
-
-/// A time, consisting of hour, minute, second and millisecond.
-pub const var_time = "time"
-
-/// A floating point number. Will be converted to int on the fly if needed.
-pub const var_float = "float"
-
-/// An integer number. Will be converted to float on the fly if needed.
-pub const var_int = "integer"
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/contenttypes.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/contenttypes.gleam
deleted file mode 100644
index 4e0167d..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/contenttypes.gleam
+++ /dev/null
@@ -1,169 +0,0 @@
-import gleam/dynamic/decode
-import gleam/json
-
-// Content main type ----------------------------------------------------------------------------
-/// Type storing all info it parses from files and json metadatas
-pub type Content {
- Content(
- filename: String,
- title: String,
- description: String,
- layout: String,
- permalink: String,
- inner_plain: String,
- data: ContentData,
- )
-}
-
-pub fn content_decoder() -> decode.Decoder(Content) {
- use filename <- decode.field("filename", decode.string)
- use title <- decode.field("title", decode.string)
- use description <- decode.field("description", decode.string)
- use layout <- decode.field("layout", decode.string)
- use permalink <- decode.field("permalink", decode.string)
- use inner_plain <- decode.field("inner_plain", decode.string)
- use data <- decode.field("data", content_data_decoder())
- decode.success(Content(
- filename:,
- title:,
- description:,
- layout:,
- permalink:,
- inner_plain:,
- data:,
- ))
-}
-
-pub fn content_decoder_and_merger(
- inner_plain: String,
- filename: String,
-) -> decode.Decoder(Content) {
- use title <- decode.field("title", decode.string)
- use description <- decode.field("description", decode.string)
- use layout <- decode.field("layout", decode.string)
- use permalink <- decode.field("permalink", decode.string)
- use data <- decode.field("data", content_data_decoder())
- decode.success(Content(
- filename:,
- title:,
- description:,
- layout:,
- permalink:,
- inner_plain:,
- data:,
- ))
-}
-
-pub fn encode_content(content: Content) -> json.Json {
- let Content(
- filename:,
- title:,
- description:,
- layout:,
- permalink:,
- inner_plain:,
- data:,
- ) = content
- json.object([
- #("filename", json.string(filename)),
- #("title", json.string(title)),
- #("description", json.string(description)),
- #("layout", json.string(layout)),
- #("permalink", json.string(permalink)),
- #("inner_plain", json.string(inner_plain)),
- #("data", encode_content_data(data)),
- ])
-}
-
-pub fn encode_content_for_fs(content: Content) -> json.Json {
- let Content(
- filename: _,
- title:,
- description:,
- layout:,
- permalink:,
- inner_plain: _,
- data:,
- ) = content
- json.object([
- #("title", json.string(title)),
- #("description", json.string(description)),
- #("layout", json.string(layout)),
- #("permalink", json.string(permalink)),
- #("data", encode_content_data(data)),
- ])
-}
-
-// Content data type ----------------------------------------------------------------------------
-
-pub type ContentData {
- /// Post metadata
- PostData(
- /// Date string: This is decoded as a string, then recoded and decoded again to make sure it complies with ISO 8601.
- /// # Date published
- /// Stores the date on which the post was published.
- date_published: String,
- /// Date string: This is decoded as a string, then recoded and decoded again to make sure it complies with ISO 8601.
- /// # Date updated
- /// Stores the date on which the post was last updated.
- date_updated: String,
- /// Category this post belongs to
- category: String,
- /// Tags that belong to this post
- tags: List(String),
- )
- /// Page metadata
- PageData(
- /// In which menus this page should appear
- in_menus: List(Int),
- /// Hide the block with title and description for a page.
- hide_meta_block: Bool,
- )
-}
-
-pub fn content_data_decoder() -> decode.Decoder(ContentData) {
- use variant <- decode.field("type", decode.string)
- case variant {
- "post_data" -> {
- use date_published <- decode.field("date_published", decode.string)
- use date_updated <- decode.field("date_updated", decode.string)
- use category <- decode.field("category", decode.string)
- use tags <- decode.field("tags", decode.list(decode.string))
- decode.success(PostData(date_published:, date_updated:, category:, tags:))
- }
- "page_data" -> {
- use in_menus <- decode.field("in_menus", decode.list(decode.int))
- use hide_meta_block <- decode.optional_field(
- "hide_meta",
- False,
- decode.bool,
- )
- decode.success(PageData(in_menus:, hide_meta_block:))
- }
- _ ->
- decode.failure(
- PostData(date_published: "", date_updated: "", category: "", tags: []),
- "ContentData",
- )
- }
-}
-
-pub fn encode_content_data(content_data: ContentData) -> json.Json {
- case content_data {
- PostData(date_published:, date_updated:, category:, tags:) ->
- json.object([
- #("type", json.string("post_data")),
- #("date_published", json.string(date_published)),
- #("date_updated", json.string(date_updated)),
- #("category", json.string(category)),
- #("tags", json.array(tags, json.string)),
- ])
- PageData(in_menus:, hide_meta_block:) ->
- json.object([
- #("type", json.string("page_data")),
- #("in_menus", json.array(in_menus, json.int)),
- #("hide_meta", json.bool(hide_meta_block)),
- ])
- }
-}
-// End of module.
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/errors.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/errors.gleam
deleted file mode 100644
index 477b703..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/errors.gleam
+++ /dev/null
@@ -1,11 +0,0 @@
-import gleam/dynamic/decode
-import gleam/fetch
-
-pub type AnError {
- WebNotFound
- DecodeError(decode.DecodeError)
- DecodeErrorsPlural(List(decode.DecodeError))
- FetchError(fetch.FetchError)
- GenericError(String)
- Unexpectance
-}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/cindy_dual.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/cindy_dual.gleam
similarity index 98%
rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/cindy_dual.gleam
rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/cindy_dual.gleam
index 5460659..d9ddc0b 100644
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/cindy_dual.gleam
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/cindy_dual.gleam
@@ -391,7 +391,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) {
False -> "hover:bg-base-300/50 transition-colors duration-200"
}
}),
- attribute.href(utils.phone_home_url() <> "#" <> current_item.to),
+ attribute.href(current_item.to),
],
[html.text(current_item.name)],
),
@@ -423,7 +423,7 @@ pub fn menu_2(from model: model_type.Model) -> List(Element(messages.Msg)) {
False -> "hover:bg-base-300/50 transition-colors duration-200"
}
}),
- attribute.href(utils.phone_home_url() <> "#" <> a.to),
+ attribute.href(a.to),
],
[html.text(a.name)],
),
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/cindy_landing.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/cindy_landing.gleam
similarity index 100%
rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/cindy_landing.gleam
rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/cindy_landing.gleam
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/cindy_simple.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/cindy_simple.gleam
similarity index 99%
rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/cindy_simple.gleam
rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/cindy_simple.gleam
index 9218f86..dc199d3 100644
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/cindy_simple.gleam
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/cindy_simple.gleam
@@ -389,7 +389,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) {
False -> "hover:bg-base-300/50 transition-colors duration-200"
}
}),
- attribute.href(utils.phone_home_url() <> "#" <> a.to),
+ attribute.href(a.to),
],
[html.text(a.name)],
),
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/documentation.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/documentation.gleam
similarity index 98%
rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/documentation.gleam
rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/documentation.gleam
index 32c19d5..508d097 100644
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/documentation.gleam
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/documentation.gleam
@@ -429,9 +429,7 @@ fn documentation_common(
attribute.class(
"flex items-center gap-2 px-4 py-2 rounded-md hover:bg-base-300/50 text-base-content/80 hover:text-base-content",
),
- attribute.href(
- utils.phone_home_url() <> "#" <> item.to,
- ),
+ attribute.href(item.to),
],
[
html.span(
@@ -473,9 +471,7 @@ fn documentation_common(
attribute.class(
"flex items-center gap-2 px-4 py-2 rounded-md hover:bg-base-300/50 text-base-content/80 hover:text-base-content",
),
- attribute.href(
- utils.phone_home_url() <> "#" <> item.to,
- ),
+ attribute.href(item.to),
],
[
html.div(
@@ -619,7 +615,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) {
False ->
"flex items-center px-3 py-2 rounded-md hover:bg-base-300/50 text-base-content/80 hover:text-base-content"
}),
- attribute.href(utils.phone_home_url() <> "#" <> to),
+ attribute.href(to),
event.on_click(messages.UserOnDocumentationLayoutToggleSidebar),
],
[
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/frutiger.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/frutiger.gleam
similarity index 99%
rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/frutiger.gleam
rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/frutiger.gleam
index 0fa59f8..fb3a3a9 100644
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/frutiger.gleam
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/frutiger.gleam
@@ -229,7 +229,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) {
"text-base-content/80 hover:bg-base-300/30 border-transparent hover:border-base-content/10"
},
),
- attribute.href(utils.phone_home_url() <> "#" <> item.1),
+ attribute.href(item.1),
],
[html.text(item.0)],
),
@@ -244,7 +244,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) {
/// @param content The main content to display
/// @param menu Navigation menu items
/// @param header Page or post header content
-/// @param variables Dictionary with page/post metadata
+/// @param variables Dictionary with page/post metadata
fn frutiger_common(
content: Element(messages.Msg),
menu: List(Element(messages.Msg)),
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/github_layout.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/github_layout.gleam
similarity index 99%
rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/github_layout.gleam
rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/github_layout.gleam
index 3e90ec9..caf46cb 100644
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/github_layout.gleam
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/github_layout.gleam
@@ -549,9 +549,7 @@ fn github_common(
"hover:bg-base-200 text-base-content/80"
},
),
- attribute.href(
- utils.phone_home_url() <> "#" <> item.1,
- ),
+ attribute.href(item.1),
event.on_click(
messages.UserOnGitHubLayoutToggleMenu,
),
@@ -757,7 +755,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) {
"border-transparent text-base-content/70 hover:border-base-300/60"
},
),
- attribute.href(utils.phone_home_url() <> "#" <> item.1),
+ attribute.href(item.1),
],
[
html.div([attribute.class("flex items-center gap-1.5")], [
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/minimalist.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/minimalist.gleam
similarity index 99%
rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/minimalist.gleam
rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/minimalist.gleam
index 3f04bc2..f9b991a 100644
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/minimalist.gleam
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/minimalist.gleam
@@ -345,7 +345,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) {
False -> "text-base-content/70 hover:text-base-content"
},
),
- attribute.href(utils.phone_home_url() <> "#" <> item.1),
+ attribute.href(item.1),
],
[html.text(item.0)],
),
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/oceanic_layout.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/oceanic_layout.gleam
similarity index 99%
rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/oceanic_layout.gleam
rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/oceanic_layout.gleam
index 9f7162a..bf93751 100644
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/oceanic_layout.gleam
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/oceanic_layout.gleam
@@ -522,7 +522,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) {
True -> "active"
False -> ""
}),
- attribute.href(utils.phone_home_url() <> "#" <> item.1),
+ attribute.href(item.1),
],
[html.text(item.0)],
),
@@ -571,7 +571,7 @@ pub fn menu_2(from model: model_type.Model) -> List(Element(messages.Msg)) {
False -> "btn btn-sm btn-outline btn-primary"
// Outline button for inactive
}),
- attribute.href(utils.phone_home_url() <> "#" <> item.1),
+ attribute.href(item.1),
],
[html.text(item.0)],
),
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/pastels.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/pastels.gleam
similarity index 99%
rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/pastels.gleam
rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/pastels.gleam
index 80a1521..017aeb5 100644
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/pastels.gleam
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/pastels.gleam
@@ -224,7 +224,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) {
False -> "text-base-content/70 hover:bg-base-200/50"
},
),
- attribute.href(utils.phone_home_url() <> "#" <> item.1),
+ attribute.href(item.1),
],
[html.text(item.0)],
),
@@ -239,7 +239,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) {
/// @param content The main content to display
/// @param menu Navigation menu items
/// @param header Page or post header content
-/// @param variables Dictionary with page/post metadata
+/// @param variables Dictionary with page/post metadata
fn pastels_common(
content: Element(messages.Msg),
menu: List(Element(messages.Msg)),
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/sepia.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/sepia.gleam
similarity index 99%
rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/sepia.gleam
rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/sepia.gleam
index 0dfeb9c..5a00ebe 100644
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/sepia.gleam
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/sepia.gleam
@@ -246,7 +246,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) {
"text-base-content/70 hover:text-primary hover:italic"
},
),
- attribute.href(utils.phone_home_url() <> "#" <> item.to),
+ attribute.href(item.to),
],
[html.text(item.name)],
),
@@ -261,7 +261,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) {
/// @param content The main content to display
/// @param menu Navigation menu items
/// @param header Page or post header content
-/// @param variables Dictionary with page/post metadata
+/// @param variables Dictionary with page/post metadata
fn sepia_common(
content: Element(messages.Msg),
menu: List(Element(messages.Msg)),
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/messages.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/messages.gleam
deleted file mode 100644
index 2fd1b92..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/messages.gleam
+++ /dev/null
@@ -1,15 +0,0 @@
-import cynthia_websites_mini_client/configtype
-import rsvp
-
-/// Msg is the parent type for all the possible messages
-/// that can be sent in the client
-pub type Msg {
- ApiReturnedData(Result(configtype.CompleteData, rsvp.Error))
- UserNavigateTo(String)
- UserSearchTerm(String)
- UserOnGitHubLayoutToggleMenu
- UserOnDocumentationLayoutToggleSidebar
- CindyToggleMenu1
- SafeTimePassed
- TriggerCheckForHashChange
-}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/model_type.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/model_type.gleam
deleted file mode 100644
index fd72589..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/model_type.gleam
+++ /dev/null
@@ -1,59 +0,0 @@
-import cynthia_websites_mini_client/configtype
-import gleam/dict.{type Dict}
-import gleam/dynamic
-import gleam/dynamic/decode
-import gleam/option.{type Option}
-import plinth/javascript/storage
-
-pub type Model {
- Model(
- /// Where are we
- path: String,
- /// Complete data that makes up the site. This is all that the server serves up.
- complete_data: Option(configtype.CompleteData),
- /// Menu's stored readily for themes to pick up.
- /// Structure:
- /// Dict(which_menu: Int, List(#(to, from)))
- computed_menus: Dict(Int, List(MenuItem)),
- /// Status
- /// Allows us to trigger the error page from the update function, without the need for more variants of Model.
- ///
- /// Normally this is `Ok(Nil)`
- /// On error this is `Error(error_message: String)`
- status: Result(Nil, String),
- /// Other variables
- /// This stores for example the current search term
- other: Dict(String, dynamic.Dynamic),
- /// Session storage
- sessionstore: storage.Storage,
- /// Safe time passed -- equals 200ms after initial load
- /// This is to allow for any hash changes that might have happened during the initial load to be caught and acted upon.
- ///
- /// This replaces the previous `ticks` variable which was a count of ticks (50ms each) since load. Ticks >= 4 was considered safe time passed.
- /// Ticks led to unnecessary re-renders, which affected mobile performance negatively.
- safetimepassed: Bool,
- )
-}
-
-pub type MenuItem {
- MenuItem(
- /// The name of the link
- name: String,
- /// The path to the link
- to: String,
- )
-}
-
-/// Configurable variable value type 'Time', can be decoded with `time_decoder` in this same module.
-pub type Time {
- Time(hours: Int, minutes: Int, seconds: Int, milis: Int)
-}
-
-/// Decodes the configurable variable value type 'Time'
-pub fn time_decoder() -> decode.Decoder(Time) {
- use hours <- decode.field("hours", decode.int)
- use minutes <- decode.field("minutes", decode.int)
- use seconds <- decode.field("seconds", decode.int)
- use milis <- decode.field("milis", decode.int)
- decode.success(Time(hours:, minutes:, seconds:, milis:))
-}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pageloader/postlistloader.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/postlistloader.gleam
similarity index 97%
rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/pageloader/postlistloader.gleam
rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/postlistloader.gleam
index 6c4380b..5151813 100644
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pageloader/postlistloader.gleam
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/postlistloader.gleam
@@ -124,7 +124,7 @@ fn postlist_to_html(
html.li([attribute.class("list-row p-10")], [
html.a(
[
- attribute.href(utils.phone_home_url() <> "#" <> post.permalink),
+ attribute.href(post.permalink),
attribute.class("post__link"),
],
[
@@ -165,7 +165,7 @@ fn postlist_to_html(
html.li([attribute.class("list-row p-10")], [
html.a(
[
- attribute.href(utils.phone_home_url() <> "#" <> page.permalink),
+ attribute.href(page.permalink),
attribute.class("post__link"),
],
[
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery.gleam
index 6a17b75..61971b8 100644
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery.gleam
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery.gleam
@@ -1,137 +1,25 @@
-import cynthia_websites_mini_client/configtype
-import cynthia_websites_mini_client/contenttypes
-import cynthia_websites_mini_client/dom
-import cynthia_websites_mini_client/messages
-import cynthia_websites_mini_client/model_type.{type Model}
-import cynthia_websites_mini_client/pottery/djotparse
-import cynthia_websites_mini_client/pottery/molds
-import cynthia_websites_mini_client/pottery/paints
-import cynthia_websites_mini_client/utils
-import gleam/dict
-import gleam/dynamic
-import gleam/list
-import gleam/option
-import gleam/result
-import gleam/string
-import lustre/attribute.{attribute}
-import lustre/element.{type Element}
-import lustre/element/html
+// let comment_color_scheme = case dom.get_color_scheme() {
+// "dark" -> "github-dark"
+// _ -> "github-light"
+// }
-pub fn render_content(
- model: Model,
- content: contenttypes.Content,
-) -> Element(messages.Msg) {
- let assert Ok(def) = paints.get_sytheme(model)
-
- let #(into, output, variables) = case content.data {
- contenttypes.PageData(_, hide_metadata_block) -> {
- let mold = case content.layout {
- "default" | "theme" | "" -> molds.into(def.layout, "page", model)
- layout -> molds.into(layout, "page", model)
- }
-
- let description =
- content.description
- |> parse_html("descr.dj")
- |> element.to_string
- let variables =
- dict.new()
- |> dict.insert("title", content.title |> dynamic.from)
- |> dict.insert("description_html", description |> dynamic.from)
- |> dict.insert("description", content.description |> dynamic.from)
- |> dict.insert(
- "hide_metadata_block",
- hide_metadata_block |> dynamic.from,
- )
- #(mold, parse_html(content.inner_plain, content.filename), variables)
- }
- contenttypes.PostData(category:, date_published:, date_updated:, tags:) -> {
- let mold = case content.layout {
- "default" | "theme" | "" -> molds.into(def.layout, "post", model)
- layout -> molds.into(layout, "post", model)
- }
- let description =
- content.description
- |> parse_html("descr.dj")
- |> element.to_string
- let variables =
- dict.new()
- |> dict.insert("title", dynamic.from(content.title))
- |> dict.insert("description_html", description |> dynamic.from)
- |> dict.insert("description", content.description |> dynamic.from)
- |> dict.insert("date_published", date_published |> dynamic.from)
- |> dict.insert("date_modified", date_updated |> dynamic.from)
- |> dict.insert("category", category |> dynamic.from)
- |> dict.insert("tags", tags |> dynamic.from)
- #(mold, parse_html(content.inner_plain, content.filename), variables)
- }
- }
- // Other stuff should be added to vars here, like site metadata, ~menu links~, etc. EDIT: Menu links go in their own thing.
- let site_name =
- model.complete_data
- |> option.map(fn(a) { a.global_site_name })
- |> option.to_result(Nil)
- |> result.unwrap("My Site Name")
- let considered_output =
- {
- let default = [output]
- case content.data, model {
- contenttypes.PostData(..),
- model_type.Model(
- complete_data: option.Some(configtype.CompleteData(
- comment_repo: option.Some(repo),
- ..,
- )),
- ..,
- )
- if repo != ""
- -> {
- let comment_color_scheme = case dom.get_color_scheme() {
- "dark" -> "github-dark"
- _ -> "github-light"
- }
-
- list.append(default, [
- html.script(
- [
- attribute("async", ""),
- attribute("crossorigin", "anonymous"),
- attribute("theme", comment_color_scheme),
- attribute("issue-term", content.permalink),
- attribute("repo", repo),
- attribute(
- "return-url",
- utils.phone_home_url() <> "#" <> model.path,
- ),
- attribute.src("https://utteranc.es/client.js"),
- ],
- "
-",
- ),
- ])
- }
- _, _ -> default
- }
- }
- |> html.div([attribute.class("contents")], _)
- html.div(
- [
- {
- utils.set_theme_body(def.daisy_ui_theme_name)
- attribute("data-theme", def.daisy_ui_theme_name)
- },
- attribute.class("contents"),
- ],
- {
- [
- into(
- considered_output,
- variables |> dict.insert("global_site_name", dynamic.from(site_name)),
- ),
- ]
- },
- )
-}
+// list.append(default, [
+// html.script(
+// [
+// attribute("async", ""),
+// attribute("crossorigin", "anonymous"),
+// attribute("theme", comment_color_scheme),
+// attribute("issue-term", content.permalink),
+// attribute("repo", repo),
+// attribute(
+// "return-url",
+// model.path,
+// ),
+// attribute.src("https://utteranc.es/client.js"),
+// ],
+// "
+// ",
+// ),
pub fn parse_html(inner: String, filename: String) -> Element(messages.Msg) {
case filename |> string.split(".") |> list.last {
@@ -154,9 +42,3 @@ pub fn parse_html(inner: String, filename: String) -> Element(messages.Msg) {
])
}
}
-
-@external(javascript, "./dom.ts", "destroy_comment_box")
-pub fn destroy_comment_box() -> Nil
-
-@external(javascript, "./dom.ts", "apply_styles_to_comment_box")
-pub fn comment_box_forced_styles() -> Nil
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/djotparse.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/djotparse.gleam
deleted file mode 100644
index 5bc1aba..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/djotparse.gleam
+++ /dev/null
@@ -1,1026 +0,0 @@
-//// Djot is parsed using jot, then converted to lustre elements using jotkey/jot_to_lustre's logic, slightly modified to fit Cynthia's needs.
-
-import cynthia_websites_mini_client/utils
-import gleam/dict.{type Dict}
-import gleam/int
-import gleam/list
-import gleam/option.{None, Some}
-import gleam/string
-import jot.{
- type Container, type Destination, type Document, type Inline, Code, Codeblock,
- Emphasis, Heading, Image, Linebreak, Link, Paragraph, Reference, Strong, Text,
- Url,
-}
-import lustre/attribute
-import lustre/element.{type Element}
-import lustre/element/html
-
-pub fn entry_to_conversion(djot: String) -> List(Element(msg)) {
- let preprocessed = preprocess_djot_extensions(djot)
- let parsed = jot.parse(preprocessed)
- document_to_lustre(parsed)
-}
-
-pub fn preprocess_djot_extensions(djot: String) -> String {
- djot
- // Normalize line endings
- |> string.replace("\r\n", "\n")
- |> string.replace("\r", "\n")
- // Convert escaped exclamation marks
- |> string.replace("\\!", "!")
- // Remove leading/trailing whitespace
- |> string.trim()
- //
- // Now that we've normalized the input, we can preprocess it further
- //
- // Process heading attributes first to ensure IDs are attached correctly
- |> preprocess_heading_attributes
- // Fix multiline images
- |> preprocess_multiline_images
- // Preprocess tables BEFORE autolinks so autolinks don't break table structure
- |> preprocess_tables
- // Preprocess autolinks
- |> preprocess_autolinks
- // Preprocess ordered lists
- |> preprocess_ordered_lists
- // Preprocess blockquotes
- |> preprocess_blockquotes
- // Preprocess task lists
- |> preprocess_task_lists
- // And then out it goes!
-}
-
-fn document_to_lustre(document: Document) -> List(Element(msg)) {
- let elements = containers_to_lustre(document.content, document.references, [])
-
- // Add footnotes section if any footnotes exist
- let elements_with_footnotes = case dict.size(document.footnotes) > 0 {
- True -> {
- let footnotes_section =
- create_footnotes_section(document.footnotes, document.references)
- list.append(elements, [footnotes_section])
- }
- False -> elements
- }
-
- list.reverse(elements_with_footnotes)
-}
-
-fn containers_to_lustre(
- containers containers: List(Container),
- refs refs: Dict(String, String),
- elements elements: List(Element(msg)),
-) -> List(Element(msg)) {
- case containers {
- [] -> elements
- [container, ..rest] -> {
- let elements = container_to_lustre(elements, container, refs)
- containers_to_lustre(rest, refs, elements)
- }
- }
-}
-
-fn container_to_lustre(
- elements: List(Element(msg)),
- container: Container,
- refs: Dict(String, String),
-) {
- let element = case container {
- Paragraph(attrs, inlines) -> {
- // Regular paragraph
- let in_a_list = case refs |> dict.get("am I in a list?") {
- Ok(..) -> True
- _ -> False
- }
-
- html.p(
- attributes_to_lustre(attrs, [
- case in_a_list {
- False -> attribute.class("mb-2")
- True -> attribute.class("whitespace-nowrap")
- },
- ]),
- inlines_to_lustre([], inlines, refs),
- )
- }
- Heading(attrs, level, inlines) -> {
- // Clean heading text to remove {#id} markup
- let clean_inlines = clean_heading_text(inlines)
-
- case level {
- 1 ->
- html.h1(
- attributes_to_lustre(attrs, [
- attribute.class("text-4xl font-bold text-accent"),
- ]),
- inlines_to_lustre([], clean_inlines, refs),
- )
- 2 ->
- html.h2(
- attributes_to_lustre(attrs, [
- attribute.class("text-3xl font-bold text-accent"),
- ]),
- inlines_to_lustre([], clean_inlines, refs),
- )
- 3 ->
- html.h3(
- attributes_to_lustre(attrs, [
- attribute.class("text-2xl font-bold text-accent"),
- ]),
- inlines_to_lustre([], clean_inlines, refs),
- )
- 4 ->
- html.h4(
- attributes_to_lustre(attrs, [
- attribute.class("text-xl font-bold text-accent"),
- ]),
- inlines_to_lustre([], clean_inlines, refs),
- )
- 5 ->
- html.h5(
- attributes_to_lustre(attrs, [
- attribute.class("text-lg font-bold text-accent"),
- ]),
- inlines_to_lustre([], clean_inlines, refs),
- )
- _ ->
- html.h6(
- attributes_to_lustre(attrs, [
- attribute.class("font-bold text-accent"),
- ]),
- inlines_to_lustre([], clean_inlines, refs),
- )
- }
- }
- Codeblock(attrs, language, content) -> {
- html.pre(
- attributes_to_lustre(attrs, [
- attribute.class(
- "bg-neutral text-neutral-content pl-4 block ml-2 mr-2 overflow-x-auto break-none whitespace-pre-wrap font-mono border-dotted border-2 border-neutral-content rounded-lg",
- ),
- ]),
- [
- html.code(
- case language {
- Some(lang) -> [
- attribute.class("language-" <> lang),
- attribute.attribute("data-language", lang),
- ]
- None -> []
- }
- |> list.append([
- attribute.class(
- "bg-neutral text-neutral-content p-1 rounded-lg",
- ),
- ]),
- [
- case language {
- Some(lang) ->
- html.div(
- [
- attribute.class(
- "text-xs text-neutral-content opacity-70 mb-2",
- ),
- ],
- [html.text(string.uppercase(lang))],
- )
- None -> element.none()
- },
- html.text(content),
- ]
- |> list.filter(fn(el) { el != element.none() }),
- ),
- ],
- )
- }
- jot.BulletList(layout:, style: _, items:) -> {
- html.ul(
- [
- attribute.class(
- "list-disc"
- <> {
- " leading-"
- <> case layout {
- jot.Loose -> "loose"
- jot.Tight -> "tight"
- }
- },
- ),
- ],
- list.map(items, fn(item) {
- html.li(
- [],
- containers_to_lustre(
- containers: item,
- refs: refs |> dict.insert("am I in a list?", "yes."),
- elements: [],
- ),
- )
- }),
- )
- }
- // Raw blocks are perfect for our preprocessed HTML
- jot.RawBlock(content:) ->
- element.unsafe_raw_html(
- "div",
- "div",
- [attribute.class("djot-processed-content")],
- content,
- )
- jot.ThematicBreak ->
- html.hr([
- attribute.class("w-48 h-1 mx-auto my-4 border-0 rounded-sm md:my-10"),
- ])
- }
- [element, ..elements]
-}
-
-fn inlines_to_lustre(
- elements: List(Element(msg)),
- inlines: List(Inline),
- refs: Dict(String, String),
-) -> List(Element(msg)) {
- case inlines {
- [] -> list.reverse(elements)
- [inline, ..rest] -> {
- let new_elements = inline_to_lustre([], inline, refs)
- inlines_to_lustre(list.append(new_elements, elements), rest, refs)
- }
- }
-}
-
-fn inline_to_lustre(
- elements: List(Element(msg)),
- inline: Inline,
- refs: Dict(String, String),
-) {
- case inline {
- Linebreak -> [html.br([])]
- Text(text) -> [html.text(text)]
- Strong(inlines) -> {
- [
- html.strong(
- [attribute.class("font-bold")],
- inlines_to_lustre(elements, inlines, refs),
- ),
- ]
- }
- Emphasis(inlines) -> {
- [
- html.em(
- [attribute.class("italic")],
- inlines_to_lustre(elements, inlines, refs),
- ),
- ]
- }
- Link(text, destination) -> {
- [
- case destination {
- Url(url) -> {
- html.a(
- [
- attribute.class("text-info underline"),
- attribute.href({
- case
- string.starts_with(url, "/")
- && !string.starts_with(url, utils.phone_home_url() <> "#")
- {
- True -> utils.phone_home_url() <> "#" <> url
- False -> url
- }
- }),
- ],
- inlines_to_lustre(elements, text, refs),
- )
- }
- Reference(ref_id) -> {
- case dict.get(refs, ref_id) {
- Ok(url) -> {
- html.a(
- [
- attribute.class("text-info underline"),
- attribute.href({
- case
- string.starts_with(url, "/")
- && !string.starts_with(
- url,
- utils.phone_home_url() <> "#",
- )
- {
- True -> utils.phone_home_url() <> "#" <> url
- False -> url
- }
- }),
- ],
- inlines_to_lustre(elements, text, refs),
- )
- }
- Error(_) -> {
- html.span([attribute.class("text-error")], [
- html.text("[Reference not found: " <> ref_id <> "]"),
- ])
- }
- }
- }
- },
- ]
- }
- Image(text, destination) -> {
- [
- html.img([
- attribute.src(destination_attribute(destination, refs)),
- attribute.alt(take_inline_text(text, "")),
- ]),
- ]
- }
- Code(content) -> {
- [
- html.code(
- [
- attribute.class(
- "bg-neutral text-neutral-content p-1 rounded-lg mt-4 mb-4",
- ),
- ],
- [html.text(content)],
- ),
- ]
- }
- jot.Footnote(reference: reference) -> {
- [
- html.a(
- [
- attribute.href("#fn:" <> reference),
- attribute.id("fnref:" <> reference),
- attribute.class("text-info text-xs align-super"),
- attribute.attribute("role", "doc-noteref"),
- ],
- [html.text("[" <> reference <> "]")],
- ),
- ]
- }
- jot.MathDisplay(content: content) -> {
- [
- element.unsafe_raw_html(
- "div",
- "div",
- [attribute.class("math-display my-4 text-center overflow-x-auto")],
- "\\[" <> content <> "\\]",
- ),
- ]
- }
- jot.MathInline(content: content) -> {
- [
- element.unsafe_raw_html(
- "span",
- "span",
- [attribute.class("math-inline")],
- "\\(" <> content <> "\\)",
- ),
- ]
- }
- jot.NonBreakingSpace -> [html.text(" ")]
- }
-}
-
-fn destination_attribute(destination: Destination, refs: Dict(String, String)) {
- case destination {
- Url(url) -> url
- Reference(id) ->
- case dict.get(refs, id) {
- Ok(url) -> url
- Error(Nil) -> ""
- }
- }
-}
-
-fn take_inline_text(inlines: List(Inline), acc: String) -> String {
- case inlines {
- [] -> acc
- [first, ..rest] ->
- case first {
- Text(text) | Code(text) -> take_inline_text(rest, acc <> text)
- Strong(inlines) | Emphasis(inlines) ->
- take_inline_text(list.append(inlines, rest), acc)
- Link(nested, _) | Image(nested, _) -> {
- let acc = take_inline_text(nested, acc)
- take_inline_text(rest, acc)
- }
- Linebreak -> {
- take_inline_text(rest, acc)
- }
- jot.Footnote(reference: reference) -> "[" <> reference <> "]"
- jot.MathDisplay(content: content) -> content
- jot.MathInline(content: content) -> content
- jot.NonBreakingSpace ->
- // Non-breaking space.
- " "
- }
- }
-}
-
-fn attributes_to_lustre(attributes: Dict(String, String), lustre_attributes) {
- attributes
- |> dict.to_list
- |> list.sort(fn(a, b) { string.compare(a.0, b.0) })
- |> list.fold(lustre_attributes, fn(lustre_attributes, pair) {
- [attribute.attribute(pair.0, pair.1), ..lustre_attributes]
- })
-}
-
-fn create_footnotes_section(
- footnotes: Dict(String, List(Container)),
- refs: Dict(String, String),
-) -> Element(msg) {
- case dict.size(footnotes) > 0 {
- True -> {
- html.section(
- [attribute.class("footnotes mt-8 pt-4 border-t border-neutral-content")],
- [
- html.h2([attribute.class("text-xl font-bold text-accent mb-4")], [
- html.text("Footnotes"),
- ]),
- html.ol(
- [attribute.class("list-decimal list-inside space-y-2")],
- footnotes
- |> dict.to_list
- |> list.map(fn(footnote) {
- let #(id, containers) = footnote
- html.li(
- [attribute.id("fn:" <> id), attribute.class("text-sm")],
- list.append(containers_to_lustre(containers, refs, []), [
- html.a(
- [
- attribute.href("#fnref:" <> id),
- attribute.class("text-info ml-2"),
- attribute.attribute("role", "doc-backlink"),
- ],
- [html.text("↩")],
- ),
- ]),
- )
- }),
- ),
- ],
- )
- }
- False -> element.none()
- }
-}
-
-fn preprocess_tables(djot: String) -> String {
- let lines = string.split(djot, "\n")
- process_table_lines(lines, False, [])
- |> string.join("\n")
-}
-
-fn process_table_lines(
- lines: List(String),
- in_table: Bool,
- table_buffer: List(String),
-) -> List(String) {
- case lines {
- [] ->
- case in_table {
- True -> [convert_table_to_raw(list.reverse(table_buffer))]
- False -> []
- }
-
- [line, ..rest] -> {
- let trimmed = string.trim(line)
- let is_table_line = string.contains(line, "|") && trimmed != ""
- let is_separator =
- string.contains(line, "|") && string.contains(line, "-")
-
- case in_table, is_table_line || is_separator {
- True, True -> process_table_lines(rest, True, [line, ..table_buffer])
-
- True, False -> {
- // End of table - process accumulated buffer
- case list.reverse(table_buffer) {
- [] -> [line, ..process_table_lines(rest, False, [])]
- table_lines -> [
- convert_table_to_raw(table_lines),
- line,
- ..process_table_lines(rest, False, [])
- ]
- }
- }
-
- False, True -> {
- // Start of new table
- process_table_lines(rest, True, [line])
- }
-
- False, False -> [line, ..process_table_lines(rest, False, [])]
- }
- }
- }
-}
-
-fn convert_table_to_raw(lines: List(String)) -> String {
- case lines {
- [] -> ""
- [single_line] -> single_line
- lines -> {
- // Find the separator line (contains both | and - and looks like a separator)
- let separator_index =
- list.index_fold(lines, None, fn(acc, line, index) {
- case acc {
- Some(_) -> acc
- None -> {
- let trimmed = string.trim(line)
- let has_pipes = string.contains(line, "|")
- let has_dashes = string.contains(line, "-")
- // A separator should be mostly dashes and pipes with minimal other content
- let is_likely_separator =
- has_pipes
- && has_dashes
- && {
- trimmed
- |> string.to_graphemes
- |> list.all(fn(char) {
- char == "|" || char == "-" || char == " " || char == ":"
- })
- }
-
- case is_likely_separator {
- True -> Some(index)
- False -> None
- }
- }
- }
- })
-
- case separator_index {
- None -> string.join(lines, "\n")
- // No valid separator found
- Some(sep_index) -> {
- // Split into header, separator, and rows
- let header_lines = list.take(lines, sep_index)
- let remaining = list.drop(lines, sep_index + 1)
-
- case header_lines {
- [] -> string.join(lines, "\n")
- // No header
- _ -> {
- // Use the last header line if there are multiple
- let actual_header = case list.reverse(header_lines) {
- [last_header, ..] -> last_header
- [] -> ""
- // Should not happen since header_lines is not empty
- }
-
- let header_cells =
- actual_header
- |> string.split("|")
- |> list.map(string.trim)
- |> list.filter(fn(cell) { cell != "" })
-
- // Validate that we have at least some header cells
- case list.length(header_cells) {
- 0 -> string.join(lines, "\n")
- // No valid header cells
- _ -> {
- let data_rows =
- remaining
- |> list.map(fn(row) {
- row
- |> string.split("|")
- |> list.map(string.trim)
- |> list.filter(fn(cell) { cell != "" })
- })
- |> list.filter(fn(row) { list.length(row) > 0 })
-
- let header_elements = {
- list.map(header_cells, fn(cell) {
- html.th(
- [attribute.class("px-4 py-2 text-left font-bold")],
- entry_to_conversion(cell),
- )
- })
- }
- let row_elements = {
- list.map(data_rows, fn(row) {
- html.tr([], {
- list.map(row, fn(cell) {
- html.td(
- [
- attribute.class(
- "px-4 py-2 border-t border-neutral-content",
- ),
- ],
- entry_to_conversion(cell),
- )
- })
- })
- })
- }
-
- html.table(
- [
- attribute.class(
- "table table-zebra w-full my-4 border border-neutral-content",
- ),
- ],
- [
- html.thead(
- [attribute.class("bg-neutral text-neutral-content")],
- [html.tr([], header_elements)],
- ),
- html.tbody([], row_elements),
- ],
- )
- |> element_to_raw_djotstring
- }
- }
- }
- }
- }
- }
- }
- }
-}
-
-fn preprocess_blockquotes(djot: String) -> String {
- // Process blockquotes as groups, not individual lines
- let lines = string.split(djot, "\n")
- process_blockquote_lines(lines, [], [], False)
- |> string.join("\n")
-}
-
-fn process_blockquote_lines(
- lines: List(String),
- processed: List(String),
- blockquote_buffer: List(String),
- in_blockquote: Bool,
-) -> List(String) {
- case lines {
- [] -> {
- // If we have a blockquote buffer at the end, process it
- case in_blockquote {
- True -> {
- let blockquote =
- convert_blockquote_to_raw(list.reverse(blockquote_buffer))
- list.reverse([blockquote, ..processed])
- }
- False -> list.reverse(processed)
- }
- }
-
- [line, ..rest] -> {
- let trimmed = string.trim(line)
- let is_blockquote_line = string.starts_with(trimmed, "> ")
-
- case in_blockquote, is_blockquote_line {
- // Continue collecting blockquote lines
- True, True -> {
- let content = string.drop_start(trimmed, 2) |> string.trim()
- process_blockquote_lines(
- rest,
- processed,
- [content, ..blockquote_buffer],
- True,
- )
- }
-
- // End of blockquote
- True, False -> {
- let blockquote =
- convert_blockquote_to_raw(list.reverse(blockquote_buffer))
- process_blockquote_lines(rest, [blockquote, ..processed], [], False)
- }
-
- // Start of blockquote
- False, True -> {
- let content = string.drop_start(trimmed, 2) |> string.trim()
- process_blockquote_lines(rest, processed, [content], True)
- }
-
- // Regular line, not in blockquote
- False, False -> {
- process_blockquote_lines(rest, [line, ..processed], [], False)
- }
- }
- }
- }
-}
-
-fn convert_blockquote_to_raw(lines: List(String)) -> String {
- let content =
- lines
- |> list.map(fn(line) {
- case line {
- // Any line without content should be an empty line broken.
- "" -> "\n"
- " " -> "\n"
- "\\" -> ""
- _ -> line
- }
- })
- |> string.join("\n")
-
- html.blockquote(
- [
- attribute.class(
- "border-l-4 border-accent border-dotted pl-4 bg-secondary bg-opacity-10 mb-4 mt-4",
- ),
- ],
- entry_to_conversion(content),
- )
- |> element_to_raw_djotstring
-}
-
-/// Converts a Lustre element to a raw block Djot representation.
-fn element_to_raw_djotstring(elm: element.Element(a)) {
- "\n```=html\n" <> { elm |> element.to_string } <> "\n```\n"
-}
-
-fn preprocess_task_lists(djot: String) -> String {
- djot
- |> string.split("\n")
- |> list.map(fn(line) {
- let trimmed = string.trim(line)
- case string.starts_with(trimmed, "- [ ] ") {
- True -> {
- let content = string.drop_start(trimmed, 6)
- {
- html.div([attribute.class("flex items-center mb-2")], [
- html.input([
- attribute.type_("checkbox"),
- attribute.disabled(True),
- attribute.class("mr-2 accent-primary"),
- ]),
- html.span([], entry_to_conversion(content)),
- ])
- }
- |> element_to_raw_djotstring
- }
- False ->
- case
- string.starts_with(trimmed, "- [x] ")
- || string.starts_with(trimmed, "- [X] ")
- {
- True -> {
- let content = string.drop_start(trimmed, 6)
- {
- html.div([attribute.class("flex items-center mb-2")], [
- html.input([
- attribute.type_("checkbox"),
- attribute.checked(True),
- attribute.disabled(True),
- attribute.class("mr-2 accent-primary"),
- ]),
- html.span([], entry_to_conversion(content)),
- ])
- }
- |> element_to_raw_djotstring
- }
- False -> line
- }
- }
- })
- |> string.join("\n")
-}
-
-fn preprocess_autolinks(djot: String) -> String {
- // Convert to [url](url) format for proper Djot parsing
- djot
- |> string.replace(" string.replace(" string.replace(" string.replace(" process_autolink_markers()
-}
-
-fn process_autolink_markers(input: String) -> String {
- case string.split_once(input, "🔗AUTOLINK🔗") {
- Ok(#(before, after)) -> {
- case string.split_once(after, ">") {
- Ok(#(url, rest)) ->
- before
- <> "["
- <> url
- <> "]("
- <> url
- <> ")"
- <> process_autolink_markers(rest)
- Error(_) -> before <> "<" <> after
- // Restore if no closing >
- }
- }
- Error(_) -> input
- // No markers found
- }
-}
-
-fn preprocess_ordered_lists(djot: String) -> String {
- let lines = string.split(djot, "\n")
- process_ordered_list_lines(lines, False, [])
- |> string.join("\n")
-}
-
-fn process_ordered_list_lines(
- lines: List(String),
- in_list: Bool,
- list_buffer: List(String),
-) -> List(String) {
- case lines {
- [] ->
- case in_list {
- True -> [convert_ordered_list_to_raw(list.reverse(list_buffer))]
- False -> []
- }
-
- [line, ..rest] -> {
- let trimmed = string.trim(line)
- let is_list_item = is_ordered_list_item(trimmed)
-
- case in_list, is_list_item {
- True, True ->
- process_ordered_list_lines(rest, True, [line, ..list_buffer])
-
- True, False -> [
- convert_ordered_list_to_raw(list.reverse(list_buffer)),
- line,
- ..process_ordered_list_lines(rest, False, [])
- ]
-
- False, True -> process_ordered_list_lines(rest, True, [line])
-
- False, False -> [line, ..process_ordered_list_lines(rest, False, [])]
- }
- }
- }
-}
-
-fn is_ordered_list_item(line: String) -> Bool {
- case string.split_once(line, ". ") {
- Ok(#(num_str, _)) -> {
- case int.parse(string.trim(num_str)) {
- Ok(_) -> True
- Error(_) -> False
- }
- }
- Error(_) -> False
- }
-}
-
-fn convert_ordered_list_to_raw(lines: List(String)) -> String {
- case lines {
- [] -> ""
- _ -> {
- let list_items =
- lines
- |> list.map(fn(line) {
- case string.split_once(string.trim(line), ". ") {
- Ok(#(_, content)) -> html.li([], entry_to_conversion(content))
- Error(_) -> html.li([], [html.text(line)])
- }
- })
-
- html.ol([attribute.class("list-decimal mb-4")], list_items)
- |> element_to_raw_djotstring
- }
- }
-}
-
-fn preprocess_multiline_images(djot: String) -> String {
- string.split(djot, "\n")
- |> process_multiline_image_lines([], "")
- |> string.join("\n")
-}
-
-fn process_multiline_image_lines(
- lines: List(String),
- processed: List(String),
- buffer: String,
-) -> List(String) {
- case lines {
- [] -> list.reverse(processed)
-
- [line, ..rest] -> {
- let has_image_start = string.contains(line, "![")
- let has_image_end = string.contains(line, ")")
- let has_incomplete_image = has_image_start && !has_image_end
-
- case buffer {
- // No buffer yet
- "" -> {
- case has_incomplete_image {
- // Start collecting a multiline image
- True -> process_multiline_image_lines(rest, processed, line)
-
- // Normal line
- False ->
- process_multiline_image_lines(rest, [line, ..processed], "")
- }
- }
-
- // Continue collecting an existing multiline image
- _ -> {
- // If we have a buffer, we're in the middle of processing a multiline image
- // We need to combine all lines until we find the closing parenthesis
- case has_image_end {
- // Complete the image and add to processed
- True -> {
- let complete_line = buffer <> " " <> line
- process_multiline_image_lines(
- rest,
- [complete_line, ..processed],
- "",
- )
- }
- // Continue buffering
- False -> {
- process_multiline_image_lines(
- rest,
- processed,
- buffer <> " " <> line,
- )
- }
- }
- }
- }
- }
- }
-}
-
-// Process heading attributes like {#id} before headings
-fn preprocess_heading_attributes(djot: String) -> String {
- let lines = string.split(djot, "\n")
- let processed = process_heading_attribute_lines(lines, [])
- string.join(processed, "\n")
-}
-
-fn process_heading_attribute_lines(
- lines: List(String),
- processed: List(String),
-) -> List(String) {
- case lines {
- [] -> list.reverse(processed)
-
- [line, ..rest] -> {
- // Check for attribute pattern {#something} followed by heading
- let is_attribute =
- string.starts_with(string.trim(line), "{#")
- && string.contains(line, "}")
-
- case is_attribute, rest {
- True, [next, ..next_rest] -> {
- let next_trimmed = string.trim(next)
- // Check if next line is a heading
- case
- string.starts_with(next_trimmed, "# ")
- || string.starts_with(next_trimmed, "## ")
- || string.starts_with(next_trimmed, "### ")
- || string.starts_with(next_trimmed, "#### ")
- || string.starts_with(next_trimmed, "##### ")
- || string.starts_with(next_trimmed, "###### ")
- {
- True -> {
- // Extract ID from {#id}
- case string.split_once(line, "{#") {
- Ok(#(_, with_id)) -> {
- case string.split_once(with_id, "}") {
- Ok(#(id, _)) -> {
- let modified_heading = next <> " {#" <> id <> "}"
- process_heading_attribute_lines(next_rest, [
- modified_heading,
- ..processed
- ])
- }
- Error(_) ->
- process_heading_attribute_lines(rest, [line, ..processed])
- }
- }
- Error(_) ->
- process_heading_attribute_lines(rest, [line, ..processed])
- }
- }
-
- False -> process_heading_attribute_lines(rest, [line, ..processed])
- }
- }
-
- _, _ -> process_heading_attribute_lines(rest, [line, ..processed])
- }
- }
- }
-}
-
-// Clean heading text by removing any {#id} attributes
-fn clean_heading_text(inlines: List(Inline)) -> List(Inline) {
- inlines
- |> list.map(fn(inline) {
- case inline {
- Text(text) -> {
- // Remove {#id} pattern from the text
- case string.split_once(text, " {#") {
- Ok(#(content, _)) -> Text(content)
- Error(_) -> inline
- }
- }
- _ -> inline
- }
- })
-}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/ownit.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/ownit.gleam
deleted file mode 100644
index a341bba..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/ownit.gleam
+++ /dev/null
@@ -1,243 +0,0 @@
-//// # Ownit layout
-////
-//// Custom layout for Cynthia Mini.
-//// Allows to create own templates in Handlebars.
-////
-//// Ownit is a unique layout in the sense that, it does not contain a layout, it's merely a wrap around Handlebars to allow own templates to be used in Cynthia Mini.
-////
-//// ## Writing templates for ownit
-////
-//// Writing templates for ownit can be done in the [Handlebars](https://handlebarsjs.com/) language.
-//// Your template should be stored under `[variables] -> ownit_template` as a `"string"` or as a `{ path = "filename.hbs" }` or `{ url = "some-site.com/name.hbs" }` url.
-////
-//// ### Available context variables:
-////
-//// - `body`: Contains the content body, for example the text from your blog post.
-//// etc: More to come!
-
-import cynthia_websites_mini_client/messages
-import cynthia_websites_mini_client/model_type
-import cynthia_websites_mini_client/pottery/oven
-import gleam/dict.{type Dict}
-import gleam/dynamic
-import gleam/dynamic/decode.{type Dynamic}
-import gleam/javascript/array.{type Array}
-import gleam/list
-import gleam/option.{None}
-import gleam/result
-import lustre/element.{type Element}
-
-pub fn main(
- from content: Element(messages.Msg),
- with variables: Dict(String, Dynamic),
- store model: model_type.Model,
- is_post is_post: Bool,
-) {
- let #(
- title,
- description,
- site_name,
- category,
- date_modified,
- date_published,
- tags,
- ): #(String, String, String, String, String, String, Array(String)) = case
- is_post
- {
- True -> {
- let assert Ok(title) =
- dict.get(variables, "title")
- |> result.unwrap(dynamic.from(None))
- |> decode.run(decode.string)
- let assert Ok(description) =
- dict.get(variables, "description_html")
- |> result.unwrap(dynamic.from(None))
- |> decode.run(decode.string)
- let assert Ok(site_name) =
- dict.get(variables, "global_site_name")
- |> result.unwrap(dynamic.from(None))
- |> decode.run(decode.string)
- let assert Ok(category) =
- dict.get(variables, "category")
- |> result.unwrap(dynamic.from(None))
- |> decode.run(decode.string)
- let assert Ok(date_modified) =
- dict.get(variables, "date_modified")
- |> result.unwrap(dynamic.from(None))
- |> decode.run(decode.string)
- let assert Ok(date_published) =
- dict.get(variables, "date_published")
- |> result.unwrap(dynamic.from(None))
- |> decode.run(decode.string)
- let assert Ok(tags) =
- dict.get(variables, "tags")
- |> result.unwrap(dynamic.from([]))
- |> decode.run(decode.list(decode.string))
- let tags = tags |> array.from_list
- #(
- title,
- description,
- site_name,
- category,
- date_modified,
- date_published,
- tags,
- )
- }
- False -> {
- let assert Ok(title) =
- dict.get(variables, "title")
- |> result.unwrap(dynamic.from(None))
- |> decode.run(decode.string)
- let assert Ok(description) =
- dict.get(variables, "description_html")
- |> result.unwrap(dynamic.from(None))
- |> decode.run(decode.string)
- let assert Ok(site_name) =
- dict.get(variables, "global_site_name")
- |> result.unwrap(dynamic.from(None))
- |> decode.run(decode.string)
- let category = ""
- let date_modified = ""
- let date_published = ""
- let tags = [] |> array.from_list
- #(
- title,
- description,
- site_name,
- category,
- date_modified,
- date_published,
- tags,
- )
- }
- }
-
- let menu_map = fn(item: model_type.MenuItem) {
- let to = case item.to {
- "/" <> _ -> {
- // If the link starts with a slash, we assume it's a local link.
- "#" <> item.to
- }
- "!" <> _ -> {
- // If the link starts with an exclamation mark, we assume it's a local link.
- "#" <> item.to
- }
- _ -> {
- // Otherwise, we keep the link as is.
- item.to
- }
- }
-
- [item.name, to] |> array.from_list
- }
-
- let menu_1_items = {
- dict.get(model.computed_menus, 1)
- |> result.unwrap([])
- |> list.map(menu_map)
- |> array.from_list
- }
- let menu_2_items = {
- dict.get(model.computed_menus, 2)
- |> result.unwrap([])
- |> list.map(menu_map)
- |> array.from_list
- }
- let menu_3_items = {
- dict.get(model.computed_menus, 3)
- |> result.unwrap([])
- |> list.map(menu_map)
- |> array.from_list
- }
-
- case get_template(model) {
- Ok(template) -> {
- case
- {
- OwnitCtx(
- content: content |> element.to_string(),
- is_post:,
- title:,
- description:,
- site_name:,
- category:,
- date_modified:,
- date_published:,
- tags:,
- menu_1_items:,
- menu_2_items:,
- menu_3_items:,
- )
- |> context_into_template_run(template, _)
- }
- {
- Ok(html_) -> element.unsafe_raw_html("div", "div", [], html_)
- Error(_) ->
- oven.error(
- "Could not parse context into the Handlebars template from the configurated variable at 'ownit_template'.",
- recoverable: True,
- )
- }
- }
- Error(error_message) -> {
- oven.error(error_message, recoverable: False)
- }
- }
-}
-
-fn get_template(model: model_type.Model) {
- use template_string_dynamic <- result.try(result.replace_error(
- dict.get(model.other, "config_ownit_template"),
- "An error occurred while loading the Handlebars template from the configurated variable at 'ownit_template'.",
- ))
- use template_string <- result.try(result.replace_error(
- decode.run(template_string_dynamic, decode.string),
- "An error occurred while trying to decode the Handlebars template from the configurated variable at 'ownit_template'.",
- ))
- compile_template_string(template_string)
- |> result.replace_error(
- "Could not compile the Handlebars template from the configurated variable at 'ownit_template'.",
- )
-}
-
-/// Context sent into Handlebars template, obviously needs to be generated first. Is translated into an Ecmascript object by FFI.
-type OwnitCtx {
- OwnitCtx(
- /// JS: string
- content: String,
- /// JS: boolean
- is_post: Bool,
- /// JS: string
- title: String,
- /// JS: string
- description: String,
- /// JS: string
- site_name: String,
- /// JS: string
- category: String,
- /// JS: string
- date_modified: String,
- /// JS: string
- date_published: String,
- /// JS: string[]
- tags: Array(String),
- /// JS: [string, string][]
- menu_1_items: Array(Array(String)),
- /// JS: [string, string][]
- menu_2_items: Array(Array(String)),
- /// JS: [string, string][]
- menu_3_items: Array(Array(String)),
- )
-}
-
-@external(javascript, "./ownit_ffi", "compile_template_string")
-fn compile_template_string(in: String) -> Result(CompiledTemplate, Nil)
-
-type CompiledTemplate
-
-@external(javascript, "./ownit_ffi", "context_into_template_run")
-fn context_into_template_run(
- template: CompiledTemplate,
- context: OwnitCtx,
-) -> Result(String, Nil)
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/ownit_ffi.ts b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/ownit_ffi.ts
deleted file mode 100644
index 00e201e..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/ownit_ffi.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-// Ownit layout FFI module
-// Ownit is the only layout that has it's own FFI implementations, since Gleam doesn't have any direct bindings to Handlebars.js
-// And well, I'd like to support full Handlebars :shrug:
-
-import Handlebars from "handlebars";
-import { Ok, Error } from "../../../../prelude";
-
-export function compile_template_string(template_string: string) {
- try {
- return new Ok(Handlebars.compile(template_string));
- } catch (e) {
- console.error("Error while compiling Handlebars template string:", e);
- return new Error(null);
- }
-}
-
-export function context_into_template_run(
- template: HandlebarsTemplateDelegate,
- ctx_record: any,
-) {
- const ctx = turn_gleam_record_into_js_object(ctx_record);
- try {
- return new Ok(template(ctx));
- } catch (e) {
- console.error("Error while running Handlebars template with context:", e);
- return new Error(null);
- }
-}
-
-interface context {
- body: string;
- is_post: boolean;
- title: string;
- description: string;
- site_name: string;
- category: string;
- date_modified: string;
- date_published: string;
- tags: string[];
- menu_1_items: [string, string][];
- menu_2_items: [string, string][];
- menu_3_items: [string, string][];
-}
-
-function turn_gleam_record_into_js_object(record: any): context {
- return {
- body: record.content,
- is_post: record.is_post,
- title: record.title,
- description: record.description,
- site_name: record.site_name,
- category: record.category,
- date_modified: record.date_modified,
- date_published: record.date_published,
- tags: record.tags,
- menu_1_items: record.menu_1_items || [],
- menu_2_items: record.menu_2_items || [],
- menu_3_items: record.menu_3_items || [],
- };
-}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/jsonld.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/jsonld.gleam-
similarity index 100%
rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/jsonld.gleam
rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/jsonld.gleam-
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/sitemap.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/sitemap.gleam-
similarity index 100%
rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/sitemap.gleam
rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/sitemap.gleam-
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/ui.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/ui.gleam
deleted file mode 100644
index d9e4b73..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/ui.gleam
+++ /dev/null
@@ -1,19 +0,0 @@
-pub const footer = "Made into this website with Cynthia Mini"
-
-/// The entire of the 404 page.
-pub fn notfoundbody() -> String {
- "
-
-
-
404!
-
Uh-oh, that page cannot be found.
-
-
-
-
-
-
-
- "
- <> footer
-}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/utils.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/utils.gleam
deleted file mode 100644
index 9255da7..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/utils.gleam
+++ /dev/null
@@ -1,82 +0,0 @@
-import gleam/float
-import gleam/http
-import gleam/http/request.{type Request}
-import gleam/order
-import gleam/string
-import gleam/time/timestamp
-import plinth/browser/window
-
-pub fn phone_home() -> Request(String) {
- request.new()
- |> request.set_scheme({
- let origin = window.origin()
- case origin {
- "http://" <> _ -> http.Http
- "https://" <> _ -> http.Https
- _ -> http.Https
- }
- })
- |> request.set_host(get_window_host())
-}
-
-pub fn phone_home_url() -> String {
- let origin = window.origin()
- let host = get_window_host()
-
- case origin {
- "http://" <> _ -> "http://" <> host
- "https://" <> _ -> "https://" <> host
- _ -> "https://" <> host
- }
- <> window.pathname()
- |> phone_home_lessener
-}
-
-fn phone_home_lessener(in: String) -> String {
- case string.ends_with(in, "//") {
- True -> phone_home_lessener(in |> string.drop_end(1))
- False ->
- case string.ends_with(in, "index.html") {
- True -> phone_home_lessener(in |> string.replace("index.html", ""))
- False ->
- case string.ends_with(in, "index.html/") {
- True -> phone_home_lessener(in |> string.replace("index.html", ""))
- False -> in
- }
- }
- }
-}
-
-@external(javascript, "./utils_ffi.ts", "getWindowHost")
-pub fn get_window_host() -> String
-
-pub fn now() -> Int {
- let now =
- timestamp.system_time()
- |> timestamp.to_unix_seconds
- |> float.truncate
- now
-}
-
-/// A natural compare
-pub fn compare_so_natural(a: String, b: String) -> order.Order {
- case compares(a, b) {
- "eq" -> order.Eq
- "lt" -> order.Lt
- "gt" -> order.Gt
- _ -> panic as "compare_so_natural failed?? This should never happen"
- }
-}
-
-@external(javascript, "./utils_ffi.ts", "compares")
-fn compares(a: String, b: String) -> String
-
-/// A js FFI implementation of string.trim, to also handle stuff like nbsp or emsp
-@external(javascript, "./utils_ffi.ts", "trims")
-pub fn js_trim(a: String) -> String
-
-@external(javascript, "./utils_ffi.ts", "set_theme_body")
-pub fn set_theme_body(themename: String) -> Nil
-
-@external(javascript, "./utils_ffi.ts", "whatever_timestamp_to_unix_millis")
-pub fn whatever_timestamp_to_unix_millis(ts: String) -> Int
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/utils_ffi.ts b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/utils_ffi.ts
deleted file mode 100644
index 686df06..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/utils_ffi.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-export function getWindowHost() {
- return window.location.host;
-}
-
-export function compares(a: string, b: string): string {
- const result = a.localeCompare(b, undefined, { numeric: true });
- if (result < 0) {
- return "lt";
- } else if (result > 0) {
- return "gt";
- } else {
- return "eq";
- }
-}
-
-export function trims(str: string) {
- return str.trim();
-}
-
-export function set_theme_body(themename: string) {
- document.body.setAttribute("data-theme", themename);
-}
-
-export function whatever_timestamp_to_unix_millis(ts: string | number): number {
- if (typeof ts === "number") {
- // assume it's already unix millis
- return ts;
- } else if (typeof ts === "string") {
- // try to parse as ISO 8601 string
- const parsed = Date.parse(ts);
- if (!isNaN(parsed)) {
- return parsed;
- } else {
- return 0;
- }
- } else {
- return 0;
- }
-}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/version_ffi.ts b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/version_ffi.ts
deleted file mode 100644
index d403743..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/version_ffi.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import { version } from "package.json";
-export function my_own_version(): string {
- return version;
-}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/view.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/view.gleam
deleted file mode 100644
index 625e402..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/view.gleam
+++ /dev/null
@@ -1,154 +0,0 @@
-import cynthia_websites_mini_client/contenttypes
-import cynthia_websites_mini_client/dom
-import cynthia_websites_mini_client/messages.{type Msg}
-import cynthia_websites_mini_client/model_type.{type Model}
-import cynthia_websites_mini_client/pageloader/postlistloader
-import cynthia_websites_mini_client/pottery
-import cynthia_websites_mini_client/pottery/oven
-import cynthia_websites_mini_client/utils
-import gleam/list
-import gleam/option.{None, Some}
-import gleam/result
-import houdini
-import lustre/attribute
-import lustre/element.{type Element}
-import lustre/element/html
-import odysseus
-
-pub fn main(model: Model) -> Element(Msg) {
- case model.status {
- Ok(_) -> {
- case model.complete_data {
- None -> initial_view()
- Some(complete_data) -> {
- let content =
- complete_data.content
- |> list.find(fn(content) { { content.permalink == model.path } })
- |> result.lazy_unwrap(fn() {
- contenttypes.Content(
- filename: "notfound.dj",
- title: "Page not found",
- description: model.path,
- layout: "theme",
- permalink: "404",
- inner_plain: "# 404!\n\nThe page you are looking for does not exist.",
- data: contenttypes.PageData([], True),
- )
- })
- let content = case model.path {
- "!" <> a -> {
- let #(tit, desc) = case content.filename {
- "notfound.dj" -> #(None, None)
- _ -> #(Some(content.title), Some(content.description))
- }
- case a {
- "/category/" <> category -> {
- let title =
- option.unwrap(tit, "Posts in category: " <> category)
- let description =
- option.unwrap(
- desc,
- "A postlist of all posts in the category: " <> category,
- )
-
- contenttypes.Content(
- title:,
- description:,
- layout: "default",
- permalink: model.path,
- filename: "postlist.html",
- data: contenttypes.PageData([], False),
- inner_plain: postlistloader.postlist_by_category(
- model,
- category,
- )
- |> element.to_string,
- )
- }
- "/tag/" <> tag -> {
- let title = option.unwrap(tit, "Posts with tag: " <> tag)
- let description =
- option.unwrap(
- desc,
- "A postlist of all posts tagged with " <> tag,
- )
- contenttypes.Content(
- title:,
- description:,
- layout: "default",
- permalink: model.path,
- filename: "postlist.html",
- data: contenttypes.PageData([], True),
- inner_plain: postlistloader.postlist_by_tag(model, tag)
- |> element.to_string,
- )
- }
- "/search/" <> search_term -> {
- let title =
- option.unwrap(tit, "Search results for: " <> search_term)
- let description = option.unwrap(desc, "")
- contenttypes.Content(
- title:,
- description:,
- layout: "default",
- permalink: model.path,
- filename: "postlist.html",
- data: contenttypes.PageData([], False),
- inner_plain: postlistloader.postlist_by_search_term(
- model,
- search_term,
- )
- |> element.to_string,
- )
- }
- _ -> {
- let title = option.unwrap(tit, "All posts")
- let description = "A postlist of all posts."
- contenttypes.Content(
- title:,
- description:,
- layout: "default",
- permalink: model.path,
- filename: "postlist.html",
- data: contenttypes.PageData([], False),
- inner_plain: postlistloader.postlist_all(model)
- |> element.to_string,
- )
- }
- }
- }
- _ -> content
- }
- let assert Ok(_) =
- dom.push_title(
- houdini.escape(utils.js_trim(odysseus.unescape(content.title))),
- )
- pottery.render_content(model, content)
- }
- }
- }
- Error(error_message) -> oven.error(error_message, True)
- }
-}
-
-pub fn initial_view() -> Element(Msg) {
- let assert Ok(_) = dom.push_title("Cynthia Mini: Loading...")
- html.div(
- [
- attribute.class(
- "absolute mr-auto ml-auto right-0 left-0 bottom-[40VH] top-[40VH] w-fit h-fit",
- ),
- ],
- [
- html.div([attribute.class("card bg-primary text-primary-content w-96")], [
- html.div([attribute.class("card-body")], [
- html.h2([attribute.class("card-title")], [html.text("Cynthia Mini")]),
- html.p([], [html.text("Loading the page you want...")]),
- html.div([attribute.class("card-actions justify-end")], [
- html.span([attribute.class("loading loading-bars loading-lg")], []),
- ]),
- ]),
- ]),
- ],
- )
-}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/site_json.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/site_json.gleam
new file mode 100644
index 0000000..4893ad1
--- /dev/null
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/site_json.gleam
@@ -0,0 +1,140 @@
+//// Site.json gleam type format and en/decoder.
+
+import cynthia_websites_mini_shared/config/v4_1
+import cynthia_websites_mini_shared/config/v4_1/decodes
+import gleam/dict
+import gleam/dynamic/decode
+import gleam/option
+
+/// This is the content of site.json, factually the entire site
+pub type SiteJSON {
+ SiteJSON(
+ config: v4_1.V4p1Mini,
+ // Slug, or a random number if not set and the content
+ content: dict.Dict(String, Content),
+ )
+}
+
+pub fn site_json_decoder() -> decode.Decoder(SiteJSON) {
+ use config <- decode.field("config", decodes.v4p1_mini_dynamic())
+ use content: dict.Dict(String, Content) <- decode.field(
+ "content",
+ decode.dict(decode.string, {
+ use variant <- decode.field("type", decode.string)
+ case variant {
+ "page" -> {
+ use title <- decode.field("title", decode.string)
+ use description <- decode.field("description", decode.string)
+ use layout <- decode.field("layout", decode.optional(decode.string))
+ use content <- decode.field("content", decode.string)
+ use in_menus <- decode.field("in_menus", decode.list(decode.int))
+ use hide_meta_block <- decode.field("hide_meta_block", decode.bool)
+ decode.success(Page(
+ title:,
+ description:,
+ layout:,
+ content:,
+ in_menus:,
+ hide_meta_block:,
+ ))
+ }
+ "post" -> {
+ use title <- decode.field("title", decode.string)
+ use description <- decode.field("description", decode.string)
+ use layout <- decode.field("layout", decode.optional(decode.string))
+ use content <- decode.field("content", decode.string)
+ use date_published <- decode.field("date_published", decode.string)
+ use date_updated <- decode.field("date_updated", decode.string)
+ use category <- decode.field("category", decode.string)
+ use tags <- decode.field("tags", decode.list(decode.string))
+ use mastodon_comments <- field_or(
+ field: "mastodon-comments",
+ decoder: decode.optional({
+ use instance <- decode.field("instance", decode.string)
+ use id <- decode.field("id", decode.string)
+ decode.success(MastodonStatus(instance:, id:))
+ }),
+ otherwise: option.None,
+ )
+
+ decode.success(Post(
+ title:,
+ description:,
+ layout:,
+ content:,
+ date_published:,
+ date_updated:,
+ category:,
+ tags:,
+ mastodon_comments:,
+ ))
+ }
+ _ ->
+ decode.failure(
+ Page("failure", "failure", option.None, "Failure", [], False),
+ "Content",
+ )
+ }
+ }),
+ )
+ decode.success(SiteJSON(config:, content:))
+}
+
+fn field_or(
+ field field: String,
+ decoder field_decoder: decode.Decoder(t),
+ otherwise default: t,
+ next next: fn(t) -> decode.Decoder(final),
+) -> decode.Decoder(final) {
+ use val <- decode.optional_field(
+ field,
+ option.None,
+ decode.optional(field_decoder),
+ )
+ next(val |> option.unwrap(default))
+}
+
+pub type Content {
+ Page(
+ /// Page title
+ title: String,
+ /// Description, converted to HTML beforehand.
+ description: String,
+ /// Layout or default
+ layout: option.Option(String),
+ /// Page content, converted to HTML beforehand.
+ content: String,
+ /// In which menus this page should appear
+ in_menus: List(Int),
+ /// Hide the block with title and description for a page.
+ hide_meta_block: Bool,
+ )
+ Post(
+ /// Page title
+ title: String,
+ /// Description, converted to HTML beforehand.
+ description: String,
+ /// Layout or default
+ layout: option.Option(String),
+ /// Page content, converted to HTML beforehand.
+ content: String,
+ /// Date string -- But it's unchecked
+ /// Stores the date on which the post was published.
+ date_published: String,
+ /// Date string -- But it's unchecked
+ /// # Date updated
+ /// Stores the date on which the post was last updated.
+ date_updated: String,
+ /// Category this post belongs to
+ category: String,
+ /// Tags that belong to this post
+ tags: List(String),
+ /// Mastodon instance and post id to link to for comments.
+ mastodon_comments: option.Option(MastodonStatus),
+ )
+}
+
+/// Mastodon instance and post id to link to for comments.
+pub type MastodonStatus {
+ MastodonStatus(instance: String, id: String)
+}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4.gleam
new file mode 100644
index 0000000..4e65fdc
--- /dev/null
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4.gleam
@@ -0,0 +1,59 @@
+//// Cynthia Mini v4 Config format
+
+import cynthia_websites_mini_shared/config/v4_1
+import gleam/string
+
+pub type V4mini {
+ V4mini(
+ global: V4miniGlobal,
+ integrations: V4miniIntegrations,
+ posts: V4miniPosts,
+ )
+}
+
+pub type V4miniGlobal {
+ V4miniGlobal(
+ theme: String,
+ theme_dark: String,
+ colour: String,
+ site_name: String,
+ site_description: String,
+ )
+}
+
+pub type V4miniIntegrations {
+ V4miniIntegrations(git: Bool, sitemap: String, crawlable_context: Bool)
+}
+
+pub type V4miniPosts {
+ V4miniPosts(comment_repo: String)
+}
+
+pub fn upgrade(in: V4mini) -> v4_1.V4p1Mini {
+ v4_1.V4p1Mini(
+ global: v4_1.V4p1MiniGlobal(
+ site_description: in.global.site_description,
+ theme: in.global.theme,
+ theme_dark: in.global.theme_dark,
+ site_name: in.global.site_name,
+ ),
+ integrations: v4_1.V4p1MiniIntegrations(
+ git: in.integrations.git,
+ sitemap: in.integrations.sitemap,
+ crawlable_context: in.integrations.crawlable_context,
+ ),
+ posts: v4_1.V4p1MiniPosts(comments: {
+ case in.posts.comment_repo |> string.lowercase {
+ "" -> v4_1.CommentsDisabled
+ "mastodon" -> v4_1.CommentsMastodonStored
+ _ -> {
+ case in.posts.comment_repo |> string.split_once("/") {
+ Ok(#(username, repositoryname)) ->
+ v4_1.CommentsGithubStored(username:, repositoryname:)
+ _ -> v4_1.CommentsDisabled
+ }
+ }
+ }
+ }),
+ )
+}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4/decodes.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4/decodes.gleam
new file mode 100644
index 0000000..4b1a184
--- /dev/null
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4/decodes.gleam
@@ -0,0 +1,101 @@
+import cynthia_websites_mini_shared/config/v4
+import gleam/dynamic/decode
+import plinth/javascript/console
+import tom
+
+pub fn v4mini_dynamic() -> decode.Decoder(v4.V4mini) {
+ use global <- decode.field("global", {
+ use theme <- decode.field("theme", decode.string)
+ use theme_dark <- decode.field("theme_dark", decode.string)
+ use colour <- decode.field("colour", decode.string)
+ use site_name <- decode.field("site_name", decode.string)
+ use site_description <- decode.field("site_description", decode.string)
+ decode.success(v4.V4miniGlobal(
+ theme:,
+ theme_dark:,
+ colour:,
+ site_name:,
+ site_description:,
+ ))
+ })
+ use integrations <- decode.field("integrations", {
+ use git <- decode.field("git", decode.bool)
+ use sitemap <- decode.field("sitemap", decode.string)
+ use crawlable_context <- decode.field("crawlable_context", decode.bool)
+ decode.success(v4.V4miniIntegrations(git:, sitemap:, crawlable_context:))
+ })
+ use posts <- decode.field("posts", {
+ use comment_repo <- decode.field("comment_repo", decode.string)
+ decode.success(v4.V4miniPosts(comment_repo:))
+ })
+ decode.success(v4.V4mini(global:, integrations:, posts:))
+}
+
+pub fn v4mini_toml(toml_source: String) -> Result(v4.V4mini, Nil) {
+ case tom.parse(toml_source) {
+ Ok(toml) -> {
+ v4.V4mini(
+ global: {
+ let theme = tom.get_string(toml, ["global", "theme"]) |> unsafe_unwrap
+
+ v4.V4miniGlobal(
+ theme:,
+ theme_dark: tom.get_string(toml, ["global", "theme", "dark"])
+ |> unsafe_unwrap,
+ site_name: tom.get_string(toml, ["global", "site_name"])
+ |> unsafe_unwrap,
+ site_description: tom.get_string(toml, [
+ "global",
+ "site_description",
+ ])
+ |> unsafe_unwrap,
+ colour: tom.get_string(toml, ["global", "colour"])
+ |> unsafe_unwrap,
+ )
+ },
+ integrations: v4.V4miniIntegrations(
+ git: tom.get_bool(toml, [
+ "integrations",
+ "git",
+ ])
+ |> unsafe_unwrap,
+ sitemap: tom.get_string(toml, [
+ "integrations",
+ "sitemap",
+ ])
+ |> unsafe_unwrap,
+ crawlable_context: tom.get_bool(toml, [
+ "integrations",
+ "crawlable_context",
+ ])
+ |> unsafe_unwrap,
+ ),
+ posts: v4.V4miniPosts(comment_repo: {
+ tom.get_string(toml, [
+ "posts",
+ "comment_repo",
+ ])
+ |> unsafe_unwrap
+ }),
+ )
+ |> Ok
+ }
+ // We don't propogate upwards, we give back a Error value but inform here and then exit upstream.
+ Error(_) -> {
+ console.log("Could not parse TOML!")
+ Error(Nil)
+ }
+ }
+}
+
+fn unsafe_unwrap(v: Result(s, _)) {
+ case v {
+ Ok(a) -> a
+ Error(_) -> {
+ let d =
+ "Encountered invalid value in legacy config, Cynthia Mini won't try to recover for this in legacy configs."
+ console.error(d)
+ panic as d
+ }
+ }
+}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1.gleam
new file mode 100644
index 0000000..db9e30a
--- /dev/null
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1.gleam
@@ -0,0 +1,49 @@
+//// Cynthia v4.1 [external] Config format
+
+pub type V4p1Mini {
+ V4p1Mini(
+ global: V4p1MiniGlobal,
+ integrations: V4p1MiniIntegrations,
+ posts: V4p1MiniPosts,
+ )
+}
+
+pub type V4p1MiniGlobal {
+ V4p1MiniGlobal(
+ theme: String,
+ theme_dark: String,
+ site_name: String,
+ site_description: String,
+ )
+}
+
+pub type V4p1MiniIntegrations {
+ V4p1MiniIntegrations(git: Bool, sitemap: String, crawlable_context: Bool)
+}
+
+pub type V4p1MiniPosts {
+ V4p1MiniPosts(comments: V4p1MiniPostsComments)
+}
+
+pub type V4p1MiniPostsComments {
+ CommentsMastodonStored
+ CommentsGithubStored(username: String, repositoryname: String)
+ CommentsDisabled
+}
+
+pub fn new() -> V4p1Mini {
+ V4p1Mini(
+ global: V4p1MiniGlobal(
+ theme: "autumn",
+ theme_dark: "night",
+ site_name: "My Site",
+ site_description: "A big site on a mini Cynthia!",
+ ),
+ integrations: V4p1MiniIntegrations(
+ git: True,
+ sitemap: "",
+ crawlable_context: False,
+ ),
+ posts: V4p1MiniPosts(comments: CommentsDisabled),
+ )
+}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1/decodes.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1/decodes.gleam
new file mode 100644
index 0000000..85aa898
--- /dev/null
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1/decodes.gleam
@@ -0,0 +1,194 @@
+import cynthia_websites_mini_shared/config/v4
+import cynthia_websites_mini_shared/config/v4/decodes
+import cynthia_websites_mini_shared/config/v4_1
+import gleam/dynamic/decode
+import gleam/float
+import gleam/int
+import gleam/option
+import gleam/result
+import gleam/string
+import plinth/javascript/console
+import tom
+
+pub fn v4p1_mini_dynamic() -> decode.Decoder(v4_1.V4p1Mini) {
+ use global <- decode.field("global", {
+ use theme <- decode.field("theme", decode.string)
+ use theme_dark <- decode.field("theme_dark", decode.string)
+ use site_name <- decode.field("site_name", decode.string)
+ use site_description <- decode.field("site_description", decode.string)
+ decode.success(v4_1.V4p1MiniGlobal(
+ theme:,
+ theme_dark:,
+ site_name:,
+ site_description:,
+ ))
+ })
+ use integrations <- decode.field("integrations", {
+ use git <- decode.field("git", decode.bool)
+ use sitemap <- decode.field("sitemap", decode.string)
+ use crawlable_context <- decode.field("crawlable_context", decode.bool)
+ decode.success(v4_1.V4p1MiniIntegrations(git:, sitemap:, crawlable_context:))
+ })
+ use posts <- decode.field("posts", {
+ use comments <- decode.field("comments", {
+ use comments <- field_or(
+ field: "comments",
+ decoder: {
+ use variant <- decode.field("store", decode.string)
+ case variant |> string.lowercase {
+ "mastodon" -> decode.success(v4_1.CommentsMastodonStored)
+ "github" -> {
+ use username <- decode.field("username", decode.string)
+ use repositoryname <- decode.field(
+ "repositoryname",
+ decode.string,
+ )
+ decode.success(v4_1.CommentsGithubStored(
+ username:,
+ repositoryname:,
+ ))
+ }
+ "disabled" | "" -> decode.success(v4_1.CommentsDisabled)
+ _ ->
+ decode.failure(
+ v4_1.CommentsDisabled,
+ "v4_1.V4p1MiniPostsComments",
+ )
+ }
+ },
+ otherwise: v4_1.CommentsDisabled,
+ )
+ decode.success(comments)
+ })
+ decode.success(v4_1.V4p1MiniPosts(comments:))
+ })
+ decode.success(v4_1.V4p1Mini(global:, integrations:, posts:))
+}
+
+fn field_or(
+ field field: String,
+ decoder field_decoder: decode.Decoder(t),
+ otherwise default: t,
+ next next: fn(t) -> decode.Decoder(final),
+) -> decode.Decoder(final) {
+ use val <- decode.optional_field(
+ field,
+ option.None,
+ decode.optional(field_decoder),
+ )
+ next(val |> option.unwrap(default))
+}
+
+pub fn vp4p1mini_toml(toml_source: String) {
+ case tom.parse(toml_source) {
+ Ok(toml) -> {
+ let edition =
+ tom.get_string(toml, ["edition"]) |> result.map(string.lowercase)
+ let version =
+ result.or(tom.get_float(toml, ["version"]), {
+ tom.get_int(toml, ["version"]) |> result.map(int.to_float)
+ })
+
+ case edition, version {
+ Ok("mini"), Ok(4.1) -> {
+ v4_1.V4p1Mini(
+ global: {
+ let theme =
+ tom.get_string(toml, ["global", "theme", "default"])
+ |> result.unwrap({
+ tom.get_string(toml, ["global", "theme"])
+ |> result.unwrap(v4_1.new().global.theme)
+ })
+ v4_1.V4p1MiniGlobal(
+ theme:,
+ theme_dark: result.unwrap(
+ tom.get_string(toml, ["global", "theme", "dark"]),
+ theme,
+ ),
+ site_name: tom.get_string(toml, ["global", "site_name"])
+ |> result.unwrap(v4_1.new().global.site_name),
+ site_description: tom.get_string(toml, [
+ "global",
+ "site_description",
+ ])
+ |> result.unwrap(v4_1.new().global.site_description),
+ )
+ },
+ integrations: v4_1.V4p1MiniIntegrations(
+ git: tom.get_bool(toml, [
+ "integrations",
+ "git",
+ ])
+ |> result.unwrap(v4_1.new().integrations.git),
+ sitemap: tom.get_string(toml, [
+ "integrations",
+ "sitemap",
+ ])
+ |> result.unwrap(v4_1.new().integrations.sitemap),
+ crawlable_context: tom.get_bool(toml, [
+ "integrations",
+ "crawlable_context",
+ ])
+ |> result.unwrap(v4_1.new().integrations.crawlable_context),
+ ),
+ posts: v4_1.V4p1MiniPosts(comments: {
+ case
+ tom.get_string(toml, ["posts", "comments", "store"])
+ |> result.unwrap("disabled")
+ {
+ "mastodon" -> v4_1.CommentsMastodonStored
+ "github" -> {
+ case
+ tom.get_string(toml, ["posts", "comments", "username"]),
+ tom.get_string(toml, ["posts", "comments", "repositoryname"])
+ {
+ Ok(username), Ok(repositoryname) ->
+ v4_1.CommentsGithubStored(username:, repositoryname:)
+ _, _ -> v4_1.new().posts.comments
+ }
+ }
+ _ -> v4_1.CommentsDisabled
+ }
+ }),
+ )
+ |> Ok
+ }
+ Ok("mini"), Ok(4.0) -> {
+ decodes.v4mini_toml(toml_source) |> result.map(v4.upgrade)
+ }
+ Ok(_), Error(_) | Error(_), Ok(_) -> {
+ console.error("Unknown combination of edition and version.")
+ Error(Nil)
+ }
+ Error(_), Error(_) -> {
+ console.log("Could not parse TOML!")
+ Error(Nil)
+ }
+ Ok(edition), Ok(version) -> {
+ console.error(
+ "Config version "
+ <> version |> float.to_string()
+ <> " with edition '"
+ <> edition
+ <> "' is NOT supported by this version of Cynthia."
+ <> "\n Usually this means one of these options:"
+ <> "\n - it was written for a different edition"
+ <> "\n - it is invalid"
+ <> "\n - or this version of cynthia is too old to understand this file."
+ <> case edition == "mini" {
+ True ->
+ "\n\n\n It seems to be that last option, since the edition it is written for, does match 'mini'."
+ False -> ""
+ },
+ )
+ Error(Nil)
+ }
+ }
+ }
+ // We don't propogate upwards, we give back a Error value but inform here and then exit upstream.
+ Error(_) -> {
+ console.log("Could not parse TOML!")
+ Error(Nil)
+ }
+ }
+}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1/encodes.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1/encodes.gleam
new file mode 100644
index 0000000..e4fc3e9
--- /dev/null
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1/encodes.gleam
@@ -0,0 +1,95 @@
+import cynthia_websites_mini_shared/config/v4_1
+import gleam/bool
+import gleam/json
+
+pub fn v4p1_mini_json(v4p1_mini: v4_1.V4p1Mini) -> json.Json {
+ json.object([
+ #(
+ "global",
+ json.object([
+ #("theme", json.string(v4p1_mini.global.theme)),
+ #("theme_dark", json.string(v4p1_mini.global.theme_dark)),
+ #("site_name", json.string(v4p1_mini.global.site_name)),
+ #("site_description", json.string(v4p1_mini.global.site_description)),
+ ]),
+ ),
+ #(
+ "integrations",
+ json.object([
+ #("git", json.bool(v4p1_mini.integrations.git)),
+ #("sitemap", json.string(v4p1_mini.integrations.sitemap)),
+ #(
+ "crawlable_context",
+ json.bool(v4p1_mini.integrations.crawlable_context),
+ ),
+ ]),
+ ),
+ #("posts", {
+ json.object([
+ #("comments", case v4p1_mini.posts.comments {
+ v4_1.CommentsMastodonStored ->
+ json.object([
+ #("store", json.string("mastodon")),
+ ])
+ v4_1.CommentsGithubStored(username:, repositoryname:) ->
+ json.object([
+ #("store", json.string("github")),
+ #("username", json.string(username)),
+ #("repositoryname", json.string(repositoryname)),
+ ])
+ v4_1.CommentsDisabled ->
+ json.object([
+ #("store", json.string("disabled")),
+ ])
+ }),
+ ])
+ }),
+ ])
+}
+
+pub fn v4p1_mini_toml(v4p1_mini: v4_1.V4p1Mini) -> String {
+ "# Do not edit these variables! It is set by Cynthia to tell it's config format apart.
+ config.edition=\"mini\"
+ config.version=4.1
+ [global]
+ # Theme to use for light mode - default themes: autumn, default
+ # Theme to use for dark mode - default themes: night, default-dark
+ theme = { default = \""
+ <> v4p1_mini.global.theme
+ <> "\", dark = \""
+ <> v4p1_mini.global.theme_dark
+ <> "\" }
+ # Your website's name, displayed in various places
+ site_name = \""
+ <> v4p1_mini.global.site_name
+ <> "\"
+ # A brief description of your website
+ site_description = \""
+ <> v4p1_mini.global.site_description
+ <> "\"
+
+ [integrations]
+ # Enable git integration for the website
+ # This will allow Cynthia Mini to detect the git repository
+ # For example linking to the commit hash in the footer
+ git = "
+ <> v4p1_mini.integrations.git |> bool.to_string
+ <> "
+
+ # Enable sitemap generation
+ # This will generate a sitemap.xml file in the root of the website
+ #
+ # You will need to enter the base URL of your website in the sitemap variable below.
+ # If your homepage is at \"https://example.com/#/\", then the sitemap variable should be set to \"https://example.com\".
+ # If you do not want to use a sitemap, set this to \"false\", or leave it empty (\"\"), you can also remove the sitemap variable altogether.
+ sitemap = \""
+ <> v4p1_mini.integrations.sitemap
+ <> "\"
+
+ # Enable crawlable context (JSON-LD injection)
+ # This will allow search engines to crawl the website, and makes it
+ # possible for the website to be indexed by search engine and LLMs.
+ crawlable_context = "
+ <> v4p1_mini.integrations.crawlable_context |> bool.to_string
+ <> ""
+}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/dom.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/ffi.gleam
similarity index 58%
rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/dom.gleam
rename to cynthia_websites_mini_client/src/cynthia_websites_mini_shared/ffi.gleam
index b73f03b..13fdfd2 100644
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/dom.gleam
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/ffi.gleam
@@ -3,7 +3,7 @@ import plinth/browser/document
import plinth/browser/element
pub fn push_title(title: String) -> Result(Nil, String) {
- use title_element <- result.then(
+ use title_element <- result.try(
document.query_selector("title")
|> result.replace_error("No title element found"),
)
@@ -22,23 +22,38 @@ pub fn push_title(title: String) -> Result(Nil, String) {
Ok(Nil)
}
+@external(javascript, "./ts_ffi.ts", "my_own_version")
+pub fn version() -> String
+
/// Get the color scheme of the user's system (media query)
-@external(javascript, "./dom.ts", "get_color_scheme")
+@external(javascript, "./ts_ffi.ts", "get_color_scheme")
pub fn get_color_scheme() -> String
/// Set the data attribute of an element
-@external(javascript, "./dom.ts", "set_data")
+@external(javascript, "./ts_ffi.ts", "set_data")
pub fn set_data(element: element.Element, key: String, value: String) -> Nil
/// Set the hash of the window
-@external(javascript, "./dom.ts", "set_hash")
+@external(javascript, "./ts_ffi.ts", "set_hash")
pub fn set_hash(hash: String) -> Nil
/// Get innerhtml of an element
-@external(javascript, "./dom.ts", "get_inner_html")
+@external(javascript, "./ts_ffi.ts", "get_inner_html")
pub fn get_inner_html(element: element.Element) -> String
/// jsonify_string
/// Convert a string to a JSON safe string
-@external(javascript, "./dom.ts", "jsonify_string")
+@external(javascript, "./ts_ffi.ts", "jsonify_string")
pub fn jsonify_string(str: String) -> Result(String, Nil)
+
+@external(javascript, "./ts_ffi.ts", "destroy_comment_box")
+pub fn destroy_comment_box() -> Nil
+
+@external(javascript, "./ts_ffi.ts", "apply_styles_to_comment_box")
+pub fn comment_box_forced_styles() -> Nil
+
+@external(javascript, "./ts_ffi.ts", "browse")
+pub fn browse(a: String) -> Nil
+
+@external(javascript, "./ts_ffi.ts", "browse_prompt")
+pub fn browse_prompt(s: String) -> Nil
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/dom.ts b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/ts_ffi.ts
similarity index 87%
rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/dom.ts
rename to cynthia_websites_mini_client/src/cynthia_websites_mini_shared/ts_ffi.ts
index 289ad3d..959ce36 100644
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/dom.ts
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/ts_ffi.ts
@@ -69,3 +69,17 @@ export function jsonify_string(str: string) {
return new Error(null);
}
}
+
+import { version } from "package.json";
+export function my_own_version(): string {
+ return version;
+}
+
+export function browse_prompt(l: string) {
+ if (window.confirm("Leave this page and go to '" + l + "'?")) {
+ browse(l);
+ }
+}
+export function browse(l: string) {
+ window.location.assign(l);
+}
diff --git a/cynthia_websites_mini_client/test/cynthia_websites_mini_client_test.gleam b/cynthia_websites_mini_client/test/cynthia_websites_mini_client_test.gleam
index 2fa852f..fba3c88 100644
--- a/cynthia_websites_mini_client/test/cynthia_websites_mini_client_test.gleam
+++ b/cynthia_websites_mini_client/test/cynthia_websites_mini_client_test.gleam
@@ -1,65 +1,13 @@
-import birdie
-import cynthia_websites_mini_client/configtype
-import cynthia_websites_mini_client/pottery/djotparse
import gleeunit
-import gleeunit/should
-import lustre/element
-import lustre/element/html
-pub fn main() {
+pub fn main() -> Nil {
gleeunit.main()
}
// gleeunit test functions end in `_test`
pub fn hello_world_test() {
- 1
- |> should.equal(1)
-}
-
-pub fn simple_djot_test() {
- "# Hello World\n\nThis is a test paragraph.\n\n- Item 1\n- Item 2"
- |> djotparse.entry_to_conversion()
- |> html.section([], _)
- |> element.to_readable_string
- |> birdie.snap(title: "simple_djot_test")
-}
-
-pub fn djot_with_preprocessing_test() {
- "# Hello World\n\nThis is a test paragraph.\n\n- [ ] Task item\n- [x] Completed task\n\n> This is a blockquote\n\nAnother paragraph."
- |> djotparse.entry_to_conversion()
- |> html.section([], _)
- |> element.to_readable_string
- |> birdie.snap(title: "djot_with_preprocessing_test")
-}
-
-pub fn autolinks_test() {
- "External page example, using the theme list, downloading from "
- |> djotparse.entry_to_conversion()
- |> html.section([], _)
- |> element.to_readable_string
- |> birdie.snap(title: "autolinks_test")
-}
-
-pub fn links_in_preprocessed_items_test() {
- "- [ ] Task with [link](https://example.com)\n- [x] Completed task with [another link](https://test.com)\n\n> Blockquote with [a link](https://blockquote.example)"
- |> djotparse.entry_to_conversion()
- |> html.section([], _)
- |> element.to_readable_string
- |> birdie.snap(title: "links_in_preprocessed_items_test")
-}
-
-pub fn ordered_list_with_links_test() {
- "1. First item with [link](https://first.com)\n2. Second item with [another link](https://second.com)\n3. Third item with **bold** and [link](https://third.com)"
- |> djotparse.entry_to_conversion()
- |> html.section([], _)
- |> element.to_readable_string
- |> birdie.snap(title: "ordered_list_with_links_test")
-}
+ let name = "Joe"
+ let greeting = "Hello, " <> name <> "!"
-pub fn ootb_index_rendering_test() {
- configtype.ootb_index
- |> djotparse.entry_to_conversion()
- |> html.body([], _)
- |> element.to_readable_string
- |> birdie.snap(title: "ootb_index_rendering_test")
+ assert greeting == "Hello, Joe!"
}
diff --git a/cynthia_websites_mini_server/gleam.toml b/cynthia_websites_mini_server/gleam.toml
index 573ce7c..6b522c0 100644
--- a/cynthia_websites_mini_server/gleam.toml
+++ b/cynthia_websites_mini_server/gleam.toml
@@ -18,38 +18,19 @@ typescript_declarations = true
[dependencies]
# Shared dependencies with the client
-gleam_stdlib = "0.59.0"
-plinth = ">= 0.5.9 and < 1.0.0"
-gleam_javascript = "1.0.0"
-gleam_community_colour = "1.4.1"
-
-gleam_json = "2.3.0"
-gleam_http = "3.7.2"
-gleam_fetch = "1.3.0"
+gleam_stdlib = ">= 0.44.0 and < 2.0.0"
+plinth = ">= 0.9.2 and < 1.0.0"
+tom = "1.1.1"
# Other dependencies
argv = "1.0.2"
birl = "1.8.0"
-conversation = "2.0.1"
bungibindies = "1.2.0-rc"
cynthia_websites_mini_client = { path = "../cynthia_websites_mini_client" }
-edit_distance = "2.0.1"
-envoy = "1.0.2"
-filepath = "1.1.2"
-glam = "2.0.2"
-glance = "3.0.0"
-gleam_community_ansi = "1.4.3"
-gleam_regexp = "1.1.1"
-gleam_yielder = "1.1.0"
-gleamy_lights = "2.3.0"
-javascript_mutable_reference = "1.0.0"
-justin = "1.0.1"
-ranger = "1.4.0"
simplifile = "2.2.1"
-tom = "1.1.1"
-
+gleamy_lights = ">= 2.3.1 and < 3.0.0"
+gleam_javascript = ">= 1.0.0 and < 2.0.0"
-webls = "1.5.1"
[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"
birdie = ">= 1.2.7 and < 2.0.0"
diff --git a/cynthia_websites_mini_server/manifest.toml b/cynthia_websites_mini_server/manifest.toml
index af2f9f2..40927bc 100644
--- a/cynthia_websites_mini_server/manifest.toml
+++ b/cynthia_websites_mini_server/manifest.toml
@@ -6,47 +6,32 @@ packages = [
{ name = "birdie", version = "1.3.0", build_tools = ["gleam"], requirements = ["argv", "edit_distance", "filepath", "glance", "gleam_community_ansi", "gleam_stdlib", "justin", "rank", "simplifile", "term_size", "trie_again"], otp_app = "birdie", source = "hex", outer_checksum = "425916385B6CD82A58F54CC39605262A524B169746FC9AD9C799BC76E88F7AF3" },
{ name = "birl", version = "1.8.0", build_tools = ["gleam"], requirements = ["gleam_regexp", "gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "2AC7BA26F998E3DFADDB657148BD5DDFE966958AD4D6D6957DD0D22E5B56C400" },
{ name = "bungibindies", version = "1.2.0-rc", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_stdlib"], otp_app = "bungibindies", source = "hex", outer_checksum = "C1A4DD5D0BE282E4A6F007ECE3FE1477E38CFDF1B6043A5ABAB63C53443AC473" },
- { name = "conversation", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "conversation", source = "hex", outer_checksum = "103DF47463B8432AB713D6643DC17244B9C82E2B172A343150805129FE584A2F" },
- { name = "cynthia_websites_mini_client", version = "1.2.0-rc2", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_fetch", "gleam_http", "gleam_javascript", "gleam_json", "gleam_stdlib", "gleam_time", "houdini", "jot", "lustre", "modem", "odysseus", "plinth", "rsvp"], source = "local", path = "../cynthia_websites_mini_client" },
+ { name = "cynthia_websites_mini_client", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "plinth", "tom"], source = "local", path = "../cynthia_websites_mini_client" },
{ name = "edit_distance", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "edit_distance", source = "hex", outer_checksum = "A1E485C69A70210223E46E63985FA1008B8B2DDA9848B7897469171B29020C05" },
- { name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" },
+ { name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" },
{ name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" },
{ name = "glam", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "4932A2D139AB0389E149396407F89654928D7B815E212BB02F13C66F53B1BBA1" },
{ name = "glance", version = "3.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "F3458292AFB4136CEE23142A8727C1270494E7A96978B9B9F9D2C1618583EF3D" },
{ name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" },
- { name = "gleam_community_colour", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "386CB9B01B33371538672EEA8A6375A0A0ADEF41F17C86DDCB81C92AD00DA610" },
- { name = "gleam_erlang", version = "0.34.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "0C38F2A128BAA0CEF17C3000BD2097EB80634E239CE31A86400C4416A5D0FDCC" },
- { name = "gleam_fetch", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "2CBF9F2E1C71AEBBFB13A9D5720CD8DB4263EB02FE60C5A7A1C6E17B0151C20C" },
- { name = "gleam_http", version = "3.7.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8A70D2F70BB7CFEB5DF048A2183FFBA91AF6D4CF5798504841744A16999E33D2" },
- { name = "gleam_httpc", version = "4.1.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C670EBD46FC1472AD5F1F74F1D3938D1D0AC1C7531895ED1D4DDCB6F07279F43" },
+ { name = "gleam_community_colour", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "E34DD2C896AC3792151EDA939DA435FF3B69922F33415ED3C4406C932FBE9634" },
{ name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" },
- { name = "gleam_json", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C55C5C2B318533A8072D221C5E06E5A75711C129E420DD1CE463342106012E5D" },
- { name = "gleam_otp", version = "0.16.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "50DA1539FC8E8FA09924EB36A67A2BBB0AD6B27BCDED5A7EF627057CF69D035E" },
+ { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" },
{ name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" },
{ name = "gleam_stdlib", version = "0.59.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F8FEE9B35797301994B81AF75508CF87C328FE1585558B0FFD188DC2B32EAA95" },
- { name = "gleam_time", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "0DF3834D20193F0A38D0EB21F0A78D48F2EC276C285969131B86DF8D4EF9E762" },
{ name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" },
- { name = "gleamy_lights", version = "2.3.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_community_colour", "gleam_stdlib"], otp_app = "gleamy_lights", source = "hex", outer_checksum = "8A3D43BCA0D935F7CC787F4D0D1771F822B3366114C08B93CC8D00747618499A" },
+ { name = "gleamy_lights", version = "2.3.1", build_tools = ["gleam"], requirements = ["envoy", "gleam_community_colour", "gleam_stdlib"], otp_app = "gleamy_lights", source = "hex", outer_checksum = "CD89DD48BBCD8FBB6B8CB84101C70221CBFB901F711C3C7F81F47288EC8074FD" },
{ name = "gleeunit", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "A7DD6C07B7DA49A6E28796058AA89E651D233B357D5607006D70619CD89DAAAB" },
- { name = "glexer", version = "2.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "5C235CBDF4DA5203AD5EAB1D6D8B456ED8162C5424FE2309CFFB7EF438B7C269" },
- { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" },
- { name = "javascript_mutable_reference", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "javascript_mutable_reference", source = "hex", outer_checksum = "3EE953EE7FE4FAFD17C16F24184F4C832FE260D761753F28F20D4AC1DA080F03" },
- { name = "jot", version = "5.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "houdini", "splitter"], otp_app = "jot", source = "hex", outer_checksum = "B1A0C91A3D273971D1CA1F644FF0A9CAC8256BDA249CADC927041BF14E7114A6" },
+ { name = "glexer", version = "2.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "splitter"], otp_app = "glexer", source = "hex", outer_checksum = "41D8D2E855AEA87ADC94B7AF26A5FEA3C90268D4CF2CCBBD64FD6863714EE085" },
{ name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" },
- { name = "lustre", version = "5.0.3", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "0BB69D69A9E75E675AA2C32A4A0E5086041D037829FC8AD385BA6A59E45A60A2" },
- { name = "modem", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib", "lustre"], otp_app = "modem", source = "hex", outer_checksum = "EF6B6B187E9D6425DFADA3A1AC212C01C4F34913A135DA2FF9B963EEF324C1F7" },
- { name = "odysseus", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "odysseus", source = "hex", outer_checksum = "6A97DA1075BDDEA8B60F47B1DFFAD49309FA27E73843F13A0AF32EA7087BA11C" },
- { name = "plinth", version = "0.7.1", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "63BB36AACCCCB82FBE46A862CF85CB88EBE4EF280ECDBAC4B6CB042340B9E1D8" },
+ { name = "plinth", version = "0.9.2", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "A5A14C590F9820F8447E989B66C73C9DE077FAAB75618D639DFA1F3BCA45F946" },
{ name = "pprint", version = "1.0.6", build_tools = ["gleam"], requirements = ["glam", "gleam_stdlib"], otp_app = "pprint", source = "hex", outer_checksum = "4E9B34AE03B2E81D60F230B9BAF1792BE1AC37AFB5564B8DEBEE56BAEC866B7D" },
{ name = "ranger", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_yielder"], otp_app = "ranger", source = "hex", outer_checksum = "C8988E8F8CDBD3E7F4D8F2E663EF76490390899C2B2885A6432E942495B3E854" },
{ name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" },
- { name = "rsvp", version = "1.0.4", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_fetch", "gleam_http", "gleam_httpc", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "rsvp", source = "hex", outer_checksum = "EFCA7CD53B0A8738C06E136422D1FF080DBB657C89E077F7B9DD20BFACE0A77A" },
{ name = "simplifile", version = "2.2.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "C88E0EE2D509F6D86EB55161D631657675AA7684DAB83822F7E59EB93D9A60E3" },
{ name = "splitter", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "3DFD6B6C49E61EDAF6F7B27A42054A17CFF6CA2135FF553D0CB61C234D281DD0" },
{ name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" },
{ name = "tom", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0910EE688A713994515ACAF1F486A4F05752E585B9E3209D8F35A85B234C2719" },
- { name = "trie_again", version = "1.1.3", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "365FE609649F3A098D1D7FC7EA5222EE422F0B3745587BF2AB03352357CA70BB" },
- { name = "webls", version = "1.5.1", build_tools = ["gleam"], requirements = ["birl", "gleam_stdlib"], otp_app = "webls", source = "hex", outer_checksum = "6C78FFCD3BB888725F83FBD0729BDEA1BFEDFBB06544FCA15BF98FBA04F863B0" },
+ { name = "trie_again", version = "1.1.4", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "E3BD66B4E126EF567EA8C4944EAB216413392ADF6C16C36047AF79EE5EF13466" },
]
[requirements]
@@ -54,29 +39,12 @@ argv = { version = "1.0.2" }
birdie = { version = ">= 1.2.7 and < 2.0.0" }
birl = { version = "1.8.0" }
bungibindies = { version = "1.2.0-rc" }
-conversation = { version = "2.0.1" }
cynthia_websites_mini_client = { path = "../cynthia_websites_mini_client" }
-edit_distance = { version = "2.0.1" }
-envoy = { version = "1.0.2" }
-filepath = { version = "1.1.2" }
-glam = { version = "2.0.2" }
-glance = { version = "3.0.0" }
-gleam_community_ansi = { version = "1.4.3" }
-gleam_community_colour = { version = "1.4.1" }
-gleam_fetch = { version = "1.3.0" }
-gleam_http = { version = "3.7.2" }
-gleam_javascript = { version = "1.0.0" }
-gleam_json = { version = "2.3.0" }
-gleam_regexp = { version = "1.1.1" }
-gleam_stdlib = { version = "0.59.0" }
-gleam_yielder = { version = "1.1.0" }
-gleamy_lights = { version = "2.3.0" }
+gleam_javascript = { version = ">= 1.0.0 and < 2.0.0" }
+gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
+gleamy_lights = { version = ">= 2.3.1 and < 3.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
-javascript_mutable_reference = { version = "1.0.0" }
-justin = { version = "1.0.1" }
-plinth = { version = ">= 0.5.9 and < 1.0.0" }
+plinth = { version = ">= 0.9.2 and < 1.0.0" }
pprint = { version = ">= 1.0.5 and < 2.0.0" }
-ranger = { version = "1.4.0" }
simplifile = { version = "2.2.1" }
tom = { version = "1.1.1" }
-webls = { version = "1.5.1" }
diff --git a/cynthia_websites_mini_server/src/cynthia_websites_mini_server.gleam b/cynthia_websites_mini_server/src/cynthia_websites_mini_server.gleam
index 34b1484..f8cffa0 100644
--- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server.gleam
+++ b/cynthia_websites_mini_server/src/cynthia_websites_mini_server.gleam
@@ -1,27 +1,15 @@
import bungibindies
import bungibindies/bun
-import bungibindies/bun/http/serve.{ServeOptions}
import cynthia_websites_mini_client
-import cynthia_websites_mini_client/configtype
-import cynthia_websites_mini_client/shared/jsonld
-import cynthia_websites_mini_client/shared/sitemap
-import cynthia_websites_mini_server/config
-import cynthia_websites_mini_server/mutable_model_type
-import cynthia_websites_mini_server/ssrs
-import cynthia_websites_mini_server/utils/files
-import cynthia_websites_mini_server/web
+import cynthia_websites_mini_shared/config/site_json
+import cynthia_websites_mini_shared/ffi
import gleam/bool
-import gleam/int
import gleam/javascript/array
-import gleam/javascript/promise
-import gleam/json
import gleam/list
-import gleam/option.{None, Some}
+import gleam/option.{type Option, None, Some}
import gleam/string
-import gleamy_lights/console
import gleamy_lights/premixed
-import javascript/mutable_reference
-import plinth/javascript/global
+import plinth/javascript/console
import plinth/node/process
import simplifile
@@ -55,17 +43,13 @@ pub fn main() {
<> premixed.text_bright_orange(process.cwd())
<> "!",
)
- use m <- promise.await(mutable_model_type.new())
- case process.argv() |> array.to_list() |> list.drop(2) {
- ["dynamic", ..] | ["host", ..] ->
- dynamic_site_server(m, 60_000) |> promise.resolve
- ["preview", ..] -> dynamic_site_server(m, 20) |> promise.resolve
- ["pregenerate", ..] | ["static"] -> static_site_server(m)
+ let args = process.argv() |> array.to_list() |> list.drop(2)
+ case args {
+ ["pregenerate", ..] | ["static"] -> start()
["init", ..] | ["initialise", ..] -> {
- config.initcfg()
- |> promise.resolve
+ init(args |> list.includes("--force"))
}
- ["man", ..] | ["help", ..] | ["--help", ..] | ["-h", ..] | [] -> {
+ _ -> {
case process.argv() |> array.to_list() |> list.drop(2) {
[] -> console.error("No subcommand given.\n")
_ -> Nil
@@ -85,25 +69,13 @@ pub fn main() {
premixed.text_pink("initialise\n"),
])
<> "\t\t\t\tInitialise the config file then exit\n\n"
- // Dynamic:
+ // Run:
<> string.concat([
- premixed.text_pink("\tdynamic"),
- " | ",
- premixed.text_pink("host\n"),
- ])
- <> "\t\t\t\tStart a dynamic website server\n\n"
- // Pregenerate:
- <> string.concat([
- premixed.text_pink("\tstatic"),
+ premixed.text_pink("\trun"),
" | ",
premixed.text_pink("pregenerate\n"),
])
<> "\t\t\t\tGenerate a static website\n\n"
- // Preview:
- <> premixed.text_pink("\tpreview\n")
- <> "\t\t\t\tStart a dynamic website server for previewing\n"
- <> "\t\t\t\tthis is the same as dynamic, but with a shorter\n"
- <> "\t\t\t\tinterval for the cache\n\n"
// Help:
<> premixed.text_lilac("\thelp")
<> "\n"
@@ -114,7 +86,6 @@ pub fn main() {
)
<> ".\n",
)
- |> promise.resolve
}
[a, ..] ->
console.error(
@@ -129,270 +100,136 @@ pub fn main() {
<> premixed.text_purple("help")
<> "´ to see a list of all subcommands.\n",
)
- |> promise.resolve
}
}
-fn dynamic_site_server(mutmodel: mutable_model_type.MutableModel, lease: Int) {
- console.info("Cynthia Mini is in dynamic site mode!")
- let model = mutmodel |> mutable_reference.get
- let conf = model.config
- {
- let folder = process.cwd() <> "/assets/cynthia-mini"
- case simplifile.create_directory_all(folder) {
- Ok(..) -> Nil
- Error(e) -> {
- console.error(
- "A problem occurred while creating the ´"
- <> folder
- <> "´ directory: "
- <> premixed.text_error_red(string.inspect(e)),
- )
- process.exit(1)
- panic as "We should not reach here"
- }
- }
+fn get_context() -> site_json.SiteJSON {
+ let config = case
+ {
+ let global_conf_filepath =
+ files.path_join([process.cwd(), "/cynthia.toml"])
+ let global_conf_filepath_exists = files.file_exist(global_conf_filepath)
- case files.file_exist(process.cwd() <> "/assets/cynthia-mini/README.md") {
- True -> Nil
- False -> {
- case
- simplifile.write(
- process.cwd() <> "/assets/cynthia-mini/README.md",
- "# What does this folder do?\n\r\n\rThis folder holds a few files Cynthia Mini serves to the browser to make sure everything works alright.\n\r\n\rThese are usually checked and downloaded if necessary only during start of the server,\n\rso try not to touch them! If you believe one of the files in here might be faulty, delete it, and restart the server.\n\r\n\rHave a nice day! :)",
- )
- {
- Ok(..) -> Nil
- Error(e) -> {
- console.error(
- "A problem occurred while creating the ´"
- <> process.cwd()
- <> "/assets/cynthia-mini/README.md"
- <> "´ file: "
- <> premixed.text_error_red(string.inspect(e)),
- )
- process.exit(1)
- panic as "We should not reach here"
+ case global_conf_filepath_exists {
+ True -> {
+ Nil
+ }
+ // No config was found. Let's look for legacy config or initialise.
+ False -> {
+ let global_conf_filepath_legacy =
+ files.path_join([process.cwd(), "/cynthia-mini.toml"])
+ let global_conf_filepath_legacy_exists =
+ files.file_exist(global_conf_filepath_legacy)
+ case
+ global_conf_filepath_legacy_exists,
+ simplifile.read(global_conf_filepath_legacy)
+ {
+ True, Ok(legacy_config) -> {
+ console.warn(
+ "A legacy config file was found! Cynthia Mini will attempt to auto-convert it on the go and continue.",
+ )
+ let upgraded_config =
+ "# This file was upgraded to the universal Cynthia Config format\n# Do not edit these two variables! They are set by Cynthia to tell it's config format apart.\nconfig.edition=\"mini\"\nconfig.version=4.0\n\n"
+ <> legacy_config
+ case
+ simplifile.write(
+ to: global_conf_filepath,
+ contents: upgraded_config,
+ )
+ {
+ Ok(_) -> {
+ let _ =
+ simplifile.rename(
+ at: global_conf_filepath_legacy,
+ to: global_conf_filepath_legacy <> ".old",
+ )
+ Nil
+ }
+ Error(_) -> {
+ console.error(
+ "Error: Could not write upgraded config to "
+ <> global_conf_filepath
+ <> ". Please check file permissions.",
+ )
+ process.exit(1)
+ }
+ }
+ }
+ True, Error(_) -> {
+ console.error(
+ "Some error happened while trying to read "
+ <> global_conf_filepath_legacy
+ <> ".",
+ )
+ process.exit(1)
+ }
+ // No config found, and no old config found.
+ False, _ -> {
+ init()
+ get_context()
+ }
}
}
- Nil
}
+ let e = "Could not read " <> global_conf_filepath
+ let assert Ok(toml) = simplifile.read(global_conf_filepath) as e
+ // Call the latest decoder for it and return. If it encounters an older config format it should be able to recognise and convert by itself.
+ decodes.vp4p1mini_toml(toml)
}
- }
- console.log("Starting server...")
- case
- bun.serve(ServeOptions(
- development: Some(True),
- hostname: conf.server_host,
- port: conf.server_port,
- static_served: ssrs.ssrs(mutmodel),
- handler: web.handle_request(_, mutmodel),
- id: None,
- reuse_port: None,
- ))
{
- Ok(..) -> {
- console.log(
- "Server started! Running on: "
- <> premixed.text_cyan(
- "http://"
- <> option.unwrap(conf.server_host, "localhost")
- <> ":"
- <> int.to_string(option.unwrap(conf.server_port, 8080))
- <> "/",
- ),
- )
- global.set_interval(lease, fn() {
- mutable_reference.update(mutmodel, fn(model) {
- case model.cached_response {
- None -> model
- Some(..) ->
- // Drops the cached response to keep it updated
- mutable_model_type.MutableModelContent(
- ..model,
- cached_response: None,
- )
- }
- })
- })
- }
- Error(e) -> {
- console.error(
- "A problem occurred while starting the server: "
- <> premixed.text_error_red(string.inspect(e)),
- )
+ Ok(conf) -> conf
+ Error(_) -> {
process.exit(1)
- panic as "We should not reach here"
+ panic as "Shouldn't be here."
}
}
+ let content = {
+ todo
+ }
- Nil
+ site_json.SiteJSON(config, content)
}
-fn static_site_server(mutmodel: mutable_model_type.MutableModel) {
- console.info("Cynthia Mini is in pregeneration mode!")
-
- {
- let folder = process.cwd() <> "/assets/cynthia-mini"
- case simplifile.create_directory_all(folder) {
- Ok(..) -> Nil
- Error(e) -> {
- console.error(
- "A problem occurred while creating the ´"
- <> folder
- <> "´ directory: "
- <> premixed.text_error_red(string.inspect(e)),
- )
- process.exit(1)
- panic as "We should not reach here"
- }
- }
-
- case files.file_exist(process.cwd() <> "/assets/cynthia-mini/README.md") {
- True -> Nil
- False -> {
- case
- simplifile.write(
- process.cwd() <> "/assets/cynthia-mini/README.md",
- "# What does this folder do?\n\r\n\rThis folder holds a few files Cynthia Mini serves to the browser to make sure everything works alright.\n\r\n\rThese are usually checked and downloaded if necessary only during start of the server,\n\rso try not to touch them! If you believe one of the files in here might be faulty, delete it, and restart the server.\n\r\n\rHave a nice day! :)",
- )
- {
- Ok(..) -> Nil
- Error(e) -> {
- console.error(
- "A problem occurred while creating the ´"
- <> process.cwd()
- <> "/assets/cynthia-mini/README.md"
- <> "´ file: "
- <> premixed.text_error_red(string.inspect(e)),
- )
- process.exit(1)
- panic as "We should not reach here"
- }
- }
- Nil
- }
- }
- }
-
- use complete_data <- promise.await(config.load())
+import cynthia_websites_mini_shared/config/v4_1/decodes
- // Generate JSON representations
- let complete_data_json =
- complete_data |> configtype.encode_complete_data_for_client
- let res_string = complete_data_json |> json.to_string
- let res_jsonld = jsonld.generate_jsonld(complete_data)
- let opt_sitemap = sitemap.generate_sitemap(complete_data)
+pub fn create_html(json: site_json.SiteJSON, path: String) {
+ "
+
+
- let outdir = process.cwd() <> "/out"
- case simplifile.create_directory_all(outdir) {
- Ok(..) -> Nil
- Error(e) -> {
- console.error(
- "A problem occurred while creating the ´out´ directory: "
- <> premixed.text_error_red(string.inspect(e)),
- )
- process.exit(1)
- panic as "We should not reach here"
- }
- }
- case simplifile.write(to: outdir <> "/site.json", contents: res_string) {
- Ok(..) -> Nil
- Error(e) -> {
- console.error(
- "A problem occurred while creating the ´site.json´ file: "
- <> premixed.text_error_red(string.inspect(e)),
- )
- process.exit(1)
- panic as "We should not reach here"
- }
- }
- case
- simplifile.write(
- to: outdir <> "/index.html",
- contents: ssrs.index_html(mutable_reference.get(mutmodel)),
- )
- {
- Ok(..) -> Nil
- Error(e) -> {
- console.error(
- "A problem occurred while creating the ´index.html´ file: "
- <> premixed.text_error_red(string.inspect(e)),
- )
- process.exit(1)
- panic as "We should not reach here"
- }
- }
- case
- simplifile.copy_directory(
- at: process.cwd() <> "/assets/",
- to: outdir <> "/assets/",
- )
- {
- Ok(..) -> Nil
- Error(e) -> {
- console.error(
- "A problem occurred while copying the assets directory: "
- <> premixed.text_error_red(string.inspect(e)),
- )
- process.exit(1)
- panic as "We should not reach here"
- }
- }
- case simplifile.is_file(outdir <> "/site.json") {
- Ok(True) -> Nil
- _ -> {
- console.error(
- "An unknown problem occurred while creating the ´site.json´ file.",
- )
- process.exit(1)
- panic as "We should not reach here"
- }
- }
- case simplifile.is_file(outdir <> "/index.html") {
- Ok(True) -> Nil
- _ -> {
- console.error(
- "An unknown problem occurred while creating the ´index.html´ file.",
- )
- process.exit(1)
- panic as "We should not reach here"
- }
- }
- case opt_sitemap {
- None -> Nil
- Some(res_sitemap) -> {
- case
- simplifile.write(to: outdir <> "/sitemap.xml", contents: res_sitemap)
- {
- Ok(..) -> Nil
- Error(e) -> {
- console.error(
- "A problem occurred while creating the ´sitemap.xml´ file: "
- <> premixed.text_error_red(string.inspect(e)),
- )
- process.exit(1)
- panic as "We should not reach here"
- }
- }
- }
- }
+
+<<site hosted by Cynthia mini>>
+ "/>
+ "/>
+" <> case model.cached_jsonld {
+ Some(jsonld) ->
+ ""
+ None -> " "
+ } <> "
+
+
+
+
+
+
+
+
+ " <> first_view <> "
+
+ " <> footer(True, json.config.integrations.git) <> "
+
+
+"
+}
- console.info(
- premixed.text_ok_green("Site pregeneration complete!")
- <> " Serve files from "
- <> premixed.text_orange(outdir <> "/")
- <> " and you should have a site running!",
- )
- promise.resolve(Nil)
+fn init(forced: Bool) {
+ todo
}
diff --git a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config.gleam b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config.gleam
deleted file mode 100644
index e2afaa7..0000000
--- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config.gleam
+++ /dev/null
@@ -1,711 +0,0 @@
-import bungibindies/bun
-import bungibindies/bun/spawn
-import cynthia_websites_mini_client/configtype
-import cynthia_websites_mini_client/contenttypes
-import cynthia_websites_mini_server/config/v4
-import cynthia_websites_mini_server/utils/files
-import cynthia_websites_mini_server/utils/prompts
-import gleam/bool
-import gleam/dynamic/decode
-import gleam/fetch
-import gleam/float
-import gleam/http/request
-import gleam/int
-import gleam/javascript/promise.{type Promise}
-import gleam/json
-import gleam/list
-import gleam/option.{None, Some}
-import gleam/result
-import gleam/string
-import gleamy_lights/premixed
-import plinth/javascript/console
-import plinth/node/fs
-import plinth/node/process
-import simplifile
-import tom
-
-/// # Config.load()
-/// Loads the configuration from the `cynthia.toml` file and the content from the `content` directory.
-/// Then saves the configuration to the database.
-pub fn load() -> Promise(configtype.CompleteData) {
- use global_config <- promise.await(capture_config())
- use content_list <- promise.await(content_getter())
- let content = case content_list {
- Ok(lis) -> lis
- Error(msg) -> {
- console.error("Error: There was an error getting content:\n" <> msg)
- process.exit(1)
- panic as "We should not reach here"
- }
- }
-
- let complete_data = configtype.merge(global_config, content)
- complete_data
- |> promise.resolve
-}
-
-pub fn capture_config() {
- let global_conf_filepath = files.path_join([process.cwd(), "/cynthia.toml"])
- let global_conf_filepath_exists = files.file_exist(global_conf_filepath)
-
- case global_conf_filepath_exists {
- True -> Nil
- False -> {
- let global_conf_filepath_legacy =
- files.path_join([process.cwd(), "/cynthia-mini.toml"])
- let global_conf_filepath_legacy_exists =
- files.file_exist(global_conf_filepath_legacy)
- case
- global_conf_filepath_legacy_exists,
- simplifile.read(global_conf_filepath_legacy)
- {
- True, Ok(legacy_config) -> {
- console.warn(
- "A legacy config file was found! Cynthia Mini will attempt to auto-convert it on the go and continue.",
- )
- let upgraded_config =
- "# This file was upgraded to the universal Cynthia Config format\n# Do not edit these two variables! They are set by Cynthia to tell it's config format apart.\nconfig.edition=\"mini\"\nconfig.version=4\n\n"
- <> legacy_config
- case
- simplifile.write(
- to: global_conf_filepath,
- contents: upgraded_config,
- )
- {
- Ok(_) -> {
- let _ =
- simplifile.rename(
- at: global_conf_filepath_legacy,
- to: global_conf_filepath_legacy <> ".old",
- )
- Nil
- }
- Error(_) -> {
- console.error(
- "Error: Could not write upgraded config to "
- <> global_conf_filepath
- <> ". Please check file permissions.",
- )
- process.exit(1)
- }
- }
- }
- True, Error(_) -> {
- console.error(
- "Some error happened while trying to read "
- <> global_conf_filepath_legacy
- <> ".",
- )
- process.exit(1)
- }
- False, _ -> {
- dialog_initcfg()
- process.exit(0)
- }
- }
- }
- }
- let global_conf_content_sync =
- simplifile.read(global_conf_filepath) |> result.unwrap("")
- let m = case parse_config_format(global_conf_content_sync) {
- // Correct config format: mini-4
- Ok(#("mini", 4)) -> v4.parse_mini()
-
- // Erronous config format outcomes
- Ok(#(c, d)) -> promise_error_unknown_config_format(c, d)
- Error(_) -> promise_error_cannot_read_config_format()
- }
- use parse_configtoml_result <- promise.await(m)
-
- let global_config = case parse_configtoml_result {
- Ok(config) -> config
- Error(why) -> {
- premixed.text_error_red("Error: Could not load cynthia.toml: " <> why)
- |> console.error
- process.exit(1)
- panic as "We should not reach here"
- }
- }
- global_config
- |> promise.resolve()
-}
-
-fn promise_error_unknown_config_format(
- edition: String,
- version: Int,
-) -> Promise(Result(a, String)) {
- let err =
- "Config version "
- <> version |> int.to_string()
- <> " with edition '"
- <> edition
- <> "' is NOT supported by this version of Cynthia."
- <> "\n Usually this means one of these options:"
- <> "\n - it was written for a different edition"
- <> "\n - it is invalid"
- <> "\n - or this version of cynthia is too old to understand this file."
- <> case edition == "mini" {
- True ->
- "\n\n\n It seems to be that last option, since the edition it is written for, does match 'mini'."
- False -> ""
- }
- promise.resolve(Error(err))
-}
-
-fn promise_error_cannot_read_config_format() {
- promise.resolve(Error(
- "Cannot properly read config.edition and/or config.version, Cynthia doesn't know how to parse this file anymore!",
- ))
-}
-
-fn parse_config_format(toml_str: String) -> Result(#(String, Int), Nil) {
- use d <- result.try(tom.parse(toml_str) |> result.replace_error(Nil))
- use edition <- result.try(
- tom.get_string(d, ["config", "edition"]) |> result.replace_error(Nil),
- )
- use version <- result.try(
- tom.get_int(d, ["config", "version"]) |> result.replace_error(Nil),
- )
- Ok(#(edition, version))
-}
-
-fn content_getter() -> promise.Promise(
- Result(List(contenttypes.Content), String),
-) {
- let promises: List(Promise(Result(contenttypes.Content, String))) = {
- fn(file) {
- file
- |> string.replace(".meta.json", "")
- |> files.path_normalize()
- }
- |> fn(value) {
- list.map(
- list.filter(
- result.unwrap(
- simplifile.get_files(files.path_join([process.cwd() <> "/content"])),
- [],
- ),
- fn(file) { file |> string.ends_with(".meta.json") },
- ),
- value,
- )
- }
- |> list.map(get_inner_and_meta)
- }
- promise.map(promise.await_list(promises), result.all)
-}
-
-fn get_inner_and_meta(
- file: String,
-) -> Promise(Result(contenttypes.Content, String)) {
- use meta_json <- promise.try_await(
- simplifile.read(file <> ".meta.json")
- |> result.replace_error(
- "FS error while reading ´" <> file <> ".meta.json´.",
- )
- |> promise.resolve,
- )
- // Sometimes stuff is saved somewhere else, like in a different file path or maybe somewhere on the web, of course Cynthia Mini can still find those files!
- // ...However, we first need to know there is an "external" file somewhere, we do that by checking the 'path' field.
- // The extension before .meta.json is still used to parse the content.
- let possibly_extern =
- json.parse(meta_json, {
- use path <- decode.optional_field("path", "", decode.string)
- decode.success(path)
- })
- |> result.unwrap("")
- |> string.to_option
- use permalink <- promise.try_await(
- json.parse(meta_json, {
- use path <- decode.optional_field("permalink", "", decode.string)
- decode.success(path)
- })
- |> result.replace_error("Could not decode permalink for ´" <> file <> "´")
- |> promise.resolve,
- )
-
- use inner_plain <- promise.try_await({
- // This case also check if the permalink starts with "!", in which case it is a content list.
- // Content lists will be generated on the client side, and their pre-given content
- // will be discarded, so loading it in from anywhere would be a waste of resources.
- case string.starts_with(permalink, "!"), possibly_extern {
- True, _ -> promise.resolve(Ok(""))
- False, None -> {
- promise.resolve(
- simplifile.read(file)
- |> result.replace_error("FS error while reading ´" <> file <> "´."),
- )
- }
- False, Some(p) -> get_ext(p)
- }
- })
-
- // Now, conversion to Djot for markdown files done in-place:
- let converted: Result(#(String, String), String) = case
- string.ends_with(file, "markdown")
- |> bool.or(
- string.ends_with(file, "md") |> bool.or(string.ends_with(file, "mdown")),
- )
- {
- True -> {
- // If the file is external, we need to write it to a temporary file first.
- let wri = case possibly_extern {
- Some(..) -> {
- simplifile.write(file, inner_plain)
- |> result.replace_error(
- "There was an error while writing the external content to '"
- <> file |> premixed.text_bright_yellow()
- <> "'.",
- )
- }
- None -> Ok(Nil)
- }
- use _ <- result.try(wri)
-
- use pandoc_path <- result.try(result.replace_error(
- bun.which("pandoc"),
- "There is a markdown file in Cynthia's content folder, but to convert that to Djot and display it, you need to have Pandoc installed on the PATH, which it is not!",
- ))
- let pandoc_child =
- spawn.sync(spawn.OptionsToSubprocess(
- [pandoc_path, file, "-f", "gfm", "-t", "djot"],
- cwd: Some(process.cwd()),
- env: None,
- stderr: Some(spawn.Pipe),
- stdout: Some(spawn.Pipe),
- ))
- let pandoc_child = case
- {
- let assert spawn.SyncSubprocess(asserted_sync_child) = pandoc_child
- spawn.success(asserted_sync_child)
- }
- {
- True -> Ok(pandoc_child)
- False -> {
- Error(
- "There was an error while trying to convert '"
- <> file |> premixed.text_bright_yellow()
- <> "' to Djot: \n"
- <> result.unwrap(spawn.stderr(pandoc_child), "")
- <> "\n\nMake sure you have at least Pandoc 3.7.0 installed on your system, earlier versions may not work correctly.",
- )
- }
- }
- use pandoc_child <- result.try(pandoc_child)
- let new_inner_plain: Result(String, String) =
- spawn.stdout(pandoc_child)
- |> result.replace_error("")
- use new_inner_plain <- result.try(new_inner_plain)
-
- // If the file was external, we need delete the temporary file.
- let re = case possibly_extern {
- Some(..) -> {
- simplifile.delete(file)
- |> result.replace_error(
- "There was an error while deleting the temporary file '"
- <> file |> premixed.text_bright_yellow()
- <> "'.",
- )
- }
- None -> Ok(Nil)
- }
-
- use _ <- result.try(re)
-
- Ok(#(new_inner_plain, file <> ".dj"))
- }
-
- False -> {
- Ok(#(inner_plain, file))
- }
- }
-
- let metadata = case converted {
- Ok(#(inner_plain, file)) -> {
- let decoder = contenttypes.content_decoder_and_merger(inner_plain, file)
- json.parse(meta_json, decoder)
- |> result.map_error(fn(e) {
- "Some error decoding metadata for ´"
- <> file |> premixed.text_magenta()
- <> "´: "
- <> string.inspect(e)
- })
- }
- Error(l) -> Error(l)
- }
-
- promise.resolve(metadata)
-}
-
-/// Gets external content, beit by file path or by http(s) url.
-fn get_ext(path: String) -> promise.Promise(Result(String, String)) {
- case string.starts_with(string.lowercase(path), "http") {
- True -> {
- let start = bun.nanoseconds()
- console.log(
- "Downloading external content ´" <> premixed.text_blue(path) <> "´...",
- )
-
- let assert Ok(req) = request.to(path)
- use resp <- promise.try_await(
- promise.map(fetch.send(req), fn(e) {
- result.replace_error(
- e,
- "Error while downloading external content ´"
- <> path
- <> "´: "
- <> string.inspect(e),
- )
- }),
- )
- use resp <- promise.try_await(
- promise.map(fetch.read_text_body(resp), fn(e) {
- result.replace_error(
- e,
- "Error while reading external content ´"
- <> path
- <> "´: "
- <> string.inspect(e),
- )
- }),
- )
- let end = bun.nanoseconds()
- let duration_ms = { end -. start } /. 1_000_000.0
- case resp.status {
- 200 -> {
- console.log(
- "Downloaded external content ´"
- <> premixed.text_blue(path)
- <> "´ in "
- <> int.to_string(duration_ms |> float.truncate)
- <> "ms!",
- )
- Ok(resp.body)
- }
- _ -> {
- Error(
- "Error while downloading external content ´"
- <> path
- <> "´: "
- <> string.inspect(resp.status),
- )
- }
- }
- |> promise.resolve
- }
- False -> {
- // Is a file path
- promise.resolve(
- simplifile.read(path)
- |> result.replace_error(
- "FS error while reading external content file ´" <> path <> "´.",
- ),
- )
- }
- }
-}
-
-fn dialog_initcfg() {
- console.log("No Cynthia Mini configuration found...")
- case
- prompts.for_confirmation(
- "CynthiaMini can create \n"
- <> premixed.text_orange(process.cwd() <> "/cynthia.toml")
- <> "\n ...and some sample content.\n"
- <> premixed.text_magenta(
- "Do you want to initialise new config at this location?",
- ),
- True,
- )
- {
- False -> {
- console.error("No Cynthia Mini configuration found... Exiting.")
- process.exit(1)
- panic as "We should not reach here"
- }
- True -> initcfg()
- }
-}
-
-const brand_new_config = "# Do not edit these variables! It is set by Cynthia to tell it's config format apart.
-config.edition=\"mini\"
-config.version=4
-[global]
-# Theme to use for light mode - default themes: autumn, default
-theme = \"autumn\"
-# Theme to use for dark mode - default themes: night, default-dark
-theme_dark = \"night\"
-# For some browsers, this will change the colour of UI elements such as the address bar
-# and the status bar on mobile devices.
-# This is a hex colour, e.g. #FFFFFF
-colour = \"#FFFFFF\"
-# Your website's name, displayed in various places
-site_name = \"My Site\"
-# A brief description of your website
-site_description = \"A big site on a mini Cynthia!\"
-
-[server]
-# Port number for the web server
-port = 8080
-# Host address for the web server
-host = \"localhost\"
-
-[integrations]
-# Enable git integration for the website
-# This will allow Cynthia Mini to detect the git repository
-# For example linking to the commit hash in the footer
-git = true
-
-# Enable sitemap generation
-# This will generate a sitemap.xml file in the root of the website
-#
-# You will need to enter the base URL of your website in the sitemap variable below.
-# If your homepage is at \"https://example.com/#/\", then the sitemap variable should be set to \"https://example.com\".
-# If you do not want to use a sitemap, set this to \"false\", or leave it empty (\"\"), you can also remove the sitemap variable altogether.
-sitemap = \"\"
-
-# Enable crawlable context (JSON-LD injection)
-# This will allow search engines to crawl the website, and makes it
-# possible for the website to be indexed by search engine and LLMs.
-crawlable_context = false
-
-[variables]
-# You can define your own variables here, which can be used in templates.
-
-## ownit_template
-##
-## Use this to define your own template for the 'ownit' layout.
-##
-## The template will be used for the 'ownit' layout, which is used for pages and posts.
-## You can use the following variables in the template:
-## - body: string (The main HTML content)
-## - is_post: boolean (True if the current item is a post, false if it's a page)
-## - title: string (The title of the page or post)
-## - description: string (The description of the page or post)
-## - site_name: string (The global site name)
-## - category: string (The category of the post, empty for pages)
-## - date_modified: string (The last modification date of the post, empty for pages)
-## - date_published: string (The publication date of the post, empty for pages)
-## - tags: string[] (An array of tags for the post, empty for pages)
-## - menu_1_items: [string, string][] (Array of menu items for menu 1, e.g., [[\"Home\", \"/\"], [\"About\", \"/about\"]])
-## - menu_2_items: [string, string][] (Array of menu items for menu 2)
-## - menu_3_items: [string, string][] (Array of menu items for menu 3)
-ownit_template = \"\"\"
-