diff --git a/bun.lock b/bun.lock index 67cd89f..d87c275 100644 --- a/bun.lock +++ b/bun.lock @@ -1,8 +1,12 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "@cynthiaweb/cynthiawebsiteengine-mini", + "dependencies": { + "@djot/djot": "^0.3.2", + }, "devDependencies": { "@types/bun": "latest", "@types/clean-css": "^4.2.11", @@ -26,6 +30,8 @@ "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@djot/djot": ["@djot/djot@0.3.2", "", { "bin": { "djot": "lib/cli.js" } }, "sha512-joMKR24B8rxueyFiJbpZAqEiypjvOyzTxzkhyr0q5mM/sUBaOD3unna/9IxtOotFugViyYlkIRaiXg3xM//zxg=="], + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], diff --git a/cynthia_websites_mini_client/README.md b/cynthia_websites_mini_client/README.md deleted file mode 100644 index 05b4a2c..0000000 --- a/cynthia_websites_mini_client/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Cynthia Mini's client package - -Cynthia Mini consists of two packages: A client (you are here) and a server (cynthia_websites_mini_server). - -To install or use Cynthia Mini, please refer to [GitHub releases](https://github.com/CynthiaWebsiteEngine/Mini/releases). diff --git a/cynthia_websites_mini_client/birdie_snapshots/autolinks_angle_brackets_multiple_test.new b/cynthia_websites_mini_client/birdie_snapshots/autolinks_angle_brackets_multiple_test.new deleted file mode 100644 index e809181..0000000 --- a/cynthia_websites_mini_client/birdie_snapshots/autolinks_angle_brackets_multiple_test.new +++ /dev/null @@ -1,21 +0,0 @@ ---- -version: 1.3.1 -title: autolinks_angle_brackets_multiple_test ---- -
-

- See - - https://example.com - - , - - http://foo.bar/baz - - , and - - https://sub.domain.tld/path?x=1&y=2 - - in one line. -

-
\ No newline at end of file diff --git a/cynthia_websites_mini_client/birdie_snapshots/autolinks_mailto_and_mixed_content_test.new b/cynthia_websites_mini_client/birdie_snapshots/autolinks_mailto_and_mixed_content_test.new deleted file mode 100644 index 28481a4..0000000 --- a/cynthia_websites_mini_client/birdie_snapshots/autolinks_mailto_and_mixed_content_test.new +++ /dev/null @@ -1,17 +0,0 @@ ---- -version: 1.3.1 -title: autolinks_mailto_and_mixed_content_test ---- -
-

- Contact - - mailto:info@example.com - - or see - - https://example.com/readme.md - - . -

-
\ No newline at end of file diff --git a/cynthia_websites_mini_client/birdie_snapshots/autolinks_non_angle_urls_should_not_break_test.new b/cynthia_websites_mini_client/birdie_snapshots/autolinks_non_angle_urls_should_not_break_test.new deleted file mode 100644 index 3055cc3..0000000 --- a/cynthia_websites_mini_client/birdie_snapshots/autolinks_non_angle_urls_should_not_break_test.new +++ /dev/null @@ -1,9 +0,0 @@ ---- -version: 1.3.1 -title: autolinks_non_angle_urls_should_not_break_test ---- -
-

- Visit https://no-brackets.example/path and http://another.example/query?x=1. -

-
\ No newline at end of file diff --git a/cynthia_websites_mini_client/birdie_snapshots/autolinks_test.accepted b/cynthia_websites_mini_client/birdie_snapshots/autolinks_test.accepted deleted file mode 100644 index 9b7f7b7..0000000 --- a/cynthia_websites_mini_client/birdie_snapshots/autolinks_test.accepted +++ /dev/null @@ -1,14 +0,0 @@ ---- -version: 1.3.0 -title: autolinks_test -file: ./test/cynthia_websites_mini_client_test.gleam -test_name: autolinks_test ---- -
-

- External page example, using the theme list, downloading from - - https://raw.githubusercontent.com/CynthiaWebsiteEngine/Mini-docs/refs/heads/main/content/3.%20customisation/3.2-themes.md - -

-
\ No newline at end of file diff --git a/cynthia_websites_mini_client/birdie_snapshots/blockquote_with_links_and_paragraphs_test.new b/cynthia_websites_mini_client/birdie_snapshots/blockquote_with_links_and_paragraphs_test.new deleted file mode 100644 index f5e3a4f..0000000 --- a/cynthia_websites_mini_client/birdie_snapshots/blockquote_with_links_and_paragraphs_test.new +++ /dev/null @@ -1,15 +0,0 @@ ---- -version: 1.3.1 -title: blockquote_with_links_and_paragraphs_test ---- -
-

Quote line 1 with ref

-

Quote line 2.

-

- Outside paragraph with - - https://outer.example/path - - . -

-
\ No newline at end of file diff --git a/cynthia_websites_mini_client/birdie_snapshots/djot_with_preprocessing_test.accepted b/cynthia_websites_mini_client/birdie_snapshots/djot_with_preprocessing_test.accepted deleted file mode 100644 index aa00b58..0000000 --- a/cynthia_websites_mini_client/birdie_snapshots/djot_with_preprocessing_test.accepted +++ /dev/null @@ -1,20 +0,0 @@ ---- -version: 1.3.0 -title: djot_with_preprocessing_test -file: ./test/cynthia_websites_mini_client_test.gleam -test_name: djot_with_preprocessing_test ---- -
-

- Hello World -

-

- This is a test paragraph. -

-

Task item

-

Completed task

-

This is a blockquote

-

- Another paragraph. -

-
\ No newline at end of file diff --git a/cynthia_websites_mini_client/birdie_snapshots/empty_input_renders_minimal_markup_test.new b/cynthia_websites_mini_client/birdie_snapshots/empty_input_renders_minimal_markup_test.new deleted file mode 100644 index f46b83d..0000000 --- a/cynthia_websites_mini_client/birdie_snapshots/empty_input_renders_minimal_markup_test.new +++ /dev/null @@ -1,5 +0,0 @@ ---- -version: 1.3.1 -title: empty_input_renders_minimal_markup_test ---- -
\ No newline at end of file diff --git a/cynthia_websites_mini_client/birdie_snapshots/inline_code_and_code_block_should_not_autolink_test.new b/cynthia_websites_mini_client/birdie_snapshots/inline_code_and_code_block_should_not_autolink_test.new deleted file mode 100644 index ba06947..0000000 --- a/cynthia_websites_mini_client/birdie_snapshots/inline_code_and_code_block_should_not_autolink_test.new +++ /dev/null @@ -1,27 +0,0 @@ ---- -version: 1.3.1 -title: inline_code_and_code_block_should_not_autolink_test ---- -
-

- Inline code like - - curl https://api.example.com - - should not autolink. -

-
-    
-      # A fenced code block containing an URL
-wget https://downloads.example.com/archive.tar.gz
-
-    
-  
-

- Regular text with - - https://linked.example - - after code. -

-
\ No newline at end of file diff --git a/cynthia_websites_mini_client/birdie_snapshots/invalid_markdown_like_sequences_should_not_panic_test.new b/cynthia_websites_mini_client/birdie_snapshots/invalid_markdown_like_sequences_should_not_panic_test.new deleted file mode 100644 index 8535e1d..0000000 --- a/cynthia_websites_mini_client/birdie_snapshots/invalid_markdown_like_sequences_should_not_panic_test.new +++ /dev/null @@ -1,18 +0,0 @@ ---- -version: 1.3.1 -title: invalid_markdown_like_sequences_should_not_panic_test ---- -
-

