From dff5031f9b9a3cf24976f338912b68664233d2bf Mon Sep 17 00:00:00 2001 From: MLC Bloeiman Date: Fri, 28 Nov 2025 14:05:54 +0100 Subject: [PATCH 1/9] feat: Config versions implemented --- README.md | 2 +- cynthia_websites_mini_server/manifest.toml | 12 +- .../cynthia_websites_mini_server/config.gleam | 720 +++++------------- .../config/v4.gleam | 457 +++++++++++ 4 files changed, 655 insertions(+), 536 deletions(-) create mode 100644 cynthia_websites_mini_server/src/cynthia_websites_mini_server/config/v4.gleam diff --git a/README.md b/README.md index ec8a8b4..8e9ff4b 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ For detailed information about configuration, theming, and deployment, check out article1.dj.meta.json article2.dj article2.dj.meta.json -./cynthia-mini.toml +./cynthia.toml ``` ## Contributing diff --git a/cynthia_websites_mini_server/manifest.toml b/cynthia_websites_mini_server/manifest.toml index 837db4e..af2f9f2 100644 --- a/cynthia_websites_mini_server/manifest.toml +++ b/cynthia_websites_mini_server/manifest.toml @@ -7,7 +7,7 @@ packages = [ { 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-rc", 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.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 = "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 = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, @@ -18,20 +18,20 @@ packages = [ { 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.2.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "224BF35B2091502921D1623F35E6FA52815B75D99D18AEFB9DAEA0B8AEADD7A1" }, + { 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_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 = "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 = "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 = "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.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "houdini", "splitter"], otp_app = "jot", source = "hex", outer_checksum = "2C1B30CC00B0D79F904028F48229C0BB354F3C1BC05EE99D4F3D423E223D85BF" }, + { name = "jot", version = "5.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "houdini", "splitter"], otp_app = "jot", source = "hex", outer_checksum = "B1A0C91A3D273971D1CA1F644FF0A9CAC8256BDA249CADC927041BF14E7114A6" }, { 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" }, @@ -42,7 +42,7 @@ packages = [ { 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.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "128FC521EE33B0012E3E64D5B55168586BC1B9C8D7B0D0CA223B68B0D770A547" }, + { 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" }, 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 index 74aa08e..a9b0020 100644 --- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config.gleam +++ b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config.gleam @@ -1,13 +1,11 @@ import bungibindies/bun import bungibindies/bun/spawn import cynthia_websites_mini_client/configtype -import cynthia_websites_mini_client/configurable_variables 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/bit_array import gleam/bool -import gleam/dict import gleam/dynamic/decode import gleam/fetch import gleam/float @@ -27,7 +25,7 @@ import simplifile import tom /// # Config.load() -/// Loads the configuration from the `cynthia-mini.toml` file and the content from the `content` directory. +/// 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()) @@ -47,23 +45,78 @@ pub fn load() -> Promise(configtype.CompleteData) { } pub fn capture_config() { - let global_conf_filepath = - files.path_join([process.cwd(), "/cynthia-mini.toml"]) + 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 -> { - dialog_initcfg() - Nil + 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("Some file write error.") + 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) + } + } } } - use parse_configtoml_result <- promise.await(parse_configtoml()) + 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-mini.toml: " <> why, - ) + premixed.text_error_red("Error: Could not load cynthia.toml: " <> why) |> console.error process.exit(1) panic as "We should not reach here" @@ -73,436 +126,43 @@ pub fn capture_config() { |> promise.resolve() } -fn parse_configtoml() { - use str <- promise.try_await( - fs.read_file_sync(files.path_normalize( - process.cwd() <> "/cynthia-mini.toml", - )) - |> result.map_error(fn(e) { - premixed.text_error_red("Error: Could not read cynthia-mini.toml: " <> e) - process.exit(1) - }) - |> result.map_error(string.inspect) - |> promise.resolve(), - ) - use res <- promise.try_await( - tom.parse(str) |> result.map_error(string.inspect) |> promise.resolve(), - ) - - use config <- promise.try_await( - cynthia_config_global_only_exploiter(res) - |> promise.map(result.map_error(_, string.inspect)), - ) - promise.resolve(Ok(config)) +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)) } -type ConfigTomlDecodeError { - TomlGetStringError(tom.GetError) - TomlGetIntError(tom.GetError) - FieldError(String) +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 cynthia_config_global_only_exploiter( - o: dict.Dict(String, tom.Toml), -) -> Promise( - Result(configtype.SharedCynthiaConfigGlobalOnly, ConfigTomlDecodeError), -) { - use global_theme <- promise.try_await( - { - use field <- result.try( - tom.get(o, ["global", "theme"]) - |> result.replace_error(FieldError( - "config->global.theme does not exist", - )), - ) - tom.as_string(field) - |> result.map_error(TomlGetStringError) - } - |> promise.resolve(), - ) - use global_theme_dark <- promise.try_await( - { - use field <- result.try( - tom.get(o, ["global", "theme_dark"]) - |> result.replace_error(FieldError( - "config->global.theme_dark does not exist", - )), - ) - tom.as_string(field) - |> result.map_error(TomlGetStringError) - } - |> promise.resolve(), - ) - use global_colour <- promise.try_await( - { - use field <- result.try( - tom.get(o, ["global", "colour"]) - |> result.replace_error(FieldError( - "config->global.colour does not exist", - )), - ) - tom.as_string(field) - |> result.map_error(TomlGetStringError) - } - |> promise.resolve(), - ) - use global_site_name <- promise.try_await( - { - use field <- result.try( - tom.get(o, ["global", "site_name"]) - |> result.replace_error(FieldError( - "config->global.site_name does not exist", - )), - ) - tom.as_string(field) - |> result.map_error(TomlGetStringError) - } - |> promise.resolve(), +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 global_site_description <- promise.try_await( - { - use field <- result.try( - tom.get(o, ["global", "site_description"]) - |> result.replace_error(FieldError( - "config->global.site_description does not exist", - )), - ) - tom.as_string(field) - |> result.map_error(TomlGetStringError) - } - |> promise.resolve(), + use version <- result.try( + tom.get_int(d, ["config", "version"]) |> result.replace_error(Nil), ) - let server_port = - option.from_result({ - use field <- result.try( - tom.get(o, ["server", "port"]) - |> result.replace_error(FieldError("config->server.port does not exist")), - ) - tom.as_int(field) - |> result.map_error(TomlGetIntError) - }) - let server_host = - option.from_result({ - use field <- result.try( - tom.get(o, ["server", "host"]) - |> result.replace_error(FieldError("config->server.host does not exist")), - ) - tom.as_string(field) - |> result.map_error(TomlGetStringError) - }) - let comment_repo = case - tom.get(o, ["posts", "comment_repo"]) |> result.map(tom.as_string) - { - Ok(Ok(field)) -> { - Some(field) - } - _ -> None - } - let git_integration = case - tom.get(o, ["integrations", "git"]) |> result.map(tom.as_bool) - { - Ok(Ok(field)) -> { - field - } - _ -> True - } - let sitemap = case - tom.get(o, ["integrations", "sitemap"]) |> result.map(tom.as_string) - { - Ok(Ok(field)) -> { - case string.lowercase(field) { - "" -> None - "false" -> None - _ -> Some(field) - } - } - _ -> None - } - let crawlable_context = case - tom.get(o, ["integrations", "crawlable_context"]) |> result.map(tom.as_bool) - { - Ok(Ok(field)) -> { - field - } - _ -> False - } - let other_vars = case result.map(tom.get(o, ["variables"]), tom.as_table) { - Ok(Ok(d)) -> - { - dict.map_values(d, fn(key, unasserted_value) { - let promise_of_a_somewhat_asserted_value = case unasserted_value { - tom.InlineTable(inline) -> { - case inline |> dict.to_list() { - [#("url", tom.String(url))] -> { - let start = bun.nanoseconds() - console.log( - "Downloading external data ´" - <> premixed.text_blue(url) - <> "´...", - ) - - let assert Ok(req) = request.to(url) - use resp <- promise.await( - promise.map(fetch.send(req), fn(e) { - case e { - Ok(v) -> v - Error(_) -> { - console.error( - "There was an error while trying to download '" - <> url |> premixed.text_bright_yellow() - <> "' to a variable.", - ) - process.exit(1) - panic as "We should not reach here." - } - } - }), - ) - use resp <- promise.await( - promise.map(fetch.read_bytes_body(resp), fn(e) { - case e { - Ok(v) -> v - Error(_) -> { - console.error( - "There was an error while trying to download '" - <> url |> premixed.text_bright_yellow() - <> "' to a variable.", - ) - process.exit(1) - panic as "We should not reach here." - } - } - }), - ) - 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(url) - <> "´ in " - <> int.to_string(duration_ms |> float.truncate) - <> "ms!", - ) - [ - bit_array.base64_encode(resp.body, True), - configurable_variables.var_bitstring, - ] - } - _ -> { - console.error( - "There was an error while trying to download '" - <> url |> premixed.text_bright_yellow() - <> "' to a variable.", - ) - process.exit(1) - panic as "We should not reach here." - } - } - |> promise.resolve() - } - [#("path", tom.String(path))] -> { - // let file = bun.file(path) - // use content <- promise.await(bunfile.text()) - // `bunfile.text()` pretends it's infallible but is not. It should return a promised result. - // - // Also see: https://github.com/strawmelonjuice/bungibindies/issues/2 - // Also missing: bunfile.bits(), but that is also because the bitarray and byte array transform is scary to me. - // - // For now, this means we continue using the sync simplifile.read_bits() function, - case simplifile.read_bits(path) { - Ok(bits) -> [ - bit_array.base64_encode(bits, True), - configurable_variables.var_bitstring, - ] - Error(_) -> { - console.error( - "Unable to read file '" - <> path |> premixed.text_bright_yellow() - <> "' to variable.", - ) - process.exit(1) - panic as "Should not reach here." - } - } - |> promise.resolve() - } - _ -> - [configurable_variables.var_unsupported] - |> promise.resolve() - } - } - _ -> { - case unasserted_value { - tom.Bool(z) -> [ - bool.to_string(z), - configurable_variables.var_boolean, - ] - tom.Date(date) -> [ - date.year |> int.to_string, - date.month |> int.to_string, - date.day |> int.to_string, - configurable_variables.var_date, - ] - tom.DateTime(tom.DateTimeValue(date, time, offset)) -> { - case offset { - tom.Local -> [ - int.to_string(date.year), - int.to_string(date.month), - int.to_string(date.day), - int.to_string(time.hour), - int.to_string(time.minute), - int.to_string(time.second), - int.to_string(time.millisecond), - configurable_variables.var_datetime, - ] - _ -> [configurable_variables.var_unsupported] - } - } - tom.Float(a) -> [ - float.to_string(a), - configurable_variables.var_float, - ] - tom.Int(b) -> [int.to_string(b), configurable_variables.var_int] - tom.String(guitar) -> [ - guitar, - configurable_variables.var_string, - ] - tom.Time(time) -> [ - int.to_string(time.hour), - int.to_string(time.minute), - int.to_string(time.second), - int.to_string(time.millisecond), - configurable_variables.var_time, - ] - _ -> [configurable_variables.var_unsupported] - } - |> promise.resolve() - } - } - use reality <- promise.await( - promise_of_a_somewhat_asserted_value - |> promise.map(fn(somewhat_asserted_value) { - let assert Ok(conclusion) = somewhat_asserted_value |> list.last() - as "This must be a value, since we just actively set it above." - conclusion - }), - ) - use somewhat_asserted_value <- promise.await( - promise_of_a_somewhat_asserted_value, - ) - let expectation = - configurable_variables.typecontrolled - |> list.key_find(key) - |> result.unwrap(reality) - // Sometimes, reality can be transitioned into expectation - // --that's a horrible joke. - let #(reality, expectation, somewhat_asserted_value) = { - case reality, expectation { - "bits", "string" -> { - let assert Ok(b64) = somewhat_asserted_value |> list.first() - let hopefully_bits = b64 |> bit_array.base16_decode - case hopefully_bits { - Ok(bits) -> { - case bits |> bit_array.to_string() { - Ok(str) -> #( - configurable_variables.var_unsupported, - expectation, - [str, configurable_variables.var_string], - ) - Error(..) -> #( - configurable_variables.var_unsupported, - expectation, - [configurable_variables.var_unsupported], - ) - } - } - Error(..) -> { - #(configurable_variables.var_unsupported, expectation, [ - configurable_variables.var_unsupported, - ]) - } - } - } - _, _ -> #(reality, expectation, somewhat_asserted_value) - } - } - let z: Result(List(String), ConfigTomlDecodeError) = case - reality == configurable_variables.var_unsupported - { - True -> - Error(FieldError( - "variables->" <> key <> " does not contain a supported value.", - )) - False -> { - { expectation == reality } - |> bool.guard(Ok(somewhat_asserted_value), fn() { - Error(FieldError( - "variables->" - <> key - <> " does not contain the expected value. --> Expected: " - <> expectation - <> ", got: " - <> reality, - )) - }) - } - } - promise.resolve(z) - }) - } - |> dict.to_list() - |> list.map(fn(x) { - let #(key, promise_of_a_value) = x - use value <- promise.await(promise_of_a_value) - promise.resolve(#(key, value)) - }) - |> promise.await_list() - _ -> promise.resolve([]) - } - use other_vars <- promise.await(other_vars) - // A kind of manual result.all() - let other_vars = case - list.find_map(other_vars, fn(le) { - let #(_key, result_of_value): #( - String, - Result(List(String), ConfigTomlDecodeError), - ) = le - case result_of_value { - Error(err) -> Ok(err) - _ -> Error(Nil) - } - }) - { - Ok(pq) -> Error(pq) - Error(Nil) -> { - other_vars - |> list.map(fn(it) { - let assert Ok(b) = it.1 - #(it.0, b) - }) - |> Ok - } - } - - use other_vars <- promise.try_await(other_vars |> promise.resolve) - - Ok(configtype.SharedCynthiaConfigGlobalOnly( - global_theme:, - global_theme_dark:, - global_colour:, - global_site_name:, - global_site_description:, - server_port:, - server_host:, - git_integration:, - crawlable_context:, - sitemap:, - comment_repo:, - other_vars:, - )) - |> promise.resolve() + Ok(#(edition, version)) } fn content_getter() -> promise.Promise( @@ -746,7 +406,7 @@ fn dialog_initcfg() { case prompts.for_confirmation( "CynthiaMini can create \n" - <> premixed.text_orange(process.cwd() <> "/cynthia-mini.toml") + <> 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?", @@ -763,87 +423,71 @@ fn dialog_initcfg() { } } -pub fn initcfg() { - console.log("Creating Cynthia Mini configuration...") - // Check if cynthia-mini.toml exists - case files.file_exist(process.cwd() <> "/cynthia-mini.toml") { - True -> { - console.error( - "Error: A config already exists in this directory. Please remove it and try again.", - ) - process.exit(1) - panic as "We should not reach here" - } - False -> Nil - } - let assert Ok(_) = - simplifile.create_directory_all(process.cwd() <> "/content") - let assert Ok(_) = simplifile.create_directory_all(process.cwd() <> "/assets") - let _ = - { process.cwd() <> "/cynthia-mini.toml" } - |> fs.write_file_sync( - "[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 = \"\"\" -
+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 = \"\"\" +

{{ title }}

- \"\"\" +\"\"\" - [posts] - # Enable comments on posts using utteranc.es - # Format: \"username/repositoryname\" - # - # You will need to give the utterances bot access to your repo. - # See https://github.com/apps/utterances to add the utterances bot to your repo - comment_repo = \"\" - ", - ) +[posts] +# Enable comments on posts using utteranc.es +# Format: \"username/repositoryname\" +# +# You will need to give the utterances bot access to your repo. +# See https://github.com/apps/utterances to add the utterances bot to your repo +comment_repo = \"\"" + +pub fn initcfg() { + console.log("Creating Cynthia Mini configuration...") + // Check if cynthia.toml exists + case files.file_exist(process.cwd() <> "/cynthia.toml") { + True -> { + console.error( + "Error: A config already exists in this directory. Please remove it and try again.", + ) + process.exit(1) + panic as "We should not reach here" + } + False -> Nil + } + let assert Ok(_) = + simplifile.create_directory_all(process.cwd() <> "/content") + let assert Ok(_) = simplifile.create_directory_all(process.cwd() <> "/assets") + let _ = + { process.cwd() <> "/cynthia.toml" } + |> fs.write_file_sync(brand_new_config) |> result.map_error(fn(e) { - premixed.text_error_red("Error: Could not write cynthia-mini.toml: " <> e) + premixed.text_error_red("Error: Could not write cynthia.toml: " <> e) process.exit(1) }) { diff --git a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config/v4.gleam b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config/v4.gleam new file mode 100644 index 0000000..73e54bd --- /dev/null +++ b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config/v4.gleam @@ -0,0 +1,457 @@ +//// Cynthia v4 Config format + +import bungibindies/bun +import cynthia_websites_mini_client/configtype +import cynthia_websites_mini_client/configurable_variables +import cynthia_websites_mini_server/utils/files +import gleam/bit_array +import gleam/bool +import gleam/dict +import gleam/fetch +import gleam/float +import gleam/http/request +import gleam/int +import gleam/javascript/promise.{type Promise} +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 + +/// Parses the mini edition format for v4 +pub fn parse_mini() -> Promise( + Result(configtype.SharedCynthiaConfigGlobalOnly, String), +) { + use str <- promise.try_await( + fs.read_file_sync(files.path_normalize(process.cwd() <> "/cynthia.toml")) + |> result.map_error(fn(e) { + premixed.text_error_red("Error: Could not read cynthia.toml: " <> e) + process.exit(1) + }) + |> result.map_error(string.inspect) + |> promise.resolve(), + ) + use res <- promise.try_await( + tom.parse(str) |> result.map_error(string.inspect) |> promise.resolve(), + ) + + use config <- promise.try_await( + cynthia_config_global_only_exploiter(res) + |> promise.map(result.map_error(_, string.inspect)), + ) + promise.resolve(Ok(config)) +} + +type ConfigTomlDecodeError { + TomlGetStringError(tom.GetError) + TomlGetIntError(tom.GetError) + FieldError(String) +} + +fn cynthia_config_global_only_exploiter( + o: dict.Dict(String, tom.Toml), +) -> Promise( + Result(configtype.SharedCynthiaConfigGlobalOnly, ConfigTomlDecodeError), +) { + use global_theme <- promise.try_await( + { + use field <- result.try( + tom.get(o, ["global", "theme"]) + |> result.replace_error(FieldError( + "config->global.theme does not exist", + )), + ) + tom.as_string(field) + |> result.map_error(TomlGetStringError) + } + |> promise.resolve(), + ) + use global_theme_dark <- promise.try_await( + { + use field <- result.try( + tom.get(o, ["global", "theme_dark"]) + |> result.replace_error(FieldError( + "config->global.theme_dark does not exist", + )), + ) + tom.as_string(field) + |> result.map_error(TomlGetStringError) + } + |> promise.resolve(), + ) + use global_colour <- promise.try_await( + { + use field <- result.try( + tom.get(o, ["global", "colour"]) + |> result.replace_error(FieldError( + "config->global.colour does not exist", + )), + ) + tom.as_string(field) + |> result.map_error(TomlGetStringError) + } + |> promise.resolve(), + ) + use global_site_name <- promise.try_await( + { + use field <- result.try( + tom.get(o, ["global", "site_name"]) + |> result.replace_error(FieldError( + "config->global.site_name does not exist", + )), + ) + tom.as_string(field) + |> result.map_error(TomlGetStringError) + } + |> promise.resolve(), + ) + use global_site_description <- promise.try_await( + { + use field <- result.try( + tom.get(o, ["global", "site_description"]) + |> result.replace_error(FieldError( + "config->global.site_description does not exist", + )), + ) + tom.as_string(field) + |> result.map_error(TomlGetStringError) + } + |> promise.resolve(), + ) + let server_port = + option.from_result({ + use field <- result.try( + tom.get(o, ["server", "port"]) + |> result.replace_error(FieldError("config->server.port does not exist")), + ) + tom.as_int(field) + |> result.map_error(TomlGetIntError) + }) + let server_host = + option.from_result({ + use field <- result.try( + tom.get(o, ["server", "host"]) + |> result.replace_error(FieldError("config->server.host does not exist")), + ) + tom.as_string(field) + |> result.map_error(TomlGetStringError) + }) + let comment_repo = case + tom.get(o, ["posts", "comment_repo"]) |> result.map(tom.as_string) + { + Ok(Ok(field)) -> { + Some(field) + } + _ -> None + } + let git_integration = case + tom.get(o, ["integrations", "git"]) |> result.map(tom.as_bool) + { + Ok(Ok(field)) -> { + field + } + _ -> True + } + let sitemap = case + tom.get(o, ["integrations", "sitemap"]) |> result.map(tom.as_string) + { + Ok(Ok(field)) -> { + case string.lowercase(field) { + "" -> None + "false" -> None + _ -> Some(field) + } + } + _ -> None + } + let crawlable_context = case + tom.get(o, ["integrations", "crawlable_context"]) |> result.map(tom.as_bool) + { + Ok(Ok(field)) -> { + field + } + _ -> False + } + let other_vars = case result.map(tom.get(o, ["variables"]), tom.as_table) { + Ok(Ok(d)) -> + { + dict.map_values(d, fn(key, unasserted_value) { + let promise_of_a_somewhat_asserted_value = case unasserted_value { + tom.InlineTable(inline) -> { + case inline |> dict.to_list() { + [#("url", tom.String(url))] -> { + let start = bun.nanoseconds() + console.log( + "Downloading external data ´" + <> premixed.text_blue(url) + <> "´...", + ) + + let assert Ok(req) = request.to(url) + use resp <- promise.await( + promise.map(fetch.send(req), fn(e) { + case e { + Ok(v) -> v + Error(_) -> { + console.error( + "There was an error while trying to download '" + <> url |> premixed.text_bright_yellow() + <> "' to a variable.", + ) + process.exit(1) + panic as "We should not reach here." + } + } + }), + ) + use resp <- promise.await( + promise.map(fetch.read_bytes_body(resp), fn(e) { + case e { + Ok(v) -> v + Error(_) -> { + console.error( + "There was an error while trying to download '" + <> url |> premixed.text_bright_yellow() + <> "' to a variable.", + ) + process.exit(1) + panic as "We should not reach here." + } + } + }), + ) + 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(url) + <> "´ in " + <> int.to_string(duration_ms |> float.truncate) + <> "ms!", + ) + [ + bit_array.base64_encode(resp.body, True), + configurable_variables.var_bitstring, + ] + } + _ -> { + console.error( + "There was an error while trying to download '" + <> url |> premixed.text_bright_yellow() + <> "' to a variable.", + ) + process.exit(1) + panic as "We should not reach here." + } + } + |> promise.resolve() + } + [#("path", tom.String(path))] -> { + // let file = bun.file(path) + // use content <- promise.await(bunfile.text()) + // `bunfile.text()` pretends it's infallible but is not. It should return a promised result. + // + // Also see: https://github.com/strawmelonjuice/bungibindies/issues/2 + // Also missing: bunfile.bits(), but that is also because the bitarray and byte array transform is scary to me. + // + // For now, this means we continue using the sync simplifile.read_bits() function, + case simplifile.read_bits(path) { + Ok(bits) -> [ + bit_array.base64_encode(bits, True), + configurable_variables.var_bitstring, + ] + Error(_) -> { + console.error( + "Unable to read file '" + <> path |> premixed.text_bright_yellow() + <> "' to variable.", + ) + process.exit(1) + panic as "Should not reach here." + } + } + |> promise.resolve() + } + _ -> + [configurable_variables.var_unsupported] + |> promise.resolve() + } + } + _ -> { + case unasserted_value { + tom.Bool(z) -> [ + bool.to_string(z), + configurable_variables.var_boolean, + ] + tom.Date(date) -> [ + date.year |> int.to_string, + date.month |> int.to_string, + date.day |> int.to_string, + configurable_variables.var_date, + ] + tom.DateTime(tom.DateTimeValue(date, time, offset)) -> { + case offset { + tom.Local -> [ + int.to_string(date.year), + int.to_string(date.month), + int.to_string(date.day), + int.to_string(time.hour), + int.to_string(time.minute), + int.to_string(time.second), + int.to_string(time.millisecond), + configurable_variables.var_datetime, + ] + _ -> [configurable_variables.var_unsupported] + } + } + tom.Float(a) -> [ + float.to_string(a), + configurable_variables.var_float, + ] + tom.Int(b) -> [int.to_string(b), configurable_variables.var_int] + tom.String(guitar) -> [ + guitar, + configurable_variables.var_string, + ] + tom.Time(time) -> [ + int.to_string(time.hour), + int.to_string(time.minute), + int.to_string(time.second), + int.to_string(time.millisecond), + configurable_variables.var_time, + ] + _ -> [configurable_variables.var_unsupported] + } + |> promise.resolve() + } + } + use reality <- promise.await( + promise_of_a_somewhat_asserted_value + |> promise.map(fn(somewhat_asserted_value) { + let assert Ok(conclusion) = somewhat_asserted_value |> list.last() + as "This must be a value, since we just actively set it above." + conclusion + }), + ) + use somewhat_asserted_value <- promise.await( + promise_of_a_somewhat_asserted_value, + ) + let expectation = + configurable_variables.typecontrolled + |> list.key_find(key) + |> result.unwrap(reality) + // Sometimes, reality can be transitioned into expectation + // --that's a horrible joke. + let #(reality, expectation, somewhat_asserted_value) = { + case reality, expectation { + "bits", "string" -> { + let assert Ok(b64) = somewhat_asserted_value |> list.first() + let hopefully_bits = b64 |> bit_array.base16_decode + case hopefully_bits { + Ok(bits) -> { + case bits |> bit_array.to_string() { + Ok(str) -> #( + configurable_variables.var_unsupported, + expectation, + [str, configurable_variables.var_string], + ) + Error(..) -> #( + configurable_variables.var_unsupported, + expectation, + [configurable_variables.var_unsupported], + ) + } + } + Error(..) -> { + #(configurable_variables.var_unsupported, expectation, [ + configurable_variables.var_unsupported, + ]) + } + } + } + _, _ -> #(reality, expectation, somewhat_asserted_value) + } + } + let z: Result(List(String), ConfigTomlDecodeError) = case + reality == configurable_variables.var_unsupported + { + True -> + Error(FieldError( + "variables->" <> key <> " does not contain a supported value.", + )) + False -> { + { expectation == reality } + |> bool.guard(Ok(somewhat_asserted_value), fn() { + Error(FieldError( + "variables->" + <> key + <> " does not contain the expected value. --> Expected: " + <> expectation + <> ", got: " + <> reality, + )) + }) + } + } + promise.resolve(z) + }) + } + |> dict.to_list() + |> list.map(fn(x) { + let #(key, promise_of_a_value) = x + use value <- promise.await(promise_of_a_value) + promise.resolve(#(key, value)) + }) + |> promise.await_list() + _ -> promise.resolve([]) + } + use other_vars <- promise.await(other_vars) + // A kind of manual result.all() + let other_vars = case + list.find_map(other_vars, fn(le) { + let #(_key, result_of_value): #( + String, + Result(List(String), ConfigTomlDecodeError), + ) = le + case result_of_value { + Error(err) -> Ok(err) + _ -> Error(Nil) + } + }) + { + Ok(pq) -> Error(pq) + Error(Nil) -> { + other_vars + |> list.map(fn(it) { + let assert Ok(b) = it.1 + #(it.0, b) + }) + |> Ok + } + } + + use other_vars <- promise.try_await(other_vars |> promise.resolve) + + Ok(configtype.SharedCynthiaConfigGlobalOnly( + global_theme:, + global_theme_dark:, + global_colour:, + global_site_name:, + global_site_description:, + server_port:, + server_host:, + git_integration:, + crawlable_context:, + sitemap:, + comment_repo:, + other_vars:, + )) + |> promise.resolve() +} From 47195384c045ac817e0f9161b287f453736a89d6 Mon Sep 17 00:00:00 2001 From: MLC Bloeiman Date: Fri, 28 Nov 2025 15:10:12 +0100 Subject: [PATCH 2/9] feat: Add variable for #23 --- .../src/cynthia_websites_mini_client.gleam | 4 ++-- .../contenttypes.gleam | 12 ++++++++++-- .../cynthia_websites_mini_client/pottery.gleam | 6 +++++- .../pottery/molds/cindy_simple.gleam | 16 +++++++++++++++- .../src/cynthia_websites_mini_client/view.gleam | 10 +++++----- .../cynthia_websites_mini_server/config.gleam | 8 ++++---- 6 files changed, 41 insertions(+), 15 deletions(-) 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 1decd09..4a765a8 100644 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client.gleam +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client.gleam @@ -573,7 +573,7 @@ fn compute_menus(content: List(contenttypes.Content), model: Model) { content |> list.filter_map(fn(alls) { case alls.data { - contenttypes.PageData(soms) -> Ok(soms) + contenttypes.PageData(soms, _) -> Ok(soms) _ -> Error(Nil) } }) @@ -595,7 +595,7 @@ fn add_each_menu( let hits: List(model_type.MenuItem) = list.filter_map(items, fn(item) -> Result(model_type.MenuItem, Nil) { case item.data { - contenttypes.PageData(m) -> { + contenttypes.PageData(m, _) -> { case m |> list.contains(current_menu) { True -> { Ok(model_type.MenuItem(name: item.title, to: item.permalink)) 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 index 1aff8e3..4e0167d 100644 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/contenttypes.gleam +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/contenttypes.gleam @@ -116,6 +116,8 @@ pub type ContentData { 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, ) } @@ -131,7 +133,12 @@ pub fn content_data_decoder() -> decode.Decoder(ContentData) { } "page_data" -> { use in_menus <- decode.field("in_menus", decode.list(decode.int)) - decode.success(PageData(in_menus:)) + use hide_meta_block <- decode.optional_field( + "hide_meta", + False, + decode.bool, + ) + decode.success(PageData(in_menus:, hide_meta_block:)) } _ -> decode.failure( @@ -151,10 +158,11 @@ pub fn encode_content_data(content_data: ContentData) -> json.Json { #("category", json.string(category)), #("tags", json.array(tags, json.string)), ]) - PageData(in_menus:) -> + 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)), ]) } } 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 7a174c6..6a17b75 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 @@ -24,7 +24,7 @@ pub fn render_content( let assert Ok(def) = paints.get_sytheme(model) let #(into, output, variables) = case content.data { - contenttypes.PageData(_) -> { + contenttypes.PageData(_, hide_metadata_block) -> { let mold = case content.layout { "default" | "theme" | "" -> molds.into(def.layout, "page", model) layout -> molds.into(layout, "page", model) @@ -39,6 +39,10 @@ pub fn render_content( |> 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:) -> { 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/pottery/molds/cindy_simple.gleam index a3e9575..e408907 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/pottery/molds/cindy_simple.gleam @@ -43,6 +43,7 @@ pub fn page_layout( ), decode.string, ) + html.div([attribute.class("break-words")], [ html.h3( [ @@ -200,6 +201,15 @@ fn cindy_common( |> result.unwrap(dynamic.from(option.None)) |> decode.run(decode.string) } + let hide_metadata_block = + decode.run( + result.unwrap( + dict.get(variables, "hide_metadata_block"), + dynamic.from(False), + ), + decode.bool, + ) + |> result.unwrap(False) html.div([attribute.id("content"), attribute.class("w-full mb-2")], [ html.span([], [ html.div( @@ -340,7 +350,11 @@ fn cindy_common( html.div( [ attribute.class( - "col-span-5 row-span-4 row-start-9 md:row-span-8 md:col-span[] md:col-start-1 md:row-start-2 min-h-full bg-base-200 rounded-br-2xl overflow-auto w-full md:w-fit md:max-w-[20VW] p-4 md:p-3 break-words shadow-inner", + "col-span-5 row-span-4 row-start-9 md:row-span-8 md:col-span[] md:col-start-1 md:row-start-2 min-h-full bg-base-200 rounded-br-2xl overflow-auto w-full md:w-fit md:max-w-[20VW] p-4 md:p-3 break-words shadow-inner" + <> case hide_metadata_block { + True -> " hidden" + False -> "" + }, ), ], [post_meta], 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 index ff0b4b8..625e402 100644 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/view.gleam +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/view.gleam @@ -32,7 +32,7 @@ pub fn main(model: Model) -> Element(Msg) { layout: "theme", permalink: "404", inner_plain: "# 404!\n\nThe page you are looking for does not exist.", - data: contenttypes.PageData([]), + data: contenttypes.PageData([], True), ) }) let content = case model.path { @@ -57,7 +57,7 @@ pub fn main(model: Model) -> Element(Msg) { layout: "default", permalink: model.path, filename: "postlist.html", - data: contenttypes.PageData([]), + data: contenttypes.PageData([], False), inner_plain: postlistloader.postlist_by_category( model, category, @@ -78,7 +78,7 @@ pub fn main(model: Model) -> Element(Msg) { layout: "default", permalink: model.path, filename: "postlist.html", - data: contenttypes.PageData([]), + data: contenttypes.PageData([], True), inner_plain: postlistloader.postlist_by_tag(model, tag) |> element.to_string, ) @@ -93,7 +93,7 @@ pub fn main(model: Model) -> Element(Msg) { layout: "default", permalink: model.path, filename: "postlist.html", - data: contenttypes.PageData([]), + data: contenttypes.PageData([], False), inner_plain: postlistloader.postlist_by_search_term( model, search_term, @@ -110,7 +110,7 @@ pub fn main(model: Model) -> Element(Msg) { layout: "default", permalink: model.path, filename: "postlist.html", - data: contenttypes.PageData([]), + data: contenttypes.PageData([], False), inner_plain: postlistloader.postlist_all(model) |> element.to_string, ) 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 index a9b0020..6721d98 100644 --- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config.gleam +++ b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config.gleam @@ -583,7 +583,7 @@ pub fn initcfg() { description: "An example page about hangers", layout: "theme", permalink: "/hangers", - data: contenttypes.PageData(in_menus: [2]), + data: contenttypes.PageData([2], False), inner_plain: "I have no clue. What are hangers again? This page will only show up if you have a layout with two or more menus available! :)", @@ -600,7 +600,7 @@ This page will only show up if you have a layout with two or more menus availabl description: "External page example, using the theme list, downloading from ", layout: "theme", permalink: "/themes", - data: contenttypes.PageData(in_menus: [1]), + data: contenttypes.PageData([1], False), inner_plain: "", ), ), @@ -612,7 +612,7 @@ This page will only show up if you have a layout with two or more menus availabl description: "This is an example index page", layout: "cindy-landing", permalink: "/", - data: contenttypes.PageData(in_menus: [1]), + data: contenttypes.PageData([1], True), inner_plain: configtype.ootb_index, ), ), @@ -641,7 +641,7 @@ This page will only show up if you have a layout with two or more menus availabl description: "this page is not actually shown, due to the ! prefix in the permalink", layout: "default", permalink: "!/", - data: contenttypes.PageData(in_menus: [1]), + data: contenttypes.PageData(in_menus: [1], hide_meta_block: True), inner_plain: "", ), ), From f82c2d8f29f6f614eb25c0faf0e53099973fad88 Mon Sep 17 00:00:00 2001 From: MLC Bloeiman Date: Sat, 29 Nov 2025 18:44:08 +0100 Subject: [PATCH 3/9] feat: Apply #23 to both cindy themes --- .../pottery/molds/cindy_dual.gleam | 16 +++++++++++++++- .../pottery/molds/cindy_simple.gleam | 10 ++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) 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/pottery/molds/cindy_dual.gleam index c77a0d0..1c125e2 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/pottery/molds/cindy_dual.gleam @@ -176,6 +176,19 @@ fn cindy_common( |> result.unwrap(dynamic.from(option.None)) |> decode.run(decode.string) } + let hide_metadata_block = + decode.run( + result.unwrap( + dict.get(variables, "hide_metadata_block"), + dynamic.from(False), + ), + decode.bool, + ) + |> result.unwrap(False) + let hide_metadata_block_classonly = case hide_metadata_block { + True -> " hidden" + False -> "" + } html.div([attribute.id("content"), attribute.class("w-full mb-2")], [ html.span([], [ html.div( @@ -344,7 +357,8 @@ fn cindy_common( html.div( [ attribute.class( - "col-span-5 row-span-4 row-start-9 md:row-span-8 md:col-span[] md:col-start-1 md:row-start-3 min-h-full bg-base-200 rounded-br-2xl overflow-auto w-full md:w-fit md:max-w-[20VW] p-4 md:p-3 break-words shadow-inner", + "col-span-5 row-span-4 row-start-9 md:row-span-8 md:col-span[] md:col-start-1 md:row-start-3 min-h-full bg-base-200 rounded-br-2xl overflow-auto w-full md:w-fit md:max-w-[20VW] p-4 md:p-3 break-words shadow-inner" + <> hide_metadata_block_classonly, ), ], [post_meta], 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/pottery/molds/cindy_simple.gleam index e408907..b93b43c 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/pottery/molds/cindy_simple.gleam @@ -210,8 +210,13 @@ fn cindy_common( decode.bool, ) |> result.unwrap(False) + let hide_metadata_block_classonly = case hide_metadata_block { + True -> " hidden" + False -> "" + } html.div([attribute.id("content"), attribute.class("w-full mb-2")], [ html.span([], [ + // element.text(variables |> string.inspect), html.div( [ attribute.class( @@ -351,10 +356,7 @@ fn cindy_common( [ attribute.class( "col-span-5 row-span-4 row-start-9 md:row-span-8 md:col-span[] md:col-start-1 md:row-start-2 min-h-full bg-base-200 rounded-br-2xl overflow-auto w-full md:w-fit md:max-w-[20VW] p-4 md:p-3 break-words shadow-inner" - <> case hide_metadata_block { - True -> " hidden" - False -> "" - }, + <> hide_metadata_block_classonly, ), ], [post_meta], From ad5fd9abaf4dfbe6f1f5801f0a1195f755d10d6d Mon Sep 17 00:00:00 2001 From: MLC Bloeiman Date: Sat, 29 Nov 2025 19:05:10 +0100 Subject: [PATCH 4/9] fix: Remove invalid custom value classes --- .../cynthia_websites_mini_client/pottery/molds/cindy_dual.gleam | 2 +- .../pottery/molds/cindy_simple.gleam | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/pottery/molds/cindy_dual.gleam index 1c125e2..5460659 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/pottery/molds/cindy_dual.gleam @@ -357,7 +357,7 @@ fn cindy_common( html.div( [ attribute.class( - "col-span-5 row-span-4 row-start-9 md:row-span-8 md:col-span[] md:col-start-1 md:row-start-3 min-h-full bg-base-200 rounded-br-2xl overflow-auto w-full md:w-fit md:max-w-[20VW] p-4 md:p-3 break-words shadow-inner" + "col-span-5 row-span-4 row-start-9 md:row-span-8 md:col-start-1 md:row-start-3 min-h-full bg-base-200 rounded-br-2xl overflow-auto w-full md:w-fit md:max-w-[20VW] p-4 md:p-3 break-words shadow-inner" <> hide_metadata_block_classonly, ), ], 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/pottery/molds/cindy_simple.gleam index b93b43c..9218f86 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/pottery/molds/cindy_simple.gleam @@ -355,7 +355,7 @@ fn cindy_common( html.div( [ attribute.class( - "col-span-5 row-span-4 row-start-9 md:row-span-8 md:col-span[] md:col-start-1 md:row-start-2 min-h-full bg-base-200 rounded-br-2xl overflow-auto w-full md:w-fit md:max-w-[20VW] p-4 md:p-3 break-words shadow-inner" + "col-span-5 row-span-4 row-start-9 md:row-span-8 md:col-start-1 md:row-start-2 min-h-full bg-base-200 rounded-br-2xl overflow-auto w-full md:w-fit md:max-w-[20VW] p-4 md:p-3 break-words shadow-inner" <> hide_metadata_block_classonly, ), ], From 6fa0d0a6c2f3866be2ff95d4b98d25dd5f371365 Mon Sep 17 00:00:00 2001 From: MLC Bloeiman Date: Sat, 29 Nov 2025 19:06:29 +0100 Subject: [PATCH 5/9] fix: Update themes.dj link --- .../src/cynthia_websites_mini_server/config.gleam | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 6721d98..e6d7140 100644 --- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config.gleam +++ b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config.gleam @@ -593,11 +593,11 @@ This page will only show up if you have a layout with two or more menus availabl to: "themes.dj", // We are downloading markdown content as Djot content without conversion... Hopefully it'll parse correctly. // Until the documentation is updated to reflect the new default file type :) - from: "https://raw.githubusercontent.com/CynthiaWebsiteEngine/Mini-docs/refs/heads/main/content/3.%20Customisation/3.2-themes.md", + from: "https://raw.githubusercontent.com/CynthiaWebsiteEngine/Mini-docs/refs/heads/main/content/3.%20Customisation/3.2-themes.dj", with: contenttypes.Content( filename: "themes.dj", title: "Themes", - description: "External page example, using the theme list, downloading from ", + description: "External page example, using the theme list, downloading from ", layout: "theme", permalink: "/themes", data: contenttypes.PageData([1], False), From 6a685e8a36f6a8deb2899346ab15e197b802144e Mon Sep 17 00:00:00 2001 From: MLC Bloeiman Date: Sat, 29 Nov 2025 19:07:35 +0100 Subject: [PATCH 6/9] fix: Fixed indiscriptive error message. Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../src/cynthia_websites_mini_server/config.gleam | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 index e6d7140..2facbe3 100644 --- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config.gleam +++ b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config.gleam @@ -81,7 +81,11 @@ pub fn capture_config() { Nil } Error(_) -> { - console.error("Some file write error.") + console.error( + "Error: Could not write upgraded config to " + <> global_conf_filepath + <> ". Please check file permissions.", + ) process.exit(1) } } From 982fe5a73d795823b8713d6bcaa9a1a59f4b334a Mon Sep 17 00:00:00 2001 From: MLC Bloeiman Date: Sat, 29 Nov 2025 19:12:42 +0100 Subject: [PATCH 7/9] fix: base64 decode failing because we used a base16 decoder Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../src/cynthia_websites_mini_server/config/v4.gleam | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config/v4.gleam b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config/v4.gleam index 73e54bd..e39264d 100644 --- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config/v4.gleam +++ b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config/v4.gleam @@ -353,7 +353,7 @@ fn cynthia_config_global_only_exploiter( case reality, expectation { "bits", "string" -> { let assert Ok(b64) = somewhat_asserted_value |> list.first() - let hopefully_bits = b64 |> bit_array.base16_decode + let hopefully_bits = b64 |> bit_array.base64_decode case hopefully_bits { Ok(bits) -> { case bits |> bit_array.to_string() { From 00cf1a5e5e4c9bbd92bf78002102d14d3ae499e7 Mon Sep 17 00:00:00 2001 From: MLC Bloeiman Date: Sat, 29 Nov 2025 19:10:34 +0100 Subject: [PATCH 8/9] fix: Add error handling for invalid urls in external content --- .../cynthia_websites_mini_server/config/v4.gleam | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config/v4.gleam b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config/v4.gleam index e39264d..4306bcf 100644 --- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config/v4.gleam +++ b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config/v4.gleam @@ -192,7 +192,18 @@ fn cynthia_config_global_only_exploiter( <> "´...", ) - let assert Ok(req) = request.to(url) + let req = case request.to(url) { + Ok(r) -> r + Error(_) -> { + console.error( + "Invalid URL for variable: '" + <> url |> premixed.text_bright_yellow() + <> "'.", + ) + process.exit(1) + panic as "We should not reach here." + } + } use resp <- promise.await( promise.map(fetch.send(req), fn(e) { case e { From 7e82ebe7637aa52368fe18b5cf2101fc78174716 Mon Sep 17 00:00:00 2001 From: MLC Bloeiman Date: Sat, 29 Nov 2025 19:18:39 +0100 Subject: [PATCH 9/9] fix: 'Error: Could not write cynthia.toml: ' not logged --- .../src/cynthia_websites_mini_server/config.gleam | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 index 2facbe3..e2afaa7 100644 --- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config.gleam +++ b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config.gleam @@ -552,7 +552,9 @@ pub fn initcfg() { { process.cwd() <> "/cynthia.toml" } |> fs.write_file_sync(brand_new_config) |> result.map_error(fn(e) { - premixed.text_error_red("Error: Could not write cynthia.toml: " <> e) + console.error(premixed.text_error_red( + "Error: Could not write cynthia.toml: " <> e, + )) process.exit(1) }) {