- Unclosed [link(https://bad.example -

-

- Mismatched **bold and _italic -

-

- - - https://ok.example - -

-
\ No newline at end of file diff --git a/cynthia_websites_mini_client/birdie_snapshots/links_in_preprocessed_items_test.accepted b/cynthia_websites_mini_client/birdie_snapshots/links_in_preprocessed_items_test.accepted deleted file mode 100644 index ccfa8e3..0000000 --- a/cynthia_websites_mini_client/birdie_snapshots/links_in_preprocessed_items_test.accepted +++ /dev/null @@ -1,11 +0,0 @@ ---- -version: 1.3.0 -title: links_in_preprocessed_items_test -file: ./test/cynthia_websites_mini_client_test.gleam -test_name: links_in_preprocessed_items_test ---- -
-

Task with link

-

Completed task with another link

-

Blockquote with a link

-
\ No newline at end of file diff --git a/cynthia_websites_mini_client/birdie_snapshots/links_with_parentheses_and_punctuation_test.new b/cynthia_websites_mini_client/birdie_snapshots/links_with_parentheses_and_punctuation_test.new deleted file mode 100644 index d22b4cd..0000000 --- a/cynthia_websites_mini_client/birdie_snapshots/links_with_parentheses_and_punctuation_test.new +++ /dev/null @@ -1,21 +0,0 @@ ---- -version: 1.3.1 -title: links_with_parentheses_and_punctuation_test ---- -
-

- A tricky - - link - - parens). Also - - https://example.com/trail - - , and a sentence ending link - - https://end.example - - . -

-
\ No newline at end of file diff --git a/cynthia_websites_mini_client/birdie_snapshots/multi_line_autolinks_across_paragraphs_test.new b/cynthia_websites_mini_client/birdie_snapshots/multi_line_autolinks_across_paragraphs_test.new deleted file mode 100644 index 11b9085..0000000 --- a/cynthia_websites_mini_client/birdie_snapshots/multi_line_autolinks_across_paragraphs_test.new +++ /dev/null @@ -1,23 +0,0 @@ ---- -version: 1.3.1 -title: multi_line_autolinks_across_paragraphs_test ---- -
-

- Paragraph one with - - https://one.example - -

-

- Paragraph two with - - https://two.example/path?x=1 - - and - - brackets - - . -

-
\ No newline at end of file diff --git a/cynthia_websites_mini_client/birdie_snapshots/nested_lists_with_links_and_formatting_test.new b/cynthia_websites_mini_client/birdie_snapshots/nested_lists_with_links_and_formatting_test.new deleted file mode 100644 index c56bcad..0000000 --- a/cynthia_websites_mini_client/birdie_snapshots/nested_lists_with_links_and_formatting_test.new +++ /dev/null @@ -1,32 +0,0 @@ ---- -version: 1.3.1 -title: nested_lists_with_links_and_formatting_test ---- -
- -
  1. Ordered child with another

  2. Second ordered child

-
\ No newline at end of file diff --git a/cynthia_websites_mini_client/birdie_snapshots/newline_normalization_and_trimming_variants_test.new b/cynthia_websites_mini_client/birdie_snapshots/newline_normalization_and_trimming_variants_test.new deleted file mode 100644 index ca71914..0000000 --- a/cynthia_websites_mini_client/birdie_snapshots/newline_normalization_and_trimming_variants_test.new +++ /dev/null @@ -1,24 +0,0 @@ ---- -version: 1.3.1 -title: newline_normalization_and_trimming_variants_test ---- -
-

- Title -

-

- Paragraph with Windows newlines. -

- -
\ No newline at end of file diff --git a/cynthia_websites_mini_client/birdie_snapshots/ootb_index_contains_expected_sections_test.new b/cynthia_websites_mini_client/birdie_snapshots/ootb_index_contains_expected_sections_test.new deleted file mode 100644 index 8758fa3..0000000 --- a/cynthia_websites_mini_client/birdie_snapshots/ootb_index_contains_expected_sections_test.new +++ /dev/null @@ -1,61 +0,0 @@ ---- -version: 1.3.1 -title: ootb_index_contains_expected_sections_test ---- - -

- Hello, World -

-
  1. Numbered lists

  2. Images: Gleam's Lucy    mascot

-

- The world is big -

-

- The world is a little smaller -

-

- The world is tiny -

-
- The world is tinier -
-
- The world is the tiniest -
-

Also quote blocks!

-StrawmelonJuice

-

- A task list: -

-

Task 1

-

Task 2

-

Task 3

-

- A bullet list: -

- -
-    
-      
- MYFILE.BASH -
- echo "Code blocks!" -// - StrawmelonJuice - -
-
-

- A small table: -

-

Column 1

Column 2

Value 1

Value 2

Github

Codeberg

https://github.com/CynthiaWebsiteEngine/Mini

https://github.com/strawmelonjuice/Mini-strawmelonjuice.com

- \ No newline at end of file diff --git a/cynthia_websites_mini_client/birdie_snapshots/ootb_index_rendering_test.accepted b/cynthia_websites_mini_client/birdie_snapshots/ootb_index_rendering_test.accepted deleted file mode 100644 index 228e3ba..0000000 --- a/cynthia_websites_mini_client/birdie_snapshots/ootb_index_rendering_test.accepted +++ /dev/null @@ -1,63 +0,0 @@ ---- -version: 1.3.0 -title: ootb_index_rendering_test -file: ./test/cynthia_websites_mini_client_test.gleam -test_name: ootb_index_rendering_test ---- - -

- Hello, World -

-
  1. Numbered lists

  2. Images: Gleam's Lucy    mascot

-

- The world is big -

-

- The world is a little smaller -

-

- The world is tiny -

-
- The world is tinier -
-
- The world is the tiniest -
-

Also quote blocks!

-StrawmelonJuice

-

- A task list: -

-

Task 1

-

Task 2

-

Task 3

-

- A bullet list: -

- -
-    
-      
- MYFILE.BASH -
- echo "Code blocks!" -// - StrawmelonJuice - -
-
-

- A small table: -

-

Column 1

Column 2

Value 1

Value 2

Github

Codeberg

https://github.com/CynthiaWebsiteEngine/Mini

https://github.com/strawmelonjuice/Mini-strawmelonjuice.com

- \ No newline at end of file diff --git a/cynthia_websites_mini_client/birdie_snapshots/ordered_list_with_links_test.accepted b/cynthia_websites_mini_client/birdie_snapshots/ordered_list_with_links_test.accepted deleted file mode 100644 index 72119bb..0000000 --- a/cynthia_websites_mini_client/birdie_snapshots/ordered_list_with_links_test.accepted +++ /dev/null @@ -1,9 +0,0 @@ ---- -version: 1.3.0 -title: ordered_list_with_links_test -file: ./test/cynthia_websites_mini_client_test.gleam -test_name: ordered_list_with_links_test ---- -
-
  1. First item with link

  2. Second item with another link

  3. Third item with *bold* and link

-
\ No newline at end of file diff --git a/cynthia_websites_mini_client/birdie_snapshots/ordered_list_with_mixed_content_and_strong_emphasis_test.new b/cynthia_websites_mini_client/birdie_snapshots/ordered_list_with_mixed_content_and_strong_emphasis_test.new deleted file mode 100644 index fd4f884..0000000 --- a/cynthia_websites_mini_client/birdie_snapshots/ordered_list_with_mixed_content_and_strong_emphasis_test.new +++ /dev/null @@ -1,7 +0,0 @@ ---- -version: 1.3.1 -title: ordered_list_with_mixed_content_and_strong_emphasis_test ---- -
-
  1. First item with *bold* and link

  2. Second with italic and https://second.example

  3. Third with inline code and another

-
\ No newline at end of file diff --git a/cynthia_websites_mini_client/birdie_snapshots/simple_djot_test.accepted b/cynthia_websites_mini_client/birdie_snapshots/simple_djot_test.accepted deleted file mode 100644 index 49728e0..0000000 --- a/cynthia_websites_mini_client/birdie_snapshots/simple_djot_test.accepted +++ /dev/null @@ -1,26 +0,0 @@ ---- -version: 1.3.0 -title: simple_djot_test -file: ./test/cynthia_websites_mini_client_test.gleam -test_name: simple_djot_test ---- -
-

- Hello World -

-

- This is a test paragraph. -

- -
\ No newline at end of file diff --git a/cynthia_websites_mini_client/birdie_snapshots/task_list_with_and_without_links_test.new b/cynthia_websites_mini_client/birdie_snapshots/task_list_with_and_without_links_test.new deleted file mode 100644 index bdab7d3..0000000 --- a/cynthia_websites_mini_client/birdie_snapshots/task_list_with_and_without_links_test.new +++ /dev/null @@ -1,10 +0,0 @@ ---- -version: 1.3.1 -title: task_list_with_and_without_links_test ---- -
-

Task todo

-

Task done

-

Another with link

-

Done with docs

-
\ 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..056a29f 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 = "54bbfb1ec0f40d17ffb11080de30090eee0345dc" } [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..fc20960 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 = "54bbfb1ec0f40d17ffb11080de30090eee0345dc" }, + { 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 = "54bbfb1ec0f40d17ffb11080de30090eee0345dc" } +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..c6e90df 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,397 @@ -// 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 chilp/widget/base as chilp_base +import cynthia_websites_mini_client/ui/themes_generated +import cynthia_websites_mini_shared/config/site_json +import cynthia_websites_mini_shared/config/v4_1 +import cynthia_websites_mini_shared/ffi import gleam/dict -import gleam/dynamic -import gleam/float -import gleam/int +import gleam/dynamic/decode +import gleam/fetch +import gleam/http/request +import gleam/http/response +import gleam/javascript/promise 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/component import lustre/effect.{type Effect} +import lustre/element.{type Element} +import lustre/element/html import modem -import odysseus +import plinth/browser/location 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) - - Nil +// MODEL +pub type Model { + Model( + data: site_json.SiteJSON, + route: Route, + chilp_model: chilp_base.ChilpDataInYourModel(Msg), + /// This used to be a Dict(Int, #(String, Path)), but + /// 1. We use routes now. + /// 2. We want a single item to be able to pop up in multiple menus! + menu_items: List(#(Int, #(String, Route))), + ) } -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) +pub type PostFilter { + ByCategory(String) + ByTag(String) + AnyFieldContains(String) + All } -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 +pub type Route { + Index + PostsList(PostFilter) + Content(slug: String) + NotFound(uri: Uri) } -/// 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 +pub 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 + } + } } - False -> { - let assert Ok(session) = storage.local() - as "Browser is expected to have a localstorage." + ["tagged", tag] -> PostsList(ByCategory(tag)) + ["category", cat] -> PostsList(ByTag(cat)) + ["post", slug] | ["page", slug] | ["content", slug] -> Content(slug:) - case window.get_hash() { - Ok(f) -> { - let h = case f { - "" -> { - "/" - } - d -> { - d - } + _ -> NotFound(uri:) + } +} + +pub fn stringify_route(route: Route, model: Model) { + case route { + Index -> "/" + Content(c) -> { + dict.get(model.data.content, c) + |> result.map(fn(content) { + case content { + site_json.Post(..) -> { + "/post/" <> c } - 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)) - } + site_json.Page(..) -> { + "/page/" <> c } } - _ -> { - // 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 - } - } + }) + |> result.unwrap("/content/" <> c) } + NotFound(_) -> "/404" + PostsList(ByCategory(cat)) -> "/category/" <> cat + PostsList(ByTag(tag)) -> "/tagged/" <> tag + PostsList(AnyFieldContains(q)) -> "/#!/search/" <> q + PostsList(All) -> "/#!/" } } -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 +pub fn href(route: Route, model: Model) -> Attribute(msg) { + stringify_route(route, model) + |> attribute.href() +} + +pub const version = ffi.version + +// MAIN ------------------------------------------------------------------------ + +pub fn main() { + let app = + lustre.application(init, update, fn(model) { + let #(title, elements) = view(model) + let assert Ok(_) = ffi.push_title(title) + elements + }) + 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 here is okay, we just don't activate and hope the server served well enough pregenerations. + _, _ -> Nil } - 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), - ) + + promise.resolve(Ok(Nil)) +} + +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 = chilp_base.init(Chilp) + let effect = + modem.init(fn(uri) { + uri + |> parse_route + |> UserNavigatedTo + }) + let menu_items = { + appdata.content + |> dict.values + |> list.shuffle + |> list.filter(keeping: fn(c) { + case c { + site_json.Page(in_menus:, ..) -> { + !{ in_menus |> list.is_empty } } + site_json.Post(..) -> False } - } + }) + |> list.map(fn(page) { + let assert site_json.Page(title:, in_menus:, ..) = page + list.map(in_menus, fn(menuid) { #(menuid, #(title, route)) }) + }) + |> list.flatten } - 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", "/") - "/" - } - 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 + let model = Model(appdata, route:, chilp_model:, menu_items:) + 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 { + Some(..) -> True + _ -> False + } + } + _ -> False + } + }) + |> list.map(fn(post) { + let assert site_json.Post(mastodon_comments: Some(status), ..) = post + let widget_ = + chilp_base.new( + instance: status.instance, + post_id: status.id, + chilp_model:, + ) + chilp_base.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) -} -// 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) -} - -/// 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) + #(model, effect) } // UPDATE ---------------------------------------------------------------------- +pub type Msg { + UserNavigatedTo(route: Route) + Chilp(chilp_base.ChilpMsg) + UserSearchTerm(String) +} 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)) + UserNavigatedTo(route:) -> { + let model = Model(..model, route:) + #(model, effect.none()) } - 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()) - } - } - } - } + Chilp(chilp_msg) -> { + let #(chilp_model, chilp_effects) = + chilp_base.update(chilp_msg, model.chilp_model, browse_to) + #(Model(..model, chilp_model:), chilp_effects) } - // 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()) + UserSearchTerm(term) -> { + let model = Model(..model, route: PostsList(AnyFieldContains(term))) + #(model, 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)) - } + } +} + +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) } - #(Model(..model, other:), 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()) + False -> { + ffi.browse_prompt(url) } } } -/// ----------------------------------------------------------------------------------------- -/// 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) - } +fn view(model: Model) -> #(String, Element(Msg)) { + case model.route { + Index -> view_content(model, "/") + PostsList(a) -> view_postlist(model, a) + Content(slug:) -> view_content(model, slug) + NotFound(uri:) -> view_notfound(model, uri) + } +} - "date", _values -> { - // TODO: Implement date parsing later (non-blocking for releases) - Error("Date decoding not yet implemented in " <> or_keyname) - } +fn view_notfound(model: Model, uri: Uri) -> #(String, Element(Msg)) { + #( + html.div([], [ + html.h1([], [element.text("This page could not be found")]), + html.p([], [ + element.text( + "The page at " <> uri |> uri.to_string() <> " could not be found.", + ), + ]), + ]), + site_json.Page( + title: "404: Page not found", + description: "Page could not be found", + layout: None, + content: "", + in_menus: [], + hide_meta_block: False, + ), + "/404", + ) + |> view_into_layout(model:) +} - "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)) +fn view_content(model: Model, slug: String) { + case dict.get(model.data.content, slug) { + Error(_) -> + view_notfound( + model, + rsvp.parse_relative_uri(stringify_route(Content(slug), model)) + |> result.unwrap(uri.empty), + ) + Ok(_) -> todo + } } -/// 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) - } - }) - |> list.flatten() - |> list.unique() - |> list.sort(int.compare) - add_each_menu(menu_s_available, model.computed_menus, content) +fn view_postlist(model: Model, filter: PostFilter) { + todo } -// 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)) +fn view_into_layout( + in: #(Element(Msg), site_json.Content, String), + model model: Model, +) -> #(String, Element(Msg)) { + let item = in.1 + let slug = in.2 + let in = in.0 + let global_theme = case ffi.get_color_scheme() { + True -> model.data.config.global.theme + False -> model.data.config.global.theme_dark + } + let is_post = case item { + site_json.Post(..) -> True + _ -> False + } + let item_theme = + item.layout + |> option.unwrap(global_theme) + let assert Ok(theme) = + themes_generated.themes + |> list.find(fn(i) { i.name == item_theme }) + as "Unknown theme set." + + let github_comment_color_scheme = case theme.prevalence { + themes_generated.ThemeDark -> "github-dark" + themes_generated.ThemeLight -> "github-light" + } + + // layout_cindy-simple for example, which can then be used from element.element + let component_name = "layout_" <> theme.layout + let current = model.route + let href = href(_, model) + + #( + item.title, + element.element( + // This is where the layout is actually used. + component_name, + [ + attribute.attribute("title", item.title), + attribute.attribute("description", item.description), + ], + [ + html.div([component.slot("menu1")], [ + model.menu_items + |> list.key_filter(1) + |> list.map(fn(item) { + html.a( + [ + attribute.class({ + case current == item.1 { + True -> "menu-active menu-focused active font-medium" + False -> + "hover:bg-base-300/50 transition-colors duration-200" + } + }), + href(item.1), + ], + [html.text(item.0)], + ) + }) + |> html.li([], _), + ]), + element.fragment([ + in, + case is_post, model.data.config.posts.comments { + False, _ | _, v4_1.CommentsDisabled -> element.none() + True, v4_1.CommentsMastodonStored -> { + let assert site_json.Post(mastodon_comments:, ..) = item + case mastodon_comments { + None -> { + element.none() + } + Some(mastodonstatus) -> { + chilp_base.new( + mastodonstatus.instance, + mastodonstatus.id, + model.chilp_model, + ) + |> chilp_base.show(model.chilp_model) } - 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) - } - } + True, v4_1.CommentsGithubStored(username:, repositoryname:) -> { + html.script( + [ + attribute.attribute("async", ""), + attribute.attribute("crossorigin", "anonymous"), + attribute.attribute("theme", github_comment_color_scheme), + attribute.attribute("issue-term", slug), + attribute.attribute("repo", username <> "/" <> repositoryname), + attribute.src("https://utteranc.es/client.js"), + ], + "", + ) + } + }, + ]), + ], + ), + ) } - -@external(javascript, "./cynthia_websites_mini_client/version_ffi.ts", "my_own_version") -pub fn version() -> String 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: ![Gleam\\'s Lucy - mascot](https://gleam.run/images/lucy/lucy.svg) - -{#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 not converted/cindy_dual.gleam similarity index 92% 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 not converted/cindy_dual.gleam index 5460659..2ef7637 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 not converted/cindy_dual.gleam @@ -3,12 +3,7 @@ //// Extension of the default Cindy Simple layout with a secondary menu. // Common imports for layouts -import cynthia_websites_mini_client/messages -import cynthia_websites_mini_client/model_type -import cynthia_websites_mini_client/utils -import gleam/dict.{type Dict} -import gleam/dynamic -import gleam/dynamic/decode.{type Dynamic} +import cynthia_websites_mini_client/model_messages import gleam/list import gleam/option import gleam/result @@ -24,18 +19,12 @@ import lustre/event /// - `content` pub fn page_layout( from content: Element(messages.Msg), - with variables: Dict(String, Dynamic), - store model: model_type.Model, + content item: site_json.Content, + store model: model_messages.Model, ) -> Element(messages.Msg) { let menu = menu_1(model) let secondary_menu = menu_2(model) - - let assert Ok(title) = - decode.run( - result.unwrap(dict.get(variables, "title"), dynamic.from(option.None)), - decode.string, - ) - as "Could not determine title" + let tittle = item. let assert Ok(description) = decode.run( result.unwrap( @@ -56,8 +45,8 @@ pub fn page_layout( pub fn post_layout( from content: Element(messages.Msg), - with variables: Dict(String, Dynamic), - store model: model_type.Model, + content item: site_json.Content, + store model: model_messages.Model, ) -> Element(messages.Msg) { let menu = menu_1(model) let secondary_menu = menu_2(model) @@ -169,7 +158,7 @@ fn cindy_common( post_meta: Element(messages.Msg), secondary_menu: List(Element(messages.Msg)), variables: Dict(String, Dynamic), - model: model_type.Model, + model: model_messages.Model, ) { let assert Ok(site_name) = { dict.get(variables, "global_site_name") @@ -370,7 +359,7 @@ fn cindy_common( } /// Primary menu for cindy-dual -pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { +pub fn menu_1(from model: model_messages.Model) -> List(Element(messages.Msg)) { let hash = model.path let content = model.computed_menus case dict.get(content, 1) { @@ -378,8 +367,8 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { Ok(menu_items) -> { list.map(menu_items, fn(this_item) { let current_item = case this_item { - model_type.MenuItem(name:, to: "") -> - model_type.MenuItem(name:, to: "/") + model_messages.MenuItem(name:, to: "") -> + model_messages.MenuItem(name:, to: "/") _ -> this_item } html.li([], [ @@ -391,7 +380,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)], ), @@ -402,16 +391,16 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { } /// Secondary menu for cindy-dual -pub fn menu_2(from model: model_type.Model) -> List(Element(messages.Msg)) { - let hash = model.path +pub fn menu_2(from model: model_messages.Model) -> List(Element(messages.Msg)) { + let hash = model.route let content = model.computed_menus case dict.get(content, 2) { Error(_) -> [] Ok(menu_items) -> { list.map(menu_items, fn(a) { let a = case a { - model_type.MenuItem(name:, to: "") -> - model_type.MenuItem(name:, to: "/") + model_messages.MenuItem(name:, to: "") -> + model_messages.MenuItem(name:, to: "/") _ -> a } html.li([], [ @@ -423,7 +412,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 not converted/cindy_landing.gleam similarity index 97% 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 not converted/cindy_landing.gleam index 5e71799..d1356d0 100644 --- 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 not converted/cindy_landing.gleam @@ -5,7 +5,7 @@ // Common imports for layouts import cynthia_websites_mini_client/messages -import cynthia_websites_mini_client/model_type +import cynthia_websites_mini_client/model_messages import gleam/dict.{type Dict} import gleam/dynamic import gleam/dynamic/decode.{type Dynamic} @@ -27,8 +27,8 @@ import cynthia_websites_mini_client/pottery/molds/cindy_simple.{menu_1} /// - `content` pub fn page_layout( from content: Element(messages.Msg), - with variables: Dict(String, Dynamic), - store model: model_type.Model, + content item: site_json.Content, + store model: model_messages.Model, ) -> Element(messages.Msg) { let menu = menu_1(model) let assert Ok(title) = @@ -82,7 +82,7 @@ pub fn page_layout( |> landing_common(content, menu, _, variables, model) } -/// Special common layout for landing pages: +/// Special common layout for landing pages: /// - More focused design /// - Content centered and emphasized /// - Full width content area @@ -92,7 +92,7 @@ fn landing_common( menu: List(Element(messages.Msg)), post_meta: Element(messages.Msg), variables: Dict(String, Dynamic), - model: model_type.Model, + model: model_messages.Model, ) { let assert Ok(site_name) = { dict.get(variables, "global_site_name") 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 not converted/cindy_simple.gleam similarity index 69% 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 not converted/cindy_simple.gleam index 9218f86..10be48f 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 not converted/cindy_simple.gleam @@ -4,14 +4,10 @@ //// Focused on simplicity while offering a clean, modern experience. // Common imports for layouts -import cynthia_websites_mini_client/messages -import cynthia_websites_mini_client/model_type -import cynthia_websites_mini_client/utils -import gleam/dict.{type Dict} -import gleam/dynamic -import gleam/dynamic/decode.{type Dynamic} +import cynthia_websites_mini_client/model_messages +import cynthia_websites_mini_shared/config/site_json +import gleam/dict import gleam/list -import gleam/option import gleam/result import gleam/string import lustre/attribute @@ -24,25 +20,12 @@ import lustre/event /// Dict keys: /// - `content` pub fn page_layout( - from content: Element(messages.Msg), - with variables: Dict(String, Dynamic), - store model: model_type.Model, -) -> Element(messages.Msg) { - let menu = menu_1(model) - let assert Ok(title) = - decode.run( - result.unwrap(dict.get(variables, "title"), dynamic.from(option.None)), - decode.string, - ) - as "Could not determine title" - let assert Ok(description) = - decode.run( - result.unwrap( - dict.get(variables, "description_html"), - dynamic.from(option.None), - ), - decode.string, - ) + from content: Element(model_messages.Msg), + content item: site_json.Content, + store model: model_messages.Model, +) -> Element(model_messages.Msg) { + let title = item.title + let description = item.description html.div([attribute.class("break-words")], [ html.h3( @@ -60,28 +43,27 @@ pub fn page_layout( description, ), ]) - |> cindy_common(content, menu, _, variables, model) + |> cindy_common(item, model) } pub fn post_layout( - from content: Element(messages.Msg), - with variables: Dict(String, Dynamic), - store model: model_type.Model, -) -> Element(messages.Msg) { + from content: Element(model_messages.Msg), + content item: site_json.Content, + store model: model_messages.Model, +) -> Element(model_messages.Msg) { let menu = menu_1(model) - let assert Ok(title) = - decode.run( - result.unwrap(dict.get(variables, "title"), dynamic.from(option.None)), - decode.string, - ) - let assert Ok(description) = - decode.run( - result.unwrap( - dict.get(variables, "description_html"), - dynamic.from(option.None), - ), - decode.string, - ) + + let assert site_json.Post( + title:, + description:, + layout:, + content:, + date_published:, + date_updated:, + category:, + tags:, + mastodon_comments:, + ) = item html.div([], [ html.div([], [ html.h3( @@ -108,36 +90,14 @@ pub fn post_layout( html.text("Published"), ]), html.div([attribute.class("text-base-content/90")], [ - html.text( - { - decode.run( - result.unwrap( - dict.get(variables, "date_published"), - dynamic.from("unknown"), - ), - decode.string, - ) - } - |> result.unwrap("unknown"), - ), + html.text(date_published), ]), // ---------------------- html.b([attribute.class("font-bold text-base-content/80")], [ html.text("Modified"), ]), html.div([attribute.class("text-base-content/90")], [ - html.text( - { - decode.run( - result.unwrap( - dict.get(variables, "date_modified"), - dynamic.from("unknown"), - ), - decode.string, - ) - } - |> result.unwrap("unknown"), - ), + html.text(date_updated), ]), // ---------------------- html.div([], [ @@ -146,16 +106,7 @@ pub fn post_layout( ]), ]), html.div([attribute.class("text-base-content/90")], [ - html.text( - decode.run( - result.unwrap( - dict.get(variables, "category"), - dynamic.from("unknown"), - ), - decode.string, - ) - |> result.unwrap("unknown"), - ), + html.text(category), ]), ]), html.div([attribute.class("grid grid-cols-1 grid-rows-1 gap-2 mt-3")], [ @@ -165,11 +116,7 @@ pub fn post_layout( ]), html.div( [attribute.class("flex flex-wrap gap-2 mt-2")], - variables - |> dict.get("tags") - |> result.unwrap(dynamic.from([])) - |> decode.run(decode.list(decode.string)) - |> result.unwrap([]) + tags |> list.map(string.trim) |> list.map(fn(tag) { html.a( @@ -186,30 +133,20 @@ pub fn post_layout( ]), ]), ]) - |> cindy_common(content, menu, _, variables, model) + |> cindy_common(around: content, for: item, model:) } fn cindy_common( - content: Element(messages.Msg), - menu: List(Element(messages.Msg)), - post_meta: Element(messages.Msg), - variables: Dict(String, Dynamic), - model: model_type.Model, -) { - let assert Ok(site_name) = { - dict.get(variables, "global_site_name") - |> result.unwrap(dynamic.from(option.None)) - |> decode.run(decode.string) + from post_meta: Element(model_messages.Msg), + around content: String, + for item: site_json.Content, + model model: model_messages.Model, +) -> Element(model_messages.Msg) { + let site_name = model.data.config.global.site_name + let hide_metadata_block = case item { + site_json.Page(hide_meta_block:, ..) -> hide_meta_block + site_json.Post(..) -> False } - 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 -> "" @@ -254,7 +191,9 @@ fn cindy_common( "md:hidden btn btn-ghost btn-sm fa fa-bars", ), attribute.id("cindy_menu_toggle"), - event.on_click(messages.CindyToggleMenu1), + event.on_click(model_messages.CindyMsg( + model_messages.ToggleMenu1, + )), ], [html.span([attribute.class("i-tabler-menu h-5 w-5")], [])], ), @@ -304,7 +243,7 @@ fn cindy_common( ), attribute.placeholder("Search..."), attribute.type_("text"), - event.on_input(messages.UserSearchTerm), + event.on_input(model_messages.UserSearchTerm), ]), ], ), @@ -327,7 +266,7 @@ fn cindy_common( "menu menu-horizontal flex-col md:flex-row bg-base-200 md:bg-base-200/90 rounded-box shadow-sm w-full md:w-auto divide-y md:divide-y-0 divide-base-300/40", ), ], - menu, + menu_1(model), ), ]), ], @@ -368,33 +307,8 @@ fn cindy_common( } /// Cindy Simple only has one menu, shown on the top of the page. But we still count it as menu 1. -pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { - let hash = model.path - let content = model.computed_menus - case dict.get(content, 1) { - Error(_) -> [] - Ok(menu_items) -> { - list.map(menu_items, fn(a) { - let a = case a { - model_type.MenuItem(name:, to: "") -> - model_type.MenuItem(name:, to: "/") - _ -> a - } - html.li([], [ - html.a( - [ - attribute.class({ - case hash == a.to { - True -> "menu-active menu-focused active font-medium" - False -> "hover:bg-base-300/50 transition-colors duration-200" - } - }), - attribute.href(utils.phone_home_url() <> "#" <> a.to), - ], - [html.text(a.name)], - ), - ]) - }) - } - } +pub fn menu_1( + from model: model_messages.Model, +) -> List(Element(model_messages.Msg)) { + todo } 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 not converted/documentation.gleam similarity index 95% 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 not converted/documentation.gleam index 32c19d5..261afe0 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 not converted/documentation.gleam @@ -8,7 +8,7 @@ //// - Multiple theme options (light, dark, sepia, etc.) import cynthia_websites_mini_client/messages -import cynthia_websites_mini_client/model_type +import cynthia_websites_mini_client/model_messages import cynthia_websites_mini_client/utils import gleam/dict.{type Dict} import gleam/dynamic @@ -34,8 +34,8 @@ import odysseus /// @return A fully constructed page layout pub fn page_layout( from content: Element(messages.Msg), - with variables: Dict(String, Dynamic), - store model: model_type.Model, + content item: site_json.Content, + store model: model_messages.Model, ) -> Element(messages.Msg) { let menu = menu_1(model) @@ -84,8 +84,8 @@ pub fn page_layout( /// @return A fully constructed post layout pub fn post_layout( from content: Element(messages.Msg), - with variables: Dict(String, Dynamic), - store model: model_type.Model, + content item: site_json.Content, + store model: model_messages.Model, ) -> Element(messages.Msg) { let menu = menu_1(model) let assert Ok(description) = @@ -213,7 +213,7 @@ fn documentation_common( menu: List(Element(messages.Msg)), sidebar_content: Element(messages.Msg), variables: Dict(String, Dynamic), - model: model_type.Model, + model: model_messages.Model, ) -> Element(messages.Msg) { let assert Ok(site_name) = dict.get(variables, "global_site_name") @@ -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( @@ -594,7 +590,7 @@ fn documentation_common( /// /// @param model Client model containing menus and current path /// @return List of HTML elements representing menu items -pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { +pub fn menu_1(from model: model_messages.Model) -> List(Element(messages.Msg)) { let hash = model.path let content = model.computed_menus @@ -604,10 +600,10 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { list.map(menu_items, fn(item) { // Convert item to tuple, this is not the best approach, but it works as well as refactoring for custom type here. let item = case item.to { - "" -> model_type.MenuItem(item.name, "/") + "" -> model_messages.MenuItem(item.name, "/") _ -> item } - let model_type.MenuItem(name:, to:) = item + let model_messages.MenuItem(name:, to:) = item let is_active = hash == to html.li([], [ @@ -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), ], [ @@ -649,17 +645,17 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { /// Find the previous and next menu items relative to the current path fn find_prev_next_links( - menu_items: List(model_type.MenuItem), + menu_items: List(model_messages.MenuItem), current_path: String, -) -> #(Option(model_type.MenuItem), Option(model_type.MenuItem)) { +) -> #(Option(model_messages.MenuItem), Option(model_messages.MenuItem)) { find_prev_next_links_looped(menu_items, current_path, None) } fn find_prev_next_links_looped( - left_menu_items: List(model_type.MenuItem), + left_menu_items: List(model_messages.MenuItem), current_path: String, - last_item: Option(model_type.MenuItem), -) -> #(Option(model_type.MenuItem), Option(model_type.MenuItem)) { + last_item: Option(model_messages.MenuItem), +) -> #(Option(model_messages.MenuItem), Option(model_messages.MenuItem)) { case left_menu_items, last_item { // End of list, nothing found [], None -> #(None, None) 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 not converted/frutiger.gleam similarity index 97% 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 not converted/frutiger.gleam index 0fa59f8..f91bcc2 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 not converted/frutiger.gleam @@ -8,7 +8,7 @@ //// - Modern, attention-grabbing design import cynthia_websites_mini_client/messages -import cynthia_websites_mini_client/model_type +import cynthia_websites_mini_client/model_messages import cynthia_websites_mini_client/utils import gleam/dict.{type Dict} import gleam/dynamic @@ -28,8 +28,8 @@ import lustre/event /// striking visual elements and glass-like effects. pub fn page_layout( from content: Element(messages.Msg), - with variables: Dict(String, Dynamic), - store model: model_type.Model, + content item: site_json.Content, + store model: model_messages.Model, ) -> Element(messages.Msg) { let menu = menu_1(model) @@ -78,8 +78,8 @@ pub fn page_layout( /// eye-catching metadata display and glossy effects. pub fn post_layout( from content: Element(messages.Msg), - with variables: Dict(String, Dynamic), - store model: model_type.Model, + content item: site_json.Content, + store model: model_messages.Model, ) -> Element(messages.Msg) { let menu = menu_1(model) @@ -199,7 +199,7 @@ pub fn post_layout( /// Primary navigation menu generator /// /// Creates the main site navigation with glossy, attention-grabbing styling. -pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { +pub fn menu_1(from model: model_messages.Model) -> List(Element(messages.Msg)) { let hash = model.path let content = model.computed_menus @@ -209,7 +209,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { list.map(menu_items, fn(item) { // Convert item to tuple, this is not the best approach, but it works as well as refactoring for custom type here. let item = { - let model_type.MenuItem(name:, to:) = item + let model_messages.MenuItem(name:, to:) = item #(name, to) } let item = case item.1 { @@ -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 not converted/github_layout.gleam similarity index 97% 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 not converted/github_layout.gleam index 3e90ec9..860ed20 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 not converted/github_layout.gleam @@ -9,7 +9,7 @@ //// - Responsive design that works well on all devices import cynthia_websites_mini_client/messages -import cynthia_websites_mini_client/model_type +import cynthia_websites_mini_client/model_messages import cynthia_websites_mini_client/utils import gleam/dict.{type Dict} import gleam/dynamic @@ -36,8 +36,8 @@ import cynthia_websites_mini_client/dom /// @return A fully constructed page layout pub fn page_layout( from content: Element(messages.Msg), - with variables: Dict(String, Dynamic), - store model: model_type.Model, + content item: site_json.Content, + store model: model_messages.Model, ) -> Element(messages.Msg) { // Load the primary navigation menu let menu = menu_1(model) @@ -92,8 +92,8 @@ pub fn page_layout( /// @return A fully constructed post layout pub fn post_layout( from content: Element(messages.Msg), - with variables: Dict(String, Dynamic), - store model: model_type.Model, + content item: site_json.Content, + store model: model_messages.Model, ) -> Element(messages.Msg) { // Load the primary navigation menu let menu = menu_1(model) @@ -323,7 +323,7 @@ fn github_common( menu: List(Element(messages.Msg)), sidebar: Element(messages.Msg), variables: Dict(String, Dynamic), - model: model_type.Model, + model: model_messages.Model, ) -> Element(messages.Msg) { let menu_is_open = result.is_ok(dict.get(model.other, "github-layout menu open")) @@ -526,7 +526,7 @@ fn github_common( list.map(mobile_menu_items, fn(item) { // Convert item to tuple, this is not the best approach, but it works as well as refactoring for custom type here. let item = { - let model_type.MenuItem(name:, to:) = item + let model_messages.MenuItem(name:, to:) = item #(name, to) } // Handle empty URLs as links to homepage @@ -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, ), @@ -722,7 +720,7 @@ fn github_common( /// /// @param model Client model containing menus and current path /// @return List of HTML elements representing menu items -pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { +pub fn menu_1(from model: model_messages.Model) -> List(Element(messages.Msg)) { // Get the current URL hash to identify the active page let hash = model.path let content = model.computed_menus @@ -735,7 +733,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { list.map(menu_items, fn(menu_item) { // Convert item to tuple, this is not the best approach, but it works as well as refactoring for custom type here. let item = { - let model_type.MenuItem(name:, to:) = menu_item + let model_messages.MenuItem(name:, to:) = menu_item #(name, to) } // Handle empty URLs as links to homepage @@ -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 not converted/minimalist.gleam similarity index 96% 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 not converted/minimalist.gleam index 3f04bc2..8bbb854 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 not converted/minimalist.gleam @@ -8,7 +8,7 @@ // Common imports for layouts import cynthia_websites_mini_client/messages -import cynthia_websites_mini_client/model_type +import cynthia_websites_mini_client/model_messages import cynthia_websites_mini_client/utils import gleam/dict.{type Dict} import gleam/dynamic @@ -28,8 +28,8 @@ import lustre/event /// emphasis on readability and minimal distractions. pub fn page_layout( from content: Element(messages.Msg), - with variables: Dict(String, Dynamic), - store model: model_type.Model, + content item: site_json.Content, + store model: model_messages.Model, ) -> Element(messages.Msg) { let menu = menu_1(model) @@ -75,8 +75,8 @@ pub fn page_layout( /// and subtle metadata display. pub fn post_layout( from content: Element(messages.Msg), - with variables: Dict(String, Dynamic), - store model: model_type.Model, + content item: site_json.Content, + store model: model_messages.Model, ) -> Element(messages.Msg) { let menu = menu_1(model) @@ -317,7 +317,7 @@ fn minimalist_common( } /// Generate primary menu items -pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { +pub fn menu_1(from model: model_messages.Model) -> List(Element(messages.Msg)) { let hash = model.path let content = model.computed_menus @@ -327,7 +327,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { list.map(menu_items, fn(item) { // Convert item to tuple, this is not the best approach, but it works as well as refactoring for custom type here. let item = { - let model_type.MenuItem(name:, to:) = item + let model_messages.MenuItem(name:, to:) = item #(name, to) } let item = case item.1 { @@ -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 not converted/oceanic_layout.gleam similarity index 97% 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 not converted/oceanic_layout.gleam index 9f7162a..84a6cdd 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 not converted/oceanic_layout.gleam @@ -14,7 +14,7 @@ //// good fit for the oceanic theme. import cynthia_websites_mini_client/messages -import cynthia_websites_mini_client/model_type +import cynthia_websites_mini_client/model_messages import cynthia_websites_mini_client/utils import gleam/dict.{type Dict} import gleam/dynamic @@ -44,8 +44,8 @@ import lustre/event /// @return A fully constructed page layout pub fn page_layout( from content: Element(messages.Msg), - with variables: Dict(String, Dynamic), - store model: model_type.Model, + content item: site_json.Content, + store model: model_messages.Model, ) -> Element(messages.Msg) { // Load the primary navigation menu if not in priority mode let menu = menu_1(model) @@ -129,8 +129,8 @@ pub fn page_layout( /// @return A fully constructed post layout pub fn post_layout( from content: Element(messages.Msg), - with variables: Dict(String, Dynamic), - store model: model_type.Model, + content item: site_json.Content, + store model: model_messages.Model, ) -> Element(messages.Msg) { // Load the primary navigation menu if not in priority mode let menu = menu_1(model) @@ -491,7 +491,7 @@ fn oceanic_common( /// /// @param content Dictionary mapping menu levels to lists of menu items /// @return List of HTML elements representing menu items -pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { +pub fn menu_1(from model: model_messages.Model) -> List(Element(messages.Msg)) { // Get the current URL hash to identify the active page let hash = model.path let content = model.computed_menus @@ -504,7 +504,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { list.map(menu_items, fn(item) { // Convert item to tuple, this is not the best approach, but it works as well as refactoring for custom type here. let item = { - let model_type.MenuItem(name:, to:) = item + let model_messages.MenuItem(name:, to:) = item #(name, to) } // Handle empty URLs as links to homepage @@ -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)], ), @@ -539,7 +539,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { /// /// @param content Dictionary mapping menu levels to lists of menu items /// @return List of HTML elements representing secondary menu items -pub fn menu_2(from model: model_type.Model) -> List(Element(messages.Msg)) { +pub fn menu_2(from model: model_messages.Model) -> List(Element(messages.Msg)) { // Get the current URL hash to identify the active page let hash = model.path let content = model.computed_menus @@ -551,7 +551,7 @@ pub fn menu_2(from model: model_type.Model) -> List(Element(messages.Msg)) { list.map(menu_items, fn(item) { // Convert item to tuple, this is not the best approach, but it works as well as refactoring for custom type here. let item = { - let model_type.MenuItem(name:, to:) = item + let model_messages.MenuItem(name:, to:) = item #(name, to) } // Handle empty URLs as links to homepage @@ -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 not converted/pastels.gleam similarity index 96% 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 not converted/pastels.gleam index 80a1521..e385d7c 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 not converted/pastels.gleam @@ -9,7 +9,7 @@ //// - Clean typography optimized for readability import cynthia_websites_mini_client/messages -import cynthia_websites_mini_client/model_type +import cynthia_websites_mini_client/model_messages import cynthia_websites_mini_client/utils import gleam/dict.{type Dict} import gleam/dynamic @@ -29,8 +29,8 @@ import lustre/event /// gentle visual elements and smooth transitions. pub fn page_layout( from content: Element(messages.Msg), - with variables: Dict(String, Dynamic), - store model: model_type.Model, + content item: site_json.Content, + store model: model_messages.Model, ) -> Element(messages.Msg) { let menu = menu_1(model) @@ -75,8 +75,8 @@ pub fn page_layout( /// subtle metadata display and smooth transitions. pub fn post_layout( from content: Element(messages.Msg), - with variables: Dict(String, Dynamic), - store model: model_type.Model, + content item: site_json.Content, + store model: model_messages.Model, ) -> Element(messages.Msg) { let menu = menu_1(model) @@ -196,7 +196,7 @@ pub fn post_layout( /// Primary navigation menu generator /// /// Creates the main site navigation with soft, pastel-appropriate styling. -pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { +pub fn menu_1(from model: model_messages.Model) -> List(Element(messages.Msg)) { let hash = model.path let content = model.computed_menus @@ -206,7 +206,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { list.map(menu_items, fn(item) { // Convert item to tuple, this is not the best approach, but it works as well as refactoring for custom type here. let item = { - let model_type.MenuItem(name:, to:) = item + let model_messages.MenuItem(name:, to:) = item #(name, to) } let item = case item.1 { @@ -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 not converted/sepia.gleam similarity index 96% 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 not converted/sepia.gleam index 0dfeb9c..b3ddd46 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 not converted/sepia.gleam @@ -9,7 +9,7 @@ //// - Focus on readability and classic aesthetics import cynthia_websites_mini_client/messages -import cynthia_websites_mini_client/model_type +import cynthia_websites_mini_client/model_messages import cynthia_websites_mini_client/utils import gleam/dict.{type Dict} import gleam/dynamic @@ -29,8 +29,8 @@ import lustre/event /// traditional typography and warm visual elements. pub fn page_layout( from content: Element(messages.Msg), - with variables: Dict(String, Dynamic), - store model: model_type.Model, + content item: site_json.Content, + store model: model_messages.Model, ) -> Element(messages.Msg) { let menu = menu_1(model) @@ -79,8 +79,8 @@ pub fn page_layout( /// elegant metadata presentation and classic typography. pub fn post_layout( from content: Element(messages.Msg), - with variables: Dict(String, Dynamic), - store model: model_type.Model, + content item: site_json.Content, + store model: model_messages.Model, ) -> Element(messages.Msg) { let menu = menu_1(model) @@ -221,7 +221,7 @@ pub fn post_layout( /// Primary navigation menu generator /// /// Creates the main site navigation with classic, book-inspired styling. -pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { +pub fn menu_1(from model: model_messages.Model) -> List(Element(messages.Msg)) { let hash = model.path let content = model.computed_menus @@ -230,8 +230,8 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { Ok(menu_items) -> { list.map(menu_items, fn(item) { let item = case item { - model_type.MenuItem(name:, to: "") -> - model_type.MenuItem(name:, to: "/") + model_messages.MenuItem(name:, to: "") -> + model_messages.MenuItem(name:, to: "/") _ -> item } @@ -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/layouts/cindy_simple.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/cindy_simple.gleam new file mode 100644 index 0000000..f8bcbcf --- /dev/null +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/cindy_simple.gleam @@ -0,0 +1,111 @@ +//// Cindy Simple Layout module +//// +//// Default OOTB layout for Cynthia Mini. +//// Focused on simplicity while offering a clean, modern experience. + +import gleam/bool +import lustre +import lustre/component +import lustre/effect +import lustre/element.{type Element} + +fn links_attribute_to_model(name: String) { + component.on_attribute_change(name, fn(value) { + UpdateModelValueAttribute(name, value) |> Ok + }) +} + +pub fn register() -> Result(Nil, lustre.Error) { + let component = + lustre.component(init, update, view, [ + links_attribute_to_model("title"), + links_attribute_to_model("description"), + links_attribute_to_model("content-type"), + links_attribute_to_model("date-published"), + links_attribute_to_model("date-updated"), + ]) + lustre.register(component, "layout_cindy-simple") +} + +// Layout models are always as complete possible, including may-be-impossible fields. +// Everything is just filled up with default values on init() and so, if any value +// remains unset, it'll still be possible to read or not read. +type Model { + Model(menu_1_out: Bool, content_item: ContentItem) +} + +type ContentItem { + ContentItem( + title: String, + description: String, + content_type: ContentType, + date_published: String, + date_updated: String, + ) +} + +type ContentType { + Post + Page +} + +fn init(_: a) -> #(Model, effect.Effect(Msg)) { + #( + Model( + menu_1_out: False, + content_item: ContentItem( + title: "", + description: "", + content_type: Page, + date_published: "", + date_updated: "", + ), + ), + effect.none(), + ) +} + +type Msg { + ToggleMenu1 + UpdateModelValueAttribute(String, String) +} + +fn update(model: Model, msg: Msg) -> #(Model, effect.Effect(Msg)) { + #( + case msg { + UpdateModelValueAttribute("title", value) -> + Model( + ..model, + content_item: ContentItem(..model.content_item, title: value), + ) + UpdateModelValueAttribute("description", value) -> + Model( + ..model, + content_item: ContentItem(..model.content_item, description: value), + ) + UpdateModelValueAttribute("content-type", value) -> + Model( + ..model, + content_item: ContentItem( + ..model.content_item, + content_type: case value { + "post" -> Post + _ -> Page + }, + ), + ) + UpdateModelValueAttribute(a, _) -> { + let b = "Unhandled UpdateModelValue call for field: " <> a + panic as b + } + ToggleMenu1 -> Model(..model, menu_1_out: bool.negate(model.menu_1_out)) + }, + // Cindy Simple does not have side effects from update in any case. + effect.none(), + ) +} + +fn view(model: Model) -> Element(Msg) { + component.named_slot("menu1", [], [element.text("No menu items.")]) + component.default_slot([], []) +} 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 94% 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..fe610d6 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 @@ -1,6 +1,6 @@ import cynthia_websites_mini_client/contenttypes.{PostData} import cynthia_websites_mini_client/messages -import cynthia_websites_mini_client/model_type.{type Model} +import cynthia_websites_mini_client/model_messages.{type Model} import cynthia_websites_mini_client/pottery import cynthia_websites_mini_client/utils import gleam/bool @@ -30,7 +30,7 @@ fn fetch_post_list(model: Model) { pub fn postlist_all(model: Model) { fetch_post_list(model) - |> postlist_to_html + |> postlist_to_lustre } pub fn postlist_by_tag(model: Model, card: String) { @@ -44,7 +44,7 @@ pub fn postlist_by_tag(model: Model, card: String) { ): contenttypes.ContentData = post.data tags |> list.contains(card) }) - |> postlist_to_html + |> postlist_to_lustre } pub fn postlist_by_category(model: Model, cat: String) { @@ -58,7 +58,7 @@ pub fn postlist_by_category(model: Model, cat: String) { ) = post.data category == cat }) - |> postlist_to_html + |> postlist_to_lustre } pub fn postlist_by_search_term(model: Model, search_term: String) { @@ -87,10 +87,10 @@ pub fn postlist_by_search_term(model: Model, search_term: String) { title_contains || description_contains || content_contains }) - |> postlist_to_html + |> postlist_to_lustre } -fn postlist_to_html( +fn postlist_to_lustre( posts: List(contenttypes.Content), ) -> element.Element(messages.Msg) { let ordered_posts = @@ -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 deleted file mode 100644 index 6a17b75..0000000 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery.gleam +++ /dev/null @@ -1,162 +0,0 @@ -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 - -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)), - ), - ] - }, - ) -} - -pub fn parse_html(inner: String, filename: String) -> Element(messages.Msg) { - case filename |> string.split(".") |> list.last { - // Djot is rendered with a custom renderer. After that, it will be direct lustre elements, so no need to wrap it in a unsafe raw html element. - Ok("dj") | Ok("djot") -> html.div([], djotparse.entry_to_conversion(inner)) - // HTML/SVG is directly pastable into the template. - Ok("html") | Ok("htm") | Ok("svg") -> - element.unsafe_raw_html("div", "div", [], inner) - // Text is wrapped in a
 tag. Then it can be pasted into the template.
-    //
-    Ok("txt") -> html.pre([], [html.text(inner)])
-    // Anything else is wrapped in a 
 tag with a red color. Then it can be pasted into the template. This shows that the file type is not supported.
-    _ ->
-      html.div([], [
-        html.text("Unsupported file type: "),
-        html.text(filename),
-        html.pre([attribute.class("text-red-500")], [
-          html.text(string.inspect(inner)),
-        ]),
-      ])
-  }
-}
-
-@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.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds.gleam
index 3ed7186..28a8096 100644
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds.gleam
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds.gleam
@@ -1,6 +1,6 @@
 // Imports
 import cynthia_websites_mini_client/messages
-import cynthia_websites_mini_client/model_type
+import cynthia_websites_mini_client/model_messages
 import cynthia_websites_mini_client/pottery/oven
 import gleam/bool
 import gleam/dict.{type Dict}
@@ -23,7 +23,7 @@ import cynthia_websites_mini_client/pottery/molds/pastels
 pub fn into(
   layout layout: String,
   for theme_type: String,
-  using model: model_type.Model,
+  using model: model_messages.Model,
 ) -> fn(Element(messages.Msg), Dict(String, decode.Dynamic)) ->
   element.Element(messages.Msg) {
   let #(v, is_post) = case theme_type {
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/pottery/oven.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/oven.gleam
deleted file mode 100644
index c821550..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/oven.gleam
+++ /dev/null
@@ -1,69 +0,0 @@
-import cynthia_websites_mini_client/dom
-import cynthia_websites_mini_client/messages
-import lustre/attribute
-import lustre/element.{type Element}
-import lustre/element/html
-import plinth/javascript/console
-
-pub fn error(
-  error_message msg: String,
-  recoverable recoverable: Bool,
-) -> Element(messages.Msg) {
-  console.warn("Error page invoked")
-  console.error(msg)
-  let assert Ok(_) = dom.push_title("Cynthia Mini: Error!")
-  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-neutral text-neutral-content w-96")], [
-        html.div([attribute.class("card-body items-center text-center")], [
-          html.h2([attribute.class("card-title")], [
-            html.text("An error occurred"),
-          ]),
-          html.p(
-            [
-              attribute.class(
-                "border-4 border-accent border-dotted pl-4 bg-secondary bg-opacity-10",
-              ),
-            ],
-            [html.text(msg)],
-          ),
-          case recoverable {
-            True ->
-              html.div([attribute.class("card-actions justify-end")], [
-                html.a(
-                  [
-                    attribute.href("javascript:window.location.reload(1)"),
-                    attribute.class("btn btn-neutral-300"),
-                  ],
-                  [html.text("Refresh")],
-                ),
-                html.a(
-                  [
-                    attribute.class("btn btn-ghost"),
-                    attribute.href(
-                      // You might think "oh no, this isn't activating any lustre messages". Think again.
-                      // We WANT to do a fresh page load here. It's an error, so we
-                      // can safely assume lustre doesn't know how to get out.
-                      "javascript:window.history.back(1);javascript:window.location.reload()",
-                    ),
-                  ],
-                  [html.text("Go back")],
-                ),
-              ])
-            False ->
-              html.div([attribute.class("btn btn-neutral-300")], [
-                element.text(
-                  "If you know the owner of this site, please contact them about this.",
-                ),
-              ])
-          },
-        ]),
-      ]),
-    ],
-  )
-}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/paints.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/paints.gleam
deleted file mode 100644
index ecee5d2..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/paints.gleam
+++ /dev/null
@@ -1,40 +0,0 @@
-import cynthia_websites_mini_client/configtype
-import cynthia_websites_mini_client/dom
-import cynthia_websites_mini_client/model_type.{type Model}
-import cynthia_websites_mini_client/ui/themes_generated
-import gleam/list
-import gleam/option
-import gleam/result
-import plinth/javascript/console
-
-/// Fetches the corresponding theme and layout to the user's preferred colorscheme
-pub fn get_sytheme(model: Model) {
-  let theme = case dom.get_color_scheme() {
-    "light" -> {
-      model.complete_data
-      |> option.map(fn(data) { data.global_theme })
-      |> option.to_result(Nil)
-      |> result.map_error(fn(_) {
-        console.error("Error getting light color scheme from database")
-      })
-      |> result.unwrap(
-        configtype.default_shared_cynthia_config_global_only.global_theme,
-      )
-    }
-    "dark" -> {
-      model.complete_data
-      |> option.map(fn(data) { data.global_theme_dark })
-      |> option.to_result(Nil)
-      |> result.map_error(fn(_) {
-        console.error("Error getting dark color scheme from database")
-      })
-      |> result.unwrap(
-        configtype.default_shared_cynthia_config_global_only.global_theme,
-      )
-    }
-    _ -> panic as "Invalid color scheme"
-  }
-
-  themes_generated.themes
-  |> list.find(fn(th) { th.name == theme })
-}
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 56% 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..bcb6238 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") -pub fn get_color_scheme() -> String +@external(javascript, "./ts_ffi.ts", "get_color_scheme") +pub fn get_color_scheme() -> Bool /// 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 86% 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..7d6f9b1 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 @@ -4,9 +4,9 @@ export function get_color_scheme() { // Media queries the preferred color colorscheme if (window.matchMedia("(prefers-color-scheme: dark)").matches) { - return "dark"; + return false; } - return "light"; + return true; } export function set_data(el: HTMLElement, key: string, val: string) { @@ -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 = \"\"\" -
-

{{ title }}

- - {{#if is_post}} -

- Published: {{ date_published }} - {{#if category }} | Category: {{ category }}{{/if}} -

- {{/if}} -
-
- {{{ body }}} -
- {{#if is_post}} - {{#if tags}} -
-

Tags: - {{#each tags}} -{{this}} - {{/each}} -

- {{/if}} - {{/if}} -
-\"\"\" - -[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) { - console.error(premixed.text_error_red( - "Error: Could not write cynthia.toml: " <> e, - )) - process.exit(1) - }) - { - console.log("Downloading default site icon...") - // Download https://raw.githubusercontent.com/strawmelonjuice/CynthiaWebsiteEngine-mini/refs/heads/main/asset/153916590.png to assets/site_icon.png - // Ignore any errors, if it fails, it fails. - let assert Ok(req) = - request.to( - "https://raw.githubusercontent.com/strawmelonjuice/CynthiaWebsiteEngine-mini/refs/heads/main/asset/153916590.png", - ) - use resp <- promise.try_await(fetch.send(req)) - use resp <- promise.try_await(fetch.read_bytes_body(resp)) - case - simplifile.write_bits(process.cwd() <> "/assets/site_icon.png", resp.body) - { - Ok(_) -> Nil - Error(_) -> { - console.error("Error: Could not write assets/site_icon.png") - Nil - } - } - promise.resolve(Ok(Nil)) - } - { - console.log("Creating example content...") - [ - item( - to: "hangers.dj", - with: contenttypes.Content( - filename: "hangers.dj", - title: "Hangers", - description: "An example page about hangers", - layout: "theme", - permalink: "/hangers", - 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! :)", - ), - ), - ext_item( - 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.dj", - with: contenttypes.Content( - filename: "themes.dj", - title: "Themes", - description: "External page example, using the theme list, downloading from ", - layout: "theme", - permalink: "/themes", - data: contenttypes.PageData([1], False), - inner_plain: "", - ), - ), - item( - "index.dj", - contenttypes.Content( - filename: "index.dj", - title: "Example landing", - description: "This is an example index page", - layout: "cindy-landing", - permalink: "/", - data: contenttypes.PageData([1], True), - inner_plain: configtype.ootb_index, - ), - ), - item( - to: "example-post.dj", - with: contenttypes.Content( - filename: "", - title: "An example post!", - description: "This is an example post", - layout: "theme", - permalink: "/example-post", - data: contenttypes.PostData( - category: "example", - date_published: "2021-01-01", - date_updated: "2021-01-01", - tags: ["example"], - ), - inner_plain: "# Hello, World!\n\nHello! This is an example post, you'll find me at `content/example-post.dj`.", - ), - ), - item( - to: "posts", - with: contenttypes.Content( - filename: "posts", - title: "Posts", - description: "this page is not actually shown, due to the ! prefix in the permalink", - layout: "default", - permalink: "!/", - data: contenttypes.PageData(in_menus: [1], hide_meta_block: True), - inner_plain: "", - ), - ), - ] - |> list.flatten - |> write_posts_and_pages_to_fs - } -} - -fn item( - to path: String, - with content: contenttypes.Content, -) -> List(#(String, String)) { - let path = files.path_join([process.cwd(), "/content/", path]) - let meta_json = - content - |> contenttypes.encode_content_for_fs - |> json.to_string() - let meta_path = path <> ".meta.json" - case string.starts_with(content.permalink, "!") { - True -> { - // No content file for post lists. - [#(meta_path, meta_json)] - } - False -> [#(meta_path, meta_json), #(path, content.inner_plain)] - } -} - -fn ext_item( - to fpath: String, - from path: String, - with content: contenttypes.Content, -) -> List(#(String, String)) { - let meta_json = - json.object([ - #("path", json.string(path)), - #("title", json.string(content.title)), - #("description", json.string(content.description)), - #("layout", json.string(content.layout)), - #("permalink", json.string(content.permalink)), - #("data", contenttypes.encode_content_data(content.data)), - ]) - |> json.to_string() - - [ - #( - files.path_join([process.cwd(), "/content/", fpath]) <> ".meta.json", - meta_json, - ), - ] -} - -// What? The function name is descriptive! -fn write_posts_and_pages_to_fs(items: List(#(String, String))) -> Nil { - items - |> list.each(fn(set) { - let #(path, content) = set - path - |> fs.write_file_sync(content) - }) -} 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 deleted file mode 100644 index 4306bcf..0000000 --- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config/v4.gleam +++ /dev/null @@ -1,468 +0,0 @@ -//// 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 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 { - 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.base64_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() -} diff --git a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/mutable_model_type.gleam b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/mutable_model_type.gleam deleted file mode 100644 index 2c7ed6b..0000000 --- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/mutable_model_type.gleam +++ /dev/null @@ -1,28 +0,0 @@ -import cynthia_websites_mini_client/configtype -import cynthia_websites_mini_server/config as config_module -import gleam/javascript/promise -import gleam/option.{type Option, None} -import javascript/mutable_reference - -pub type MutableModel = - mutable_reference.MutableReference(MutableModelContent) - -pub fn new() -> promise.Promise(MutableModel) { - use cfg <- promise.await(config_module.capture_config()) - mutable_reference.new(MutableModelContent( - cached_response: None, - cached_jsonld: None, - cached_sitemap: None, - config: cfg, - )) - |> promise.resolve() -} - -pub type MutableModelContent { - MutableModelContent( - cached_response: Option(String), - cached_jsonld: Option(String), - cached_sitemap: Option(String), - config: configtype.SharedCynthiaConfigGlobalOnly, - ) -} diff --git a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/request_ffi.ts b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/request_ffi.ts deleted file mode 100644 index 128cfde..0000000 --- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/request_ffi.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { BunFile } from "bun"; -import { Ok as GleamOk, Error as GleamError } from "../../prelude"; - -export async function get_request_body(req: Request) { - let a = req.body!; - const chunks: Uint8Array[] = []; - const reader = a.getReader(); - while (true) { - const { done, value } = await reader.read(); - if (done) { - break; - } else { - chunks.push(value); - } - } - return concatArrayBuffers(chunks); -} - -export async function get_request_body_as_text(req: Request): Promise { - const bits = await get_request_body(req); - const decoder = new TextDecoder("utf-8"); - return decoder.decode(bits); -} - -function concatArrayBuffers(chunks: Uint8Array[]): Uint8Array { - const result = new Uint8Array(chunks.reduce((a, c) => a + c.length, 0)); - let offset = 0; - for (const chunk of chunks) { - result.set(chunk, offset); - offset += chunk.length; - } - return result; -} - -export async function answer_bunrequest_with_file(file: BunFile) { - return new Response(await file.bytes()); -} diff --git a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/ssrs.gleam b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/ssrs.gleam index 6aa4b6f..190fa33 100644 --- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/ssrs.gleam +++ b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/ssrs.gleam @@ -3,9 +3,8 @@ import bungibindies/bun/http/serve/response import bungibindies/bun/spawn.{OptionsToSubprocess} import cynthia_websites_mini_client import cynthia_websites_mini_client/configtype -import cynthia_websites_mini_client/dom import cynthia_websites_mini_client/ui -import cynthia_websites_mini_server/mutable_model_type +import cynthia_websites_mini_server/mutable_model_messages import cynthia_websites_mini_server/utils/files.{client_css, client_js} import gleam/bool import gleam/dict @@ -15,12 +14,11 @@ import gleam/option.{type Option, None, Some} import gleam/result import gleam/string import gleamy_lights/console -import javascript/mutable_reference import plinth/node/process import simplifile -pub fn ssrs(mutable_model: mutable_model_type.MutableModel) { +pub fn ssrs(mutable_model: mutable_model_messages.MutableModel) { let model = mutable_model |> mutable_reference.get() dict.new() |> dict.insert("/index.html", main(model)) @@ -28,49 +26,7 @@ pub fn ssrs(mutable_model: mutable_model_type.MutableModel) { |> Some } -pub fn index_html(model: mutable_model_type.MutableModelContent) { - let gc: configtype.SharedCynthiaConfigGlobalOnly = model.config - " - - - - -<<site hosted by Cynthia mini>> - "/> - "/> - -" <> case model.cached_jsonld { - Some(jsonld) -> - "" - None -> " " - } <> " - - - - - - - -
-
- " <> footer(True, gc.git_integration) <> " - - -" -} - -fn main(model: mutable_model_type.MutableModelContent) { +fn main(model: mutable_model_messages.MutableModelContent) { response.new() |> response.set_body(index_html(model)) |> response.set_headers( @@ -110,8 +66,11 @@ pub fn footer(can_hide: Bool, git_integration: Bool) { True -> [ui.footer] |> list.append( - case { simplifile.is_directory(process.cwd() <> "/.git/") } { - Ok(True) -> { + case + { simplifile.is_directory(process.cwd() <> "/.git/") }, + which("git") + { + Ok(True), Some(git) -> { console.log("[Git integration] git repository detected") [ ", created from " @@ -119,7 +78,7 @@ pub fn footer(can_hide: Bool, git_integration: Bool) { helper_get_git_remote_commit(), { spawn.sync(OptionsToSubprocess( - cmd: ["git", "rev-parse", "--short", "HEAD"], + cmd: [git, "rev-parse", "--short", "HEAD"], cwd: Some(process.cwd()), env: None, stderr: Some(spawn.Ignore), @@ -163,13 +122,13 @@ pub fn footer(can_hide: Bool, git_integration: Bool) { window.setTimeout(function () { let lastScrollTop = 0; let ticking = false; - + function handleScroll(event) { if (!ticking) { requestAnimationFrame(function() { const footer = document.querySelector('#cynthiafooter'); let scrollingDown; - + if (event.type === 'wheel') { // For wheel events, use deltaY scrollingDown = event.deltaY > 0; @@ -180,7 +139,7 @@ pub fn footer(can_hide: Bool, git_integration: Bool) { scrollingDown = currentScroll > (target.lastScrollTop || 0); target.lastScrollTop = currentScroll; } - + if (scrollingDown) { // Scrolling down footer.style.transform = 'translate3d(0, 40px, 0)'; @@ -190,7 +149,7 @@ pub fn footer(can_hide: Bool, git_integration: Bool) { footer.style.transform = 'translate3d(0, 0, 0)'; footer.style.opacity = '1'; } - + ticking = false; }); ticking = true; @@ -200,7 +159,7 @@ pub fn footer(can_hide: Bool, git_integration: Bool) { // Listen at the document level for all scroll events document.addEventListener('scroll', handleScroll, { capture: true, passive: true }); document.addEventListener('wheel', handleScroll, { capture: true, passive: true }); - + document.querySelector('#cynthiafooter').addEventListener('click', function () { this.style.transform = 'translate3d(0, 0, 0)'; this.style.opacity = '1'; @@ -213,31 +172,32 @@ pub fn footer(can_hide: Bool, git_integration: Bool) { /// If succeeds, returns a html link to the current commit on the remote, by just removing the last part of the URL and adding "/commit/". fn helper_get_git_remote_commit() -> Option(String) { - let remote_cmd = - spawn.sync(OptionsToSubprocess( - cmd: ["git", "config", "--get", "remote.origin.url"], - cwd: Some(process.cwd()), - env: None, - stderr: Some(spawn.Ignore), - stdout: Some(spawn.Pipe), - )) - |> spawn.stdout() - |> result.map(string.trim) - |> option.from_result() - |> option.map(fn(str) { - case string.ends_with(str, ".git") { - True -> string.drop_end(str, 4) - False -> str - } - }) - use remote <- option.then(remote_cmd) - // If remote does not start with http(s), we can't use it. - use <- bool.guard( - when: bool.negate(string.starts_with(remote, "http")), - return: None, - ) case which("git") { Ok(git) -> { + let remote_cmd = + spawn.sync(OptionsToSubprocess( + cmd: [git, "config", "--get", "remote.origin.url"], + cwd: Some(process.cwd()), + env: None, + stderr: Some(spawn.Ignore), + stdout: Some(spawn.Pipe), + )) + |> spawn.stdout() + |> result.map(string.trim) + |> option.from_result() + |> option.map(fn(str) { + case string.ends_with(str, ".git") { + True -> string.drop_end(str, 4) + False -> str + } + }) + use remote <- option.then(remote_cmd) + // If remote does not start with http(s), we can't use it. + use <- bool.guard( + when: bool.negate(string.starts_with(remote, "http")), + return: None, + ) + let commit_cmd = spawn.sync(OptionsToSubprocess( cmd: [git, "rev-parse", "--verify", "HEAD"], diff --git a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/web.gleam b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/web.gleam deleted file mode 100644 index cdb525c..0000000 --- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/web.gleam +++ /dev/null @@ -1,217 +0,0 @@ -import bungibindies/bun -import bungibindies/bun/bunfile.{type BunFile} -import bungibindies/bun/http/serve/request.{type Request} -import bungibindies/bun/http/serve/response -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 gleam/dict -import gleam/javascript/array -import gleam/javascript/promise.{type Promise} -import gleam/json -import gleam/option.{None, Some} -import gleam/result -import gleam/uri -import gleamy_lights/console -import gleamy_lights/premixed -import javascript/mutable_reference -import plinth/node/process -import simplifile - -pub fn handle_request( - req: Request, - mutable_model: mutable_model_type.MutableModel, -) { - let assert Ok(req_uri) = req |> request.url() |> uri.parse() - as "Request URI should be valid" - let path = req_uri.path - - // Ensure JSONs are generated if needed - use _ <- promise.await( - case mutable_reference.get(mutable_model).cached_jsonld { - Some(_) -> promise.resolve(Nil) - // Cache hit, no need to generate - None -> generate_jsons(mutable_model) |> promise.map(fn(_) { Nil }) - }, - ) - - let assert Some(dynastatic) = ssrs.ssrs(mutable_model) - as "These routes should always be valid." - case path { - "/" -> { - console.log( - premixed.text_ok_green("[ 200 ]\t") - <> "(GET)\t" - <> premixed.text_lightblue("/") - <> " " - <> premixed.text_cyan( - "\t(this means client-side will now start loading a web page)", - ), - ) - dynastatic - |> dict.get("/index.html") - |> result.unwrap(response.new()) - |> promise.resolve() - } - "/site.json" -> { - console.log( - premixed.text_ok_green("[ 200 ]\t") - <> "(GET)\t" - <> premixed.text_lightblue("/site.json") - <> " " - <> premixed.text_cyan( - "\t(this means client-side will now start loading content!)", - ), - ) - let model = mutable_reference.get(mutable_model) - let re = case model.cached_response { - Some(res_string) -> { - // Cache hit! Return the cached response string so that it can be used in the response body - res_string |> promise.resolve - } - None -> { - // If there is no cached response, load the complete data from the config file - // and encode it as JSON - use res <- promise.map(generate_jsons(mutable_model)) - let res_string: String = res.0 - - // Now return the response string promise so that it can be used in the response body - res_string - } - } - use body <- promise.await(re) - response.set_body(response.new(), body) - |> response.set_headers( - [#("Content-Type", "application/json; charset=utf-8")] - |> array.from_list(), - ) - |> response.set_status(200) - |> promise.resolve - } - "/sitemap.xml" -> { - let model = mutable_reference.get(mutable_model) - case model.cached_sitemap { - Some(sitemap_xml) -> { - console.log( - premixed.text_ok_green("[ 200 ]\t") - <> "(GET)\t" - <> premixed.text_lightblue("/sitemap.xml"), - ) - response.set_body(response.new(), sitemap_xml) - |> response.set_headers( - [#("Content-Type", "application/xml; charset=utf-8")] - |> array.from_list(), - ) - |> response.set_status(200) - |> promise.resolve() - } - None -> { - use _ <- promise.await(generate_jsons(mutable_model)) - let model = mutable_reference.get(mutable_model) - - case model.cached_sitemap { - Some(sitemap_xml) -> { - console.log( - premixed.text_ok_green("[ 200 ]\t") - <> "(GET)\t" - <> premixed.text_lightblue("/sitemap.xml"), - ) - response.set_body(response.new(), sitemap_xml) - |> response.set_headers( - [#("Content-Type", "application/xml; charset=utf-8")] - |> array.from_list(), - ) - |> response.set_status(200) - |> promise.resolve() - } - None -> { - console.error( - premixed.text_error_red("[ 404 ] ") - <> "(GET)\t" - <> premixed.text_lightblue("/sitemap.xml"), - ) - dynastatic - |> dict.get("/404") - |> result.unwrap(response.new()) - |> promise.resolve() - } - } - } - } - } - "/assets/" <> f -> { - let filepath = process.cwd() <> "/assets/" <> f - case simplifile.is_file(filepath) { - Ok(True) -> { - console.log( - premixed.text_ok_green("[ 200 ]\t") - <> "(GET)\t" - <> premixed.text_lightblue("/assets/") - <> premixed.text_cyan(f), - ) - filepath - |> bun.file() - |> answer_bunrequest_with_file() - } - _ -> { - console.error( - premixed.text_error_red("[ 404 ] ") - <> "(GET)\t" - <> premixed.text_lightblue("/assets/") - <> premixed.text_cyan(f), - ) - dynastatic - |> dict.get("/404") - |> result.unwrap(response.new()) - |> promise.resolve() - } - } - } - f -> { - console.error( - premixed.text_error_red("[ 404 ] ") - <> "(" - <> req |> request.method - <> ")\t" - <> premixed.text_lightblue(f), - ) - dynastatic - |> dict.get("/404") - |> result.unwrap(response.new()) - |> promise.resolve() - } - } -} - -@external(javascript, "./request_ffi.ts", "get_request_body") -pub fn get_request_body(req: Request) -> Promise(BitArray) - -@external(javascript, "./request_ffi.ts", "get_request_body_as_text") -pub fn get_request_body_as_text(req: Request) -> Promise(String) - -@external(javascript, "./request_ffi.ts", "answer_bunrequest_with_file") -pub fn answer_bunrequest_with_file(file: BunFile) -> Promise(response.Response) - -fn generate_jsons( - mutable_model: mutable_model_type.MutableModel, -) -> Promise(#(String, String, String)) { - use complete_data <- promise.await(config.load()) - 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) - // Add all representations to the model cache - mutable_reference.update(mutable_model, fn(model) { - mutable_model_type.MutableModelContent( - ..model, - cached_response: Some({ res_string }), - cached_jsonld: Some({ res_jsonld }), - cached_sitemap: opt_sitemap, - ) - }) - #(res_string, res_jsonld, option.unwrap(opt_sitemap, "")) |> promise.resolve -} diff --git a/generate-ffi b/generate-ffi index eb9a9d3..b6bdeb6 100755 --- a/generate-ffi +++ b/generate-ffi @@ -12,6 +12,8 @@ const config_root = process.env.MISE_CONFIG_ROOT || "."; // 1. Generate Gleam theme file const daisyui_themes = []; +const layouts_doubles = []; + let themes_gleam_content = ` // This file is generated in Cynthia Mini's build process. // Do not edit it manually. @@ -36,6 +38,10 @@ pub type ThemePrevalence { pub const themes = [ `; for (const theme of themeconf) { + let layout = theme.layout.trim() + if (layout == "cindy") {layout="cindy-simple"} + layout = layout.replaceAll("-","_") + if (existsSync("./cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/" + layout + ".gleam")) { let prevalence = "ThemeLight"; if (theme.prevalence === "dark") prevalence = "ThemeDark"; const daisy_ui_theme_name = (typeof theme.daisyUI === "string") ? theme.daisyUI : Object.keys(theme.daisyUI)[0]; @@ -43,8 +49,23 @@ for (const theme of themeconf) { const font_size_mono = theme["font-size-mono"].toString().includes(".") ? theme["font-size-mono"] : `${theme["font-size-mono"]}.0`; themes_gleam_content += ` SharedCynthiaTheme(\n name: \"${theme.name}\",\n fonts: ${JSON.stringify(theme.fonts)},\n fonts_mono: ${JSON.stringify(theme["fonts-mono"])},\n font_size: ${font_size},\n font_size_mono: ${font_size_mono},\n prevalence: ${prevalence},\n daisy_ui_theme_name: \"${daisy_ui_theme_name}\",\n layout: \"${theme.layout}\"\n ),\n`; daisyui_themes.push(theme.daisyUI); + layouts_doubles.push(layout); + } } + + themes_gleam_content += `]\n`; +let s = new Set(layouts_doubles); +const layouts = [...s] +let layout_imports = ""; +let layout_registrars = ` +/// Registers all layouts as Lustre components +pub fn register_all() {` +for (let layout of layouts) { + layout_registrars+="\n\tlet assert Ok(_) = "+layout+".register()\n" + layout_imports+="\nimport cynthia_websites_mini_client/layouts/"+layout+"\n" +} +layout_registrars+="}" // Write the generated Gleam file, creating the directory if it doesn't exist const shared_ui_dir = path.join(config_root, "/cynthia_websites_mini_client/src/cynthia_websites_mini_client/ui"); if (!existsSync(shared_ui_dir)) { @@ -52,7 +73,7 @@ if (!existsSync(shared_ui_dir)) { } writeFileSync( path.join(shared_ui_dir, "themes_generated.gleam"), - themes_gleam_content + layout_imports+themes_gleam_content+layout_registrars ); // If this is part of the client's build process, we stop here. The themes file is still generated, but the CSS and client bundle will not be created. diff --git a/mise.toml b/mise.toml index 5d6f701..825dde4 100644 --- a/mise.toml +++ b/mise.toml @@ -1,6 +1,6 @@ [tools] bun = "latest" -gleam = "latest" +gleam = "1.14.0" # /================================== Tasks ================================\ # | These tasks are in comment-split sections | @@ -294,4 +294,4 @@ if (found) { } else { console.log("No prohibited keywords found."); } -""" \ No newline at end of file +""" diff --git a/package.json b/package.json index 7e214d2..8a68d54 100644 --- a/package.json +++ b/package.json @@ -38,5 +38,8 @@ "prepack": "bun run bundle", "run-in-test": "mise run run-dev", "test": "mise run test-all" + }, + "dependencies": { + "@djot/djot": "^0.3.2" } } diff --git a/themes.json b/themes.json index b3f642a..24a5210 100644 --- a/themes.json +++ b/themes.json @@ -570,15 +570,5 @@ } }, "layout": "sepia" - }, - { - "name": "ownit", - "prevalence": "light", - "fonts": [], - "font-size": 16, - "fonts-mono": ["Fira Code", "Monaco", "monospace"], - "font-size-mono": 14, - "daisyUI": "autumn", - "layout": "ownit" } ]