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..f8b203a 100644 --- a/cynthia_websites_mini_client/gleam.toml +++ b/cynthia_websites_mini_client/gleam.toml @@ -1,41 +1,28 @@ name = "cynthia_websites_mini_client" +version = "1.0.0" target = "javascript" -gleam = ">= 1.9.0" -version = "1.3.0" -description = "The Cynthia Mini client." -licences = ["AGPL-3.0"] -repository = { type = "github", user = "CynthiaWebsiteEngine", repo = "Mini", path = "cynthia_websites_mini_client" } -links = [ - { title = "NPM", href = "https://www.npmjs.com/package/@cynthiaweb/cynthiaweb-mini" }, - { title = "GitHub releases", href = "https://github.com/CynthiaWebsiteEngine/Mini/releases" } -] - -[javascript] -typescript_declarations = true +# Fill out these fields if you intend to generate HTML documentation or publish +# your project to the Hex package manager. +# +# description = "" +# licences = ["Apache-2.0"] +# repository = { type = "github", user = "", repo = "" } +# links = [{ title = "Website", href = "" }] +# +# For a full reference of all the available options, you can have a look at +# https://gleam.run/writing-gleam/gleam-toml/. [dependencies] -# Shared dependencies with the server -gleam_stdlib = "0.59.0" -plinth = ">= 0.5.9 and < 1.0.0" -gleam_javascript = "1.0.0" -gleam_community_colour = "1.4.1" -gleam_json = "2.3.0" -gleam_http = "3.7.2" -gleam_fetch = "1.3.0" - -# Lustre specific dependencies -lustre = ">= 5.0.2 and < 6.0.0" -rsvp = ">= 1.0.0 and < 2.0.0" -modem = "2.0.2" - - -# Other dependencies -odysseus = ">= 1.0.0 and < 2.0.0" -houdini = ">= 1.1.0 and < 2.0.0" -jot = ">= 5.0.0 and < 6.0.0" -gleam_time = ">= 1.2.0 and < 2.0.0" - +gleam_stdlib = ">= 0.44.0 and < 2.0.0" +gleam_json = ">= 3.1.0 and < 4.0.0" +plinth = ">= 0.9.2 and < 1.0.0" +tom = "1.1.1" +lustre = ">= 5.5.2 and < 6.0.0" +modem = ">= 2.1.2 and < 3.0.0" +rsvp = ">= 1.2.0 and < 2.0.0" +gleam_fetch = ">= 1.3.0 and < 2.0.0" +gleam_http = ">= 4.3.0 and < 5.0.0" +chilp = { git = "https://forge.strawmelonjuice.com/strawmelonjuice/chilp.git", ref = "b43d01e6c39d12121a5cbfbf53b6b1dec8bc49f1" } [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" -birdie = ">= 1.2.7 and < 2.0.0" diff --git a/cynthia_websites_mini_client/manifest.toml b/cynthia_websites_mini_client/manifest.toml index 906711e..fe52e34 100644 --- a/cynthia_websites_mini_client/manifest.toml +++ b/cynthia_websites_mini_client/manifest.toml @@ -2,54 +2,34 @@ # You typically do not need to edit this file packages = [ - { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, - { name = "birdie", version = "1.3.1", build_tools = ["gleam"], requirements = ["argv", "edit_distance", "filepath", "glance", "gleam_community_ansi", "gleam_stdlib", "justin", "rank", "simplifile", "term_size", "trie_again"], otp_app = "birdie", source = "hex", outer_checksum = "F811C9EDAF920EF48597A26E788907AAF80D9239A5E8C8CCFBD0DD1BB10184D7" }, - { name = "edit_distance", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "edit_distance", source = "hex", outer_checksum = "A1E485C69A70210223E46E63985FA1008B8B2DDA9848B7897469171B29020C05" }, - { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, - { name = "glance", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "FAA3DAC74AF71D47C67D88EB32CE629075169F878D148BB1FF225439BE30070A" }, - { name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" }, - { name = "gleam_community_colour", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "386CB9B01B33371538672EEA8A6375A0A0ADEF41F17C86DDCB81C92AD00DA610" }, - { name = "gleam_erlang", version = "0.34.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "0C38F2A128BAA0CEF17C3000BD2097EB80634E239CE31A86400C4416A5D0FDCC" }, + { name = "chilp", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "gleam_time", "lustre", "rsvp"], source = "git", repo = "https://forge.strawmelonjuice.com/strawmelonjuice/chilp.git", commit = "b43d01e6c39d12121a5cbfbf53b6b1dec8bc49f1" }, + { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, { name = "gleam_fetch", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "2CBF9F2E1C71AEBBFB13A9D5720CD8DB4263EB02FE60C5A7A1C6E17B0151C20C" }, - { name = "gleam_http", version = "3.7.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8A70D2F70BB7CFEB5DF048A2183FFBA91AF6D4CF5798504841744A16999E33D2" }, - { name = "gleam_httpc", version = "4.2.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "224BF35B2091502921D1623F35E6FA52815B75D99D18AEFB9DAEA0B8AEADD7A1" }, + { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, + { name = "gleam_httpc", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C545172618D07811494E97AAA4A0FB34DA6F6D0061FDC8041C2F8E3BE2B2E48F" }, { name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" }, - { name = "gleam_json", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C55C5C2B318533A8072D221C5E06E5A75711C129E420DD1CE463342106012E5D" }, - { name = "gleam_otp", version = "0.16.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "50DA1539FC8E8FA09924EB36A67A2BBB0AD6B27BCDED5A7EF627057CF69D035E" }, - { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, - { name = "gleam_stdlib", version = "0.59.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F8FEE9B35797301994B81AF75508CF87C328FE1585558B0FFD188DC2B32EAA95" }, - { name = "gleam_time", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "F9AB61CE910F3071B136E1C8E214A46C406734F710D3AF75C99B00DA785902A2" }, - { name = "gleeunit", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "A7DD6C07B7DA49A6E28796058AA89E651D233B357D5607006D70619CD89DAAAB" }, - { name = "glexer", version = "2.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "5C235CBDF4DA5203AD5EAB1D6D8B456ED8162C5424FE2309CFFB7EF438B7C269" }, - { name = "houdini", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "houdini", source = "hex", outer_checksum = "5BA517E5179F132F0471CB314F27FE210A10407387DA1EA4F6FD084F74469FC2" }, - { name = "jot", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "houdini", "splitter"], otp_app = "jot", source = "hex", outer_checksum = "2C1B30CC00B0D79F904028F48229C0BB354F3C1BC05EE99D4F3D423E223D85BF" }, - { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, - { name = "lustre", version = "5.0.3", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "0BB69D69A9E75E675AA2C32A4A0E5086041D037829FC8AD385BA6A59E45A60A2" }, - { name = "modem", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib", "lustre"], otp_app = "modem", source = "hex", outer_checksum = "EF6B6B187E9D6425DFADA3A1AC212C01C4F34913A135DA2FF9B963EEF324C1F7" }, - { name = "odysseus", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "odysseus", source = "hex", outer_checksum = "6A97DA1075BDDEA8B60F47B1DFFAD49309FA27E73843F13A0AF32EA7087BA11C" }, - { name = "plinth", version = "0.7.1", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "63BB36AACCCCB82FBE46A862CF85CB88EBE4EF280ECDBAC4B6CB042340B9E1D8" }, - { name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" }, - { name = "rsvp", version = "1.0.4", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_fetch", "gleam_http", "gleam_httpc", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "rsvp", source = "hex", outer_checksum = "EFCA7CD53B0A8738C06E136422D1FF080DBB657C89E077F7B9DD20BFACE0A77A" }, - { name = "simplifile", version = "2.3.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0A868DAC6063D9E983477981839810DC2E553285AB4588B87E3E9C96A7FB4CB4" }, - { name = "splitter", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "128FC521EE33B0012E3E64D5B55168586BC1B9C8D7B0D0CA223B68B0D770A547" }, - { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, - { name = "trie_again", version = "1.1.3", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "365FE609649F3A098D1D7FC7EA5222EE422F0B3745587BF2AB03352357CA70BB" }, + { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, + { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" }, + { name = "gleam_stdlib", version = "0.68.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F7FAEBD8EF260664E86A46C8DBA23508D1D11BB3BCC6EE1B89B3BC3E5C83FF1E" }, + { name = "gleam_time", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "56DB0EF9433826D3B99DB0B4AF7A2BFED13D09755EC64B1DAAB46F804A9AD47D" }, + { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, + { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" }, + { name = "lustre", version = "5.5.2", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "2DC2973D81C12E63251B636773217B8E09C5C84590A729750F6BCF009420B38E" }, + { name = "modem", version = "2.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib", "lustre"], otp_app = "modem", source = "hex", outer_checksum = "3F9682EBCBF4D26045F1038A7507E8C7967E49D43F9CA6BA68EF0C971B195A7F" }, + { name = "plinth", version = "0.9.2", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "A5A14C590F9820F8447E989B66C73C9DE077FAAB75618D639DFA1F3BCA45F946" }, + { name = "rsvp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_fetch", "gleam_http", "gleam_httpc", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "rsvp", source = "hex", outer_checksum = "40F9E0E662FF258E10C7041A9591261FE802D56625FB444B91510969644F7722" }, + { name = "tom", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0910EE688A713994515ACAF1F486A4F05752E585B9E3209D8F35A85B234C2719" }, ] [requirements] -birdie = { version = ">= 1.2.7 and < 2.0.0" } -gleam_community_colour = { version = "1.4.1" } -gleam_fetch = { version = "1.3.0" } -gleam_http = { version = "3.7.2" } -gleam_javascript = { version = "1.0.0" } -gleam_json = { version = "2.3.0" } -gleam_stdlib = { version = "0.59.0" } -gleam_time = { version = ">= 1.2.0 and < 2.0.0" } +chilp = { git = "https://forge.strawmelonjuice.com/strawmelonjuice/chilp.git", ref = "b43d01e6c39d12121a5cbfbf53b6b1dec8bc49f1" } +gleam_fetch = { version = ">= 1.3.0 and < 2.0.0" } +gleam_http = { version = ">= 4.3.0 and < 5.0.0" } +gleam_json = { version = ">= 3.1.0 and < 4.0.0" } +gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } -houdini = { version = ">= 1.1.0 and < 2.0.0" } -jot = { version = ">= 5.0.0 and < 6.0.0" } -lustre = { version = ">= 5.0.2 and < 6.0.0" } -modem = { version = "2.0.2" } -odysseus = { version = ">= 1.0.0 and < 2.0.0" } -plinth = { version = ">= 0.5.9 and < 1.0.0" } -rsvp = { version = ">= 1.0.0 and < 2.0.0" } +lustre = { version = ">= 5.5.2 and < 6.0.0" } +modem = { version = ">= 2.1.2 and < 3.0.0" } +plinth = { version = ">= 0.9.2 and < 1.0.0" } +rsvp = { version = ">= 1.2.0 and < 2.0.0" } +tom = { version = "1.1.1" } diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client.gleam index 4a765a8..91c0b07 100644 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client.gleam +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client.gleam @@ -1,621 +1,225 @@ +import chilp/widget +import cynthia_websites_mini_shared/config/site_json +import cynthia_websites_mini_shared/config/v4_1 +import cynthia_websites_mini_shared/ffi +import gleam/dynamic/decode +import gleam/fetch +import gleam/http/request +import gleam/http/response +import gleam/javascript/promise +import gleam/option.{None} +import gleam/result +import gleam/string +import plinth/browser/location +import plinth/browser/window +import rsvp + +pub const version = ffi.version + // IMPORTS --------------------------------------------------------------------- -import cynthia_websites_mini_client/configtype -import cynthia_websites_mini_client/configurable_variables -import cynthia_websites_mini_client/contenttypes -import cynthia_websites_mini_client/dom -import cynthia_websites_mini_client/messages.{ - type Msg, ApiReturnedData, SafeTimePassed, TriggerCheckForHashChange, - UserNavigateTo, -} -import cynthia_websites_mini_client/model_type.{type Model, Model} -import cynthia_websites_mini_client/pottery -import cynthia_websites_mini_client/utils -import cynthia_websites_mini_client/view -import gleam/bit_array -import gleam/bool -import gleam/dict -import gleam/dynamic -import gleam/float +import gleam/dict.{type Dict} import gleam/int import gleam/list -import gleam/option.{None, Some} -import gleam/order -import gleam/result -import gleam/string import gleam/uri.{type Uri} -import houdini import lustre +import lustre/attribute.{type Attribute} import lustre/effect.{type Effect} +import lustre/element.{type Element} +import lustre/element/html + +// Modem is a package providing effects and functionality for routing in SPAs. +// This means instead of links taking you to a new page and reloading everything, +// they are intercepted and your `update` function gets told about the new URL. import modem -import odysseus -import plinth/browser/window -import plinth/javascript/console -import plinth/javascript/global -import plinth/javascript/storage -import rsvp // MAIN ------------------------------------------------------------------------ pub fn main() { - let app = lustre.application(init, update, view.main) - let assert Ok(_) = lustre.start(app, "#viewable", Nil) + let app = lustre.application(init, update, view) + let assert Ok(sitejsonuri) = rsvp.parse_relative_uri("/site.json") + let assert Ok(req) = request.to(sitejsonuri |> uri.to_string()) + use resp <- promise.try_await(fetch.send(req)) + use resp <- promise.try_await(fetch.read_json_body(resp)) + let result = decode.run(resp.body, site_json.site_json_decoder()) + case resp.status, result { + 200, Ok(sitejson) -> { + let assert Ok(_) = lustre.start(app, "#viewable", sitejson) + Nil + } + // Failure is okay, we just don't activate and hope the server served well enough pregenerations. + _, _ -> Nil + } - Nil + promise.resolve(Ok(Nil)) } -fn await_safe_time() { - let set_timeout_nilled = fn(delay: Int, cb: fn() -> a) -> Nil { - global.set_timeout(delay, cb) - Nil - } - use dispatch <- effect.from - use <- set_timeout_nilled(200) - dispatch(SafeTimePassed) +// MODEL ----------------------------------------------------------------------- + +type Model { + Model( + data: site_json.SiteJSON, + route: Route, + chilp_model: widget.ChilpDataInYourModel(Msg), + ) } -fn check_for_hash_change_every_50ms() -> Effect(Msg) { - let set_timeout_nilled = fn(delay: Int, cb: fn() -> a) -> Nil { - global.set_timeout(delay, cb) - Nil - } - use dispatch <- effect.from - // Every 50ms, check for hash changes until 200ms have passed - set_timeout_nilled(50, fn() { dispatch(TriggerCheckForHashChange) }) - set_timeout_nilled(100, fn() { dispatch(TriggerCheckForHashChange) }) - set_timeout_nilled(150, fn() { dispatch(TriggerCheckForHashChange) }) - set_timeout_nilled(200, fn() { dispatch(TriggerCheckForHashChange) }) - Nil +type PostFilter { + ByCategory(String) + ByTag(String) + AnyFieldContains(String) } -/// Slightly more assertive way of finding url changes. This because sometimes a page change outside of the visibility of modem is undetected, mixing up the hashes and pages. This effect kicks in once they should have had their time and attempts to fix it. -fn check_for_hash_change(model: Model) -> Effect(Msg) { - use dispatch <- effect.from - case model.safetimepassed { - True -> { - Nil - } - False -> { - let assert Ok(session) = storage.local() - as "Browser is expected to have a localstorage." +type Route { + Index + PostsList(PostFilter) + Content(slug: String) + NotFound(uri: Uri) +} - case window.get_hash() { - Ok(f) -> { - let h = case f { - "" -> { - "/" - } - d -> { - d - } - } - case h == model.path { - True -> Nil - False -> { - console.log("[assertive] Hash changed to: " <> h) - let assert Ok(..) = storage.set_item(session, "last", h) - dispatch(UserNavigateTo(h)) - } - } - } - _ -> { - // This happens whenever the hash is not found - // like for example when utterances login just happened. - // This is not unexpected behaviour, since the storage knows better in those cases. - Nil +fn parse_route(uri: Uri) -> Route { + case uri.path_segments(uri.path) { + [] | [""] -> { + case location.hash(window.location(window.self())) { + Error(_) -> Index + + Ok("#!/category/" <> cat) -> PostsList(ByCategory(cat)) + Ok("#!/tag/" <> tag) -> PostsList(ByTag(tag)) + Ok("#!/search/" <> tag) -> PostsList(AnyFieldContains(tag)) + + Ok(c) -> { + let d = "Unhandled hashroute: " <> c + panic as d } } } + ["tagged", tag] -> PostsList(ByCategory(tag)) + ["category", cat] -> PostsList(ByTag(cat)) + ["post", slug] | ["page", slug] | ["content", slug] -> Content(slug:) + + _ -> NotFound(uri:) } } -fn init(_) -> #(Model, Effect(Msg)) { - console.log("Cynthia Client starting up") - let effects = - effect.batch([ - fetch_all(ApiReturnedData), - modem.init(on_url_change), - await_safe_time(), - check_for_hash_change_every_50ms(), - ]) - // Using local storage as session storage because session storage doesn't stay long enough - let assert Ok(session) = storage.local() - as "Browser is expected to have a localstorage." - // .. if the local storage is older than 1 minute though, we clear it - let val = case storage.get_item(session, "time") { - Ok(time) -> { - let now = utils.now() - let stamp = result.unwrap(int.parse(time), 0) - let diff = int.subtract(now, stamp) |> int.absolute_value - // 1 minutes = 60 seconds - let order = int.compare(diff, 60) - case order { - order.Eq | order.Gt -> False - order.Lt -> True - } - } - Error(..) -> { - False - } +/// We also need a way to turn a Route back into a an `href` attribute that we +/// can then use on `html.a` elements. It is important to keep this function in +/// sync with the parsing, but once you do, all links are guaranteed to work! +/// +fn href(route: Route, model: Model) -> Attribute(msg) { + let url = case route { + Index -> "/" + Content(c) -> { + dict.get(model.data.content, c) + |> result.map(fn(content) { + case content { + site_json.Post(..) -> { + "/post/" <> c + } + site_json.Page(..) -> { + "/page/" <> c + } + } + }) + |> result.unwrap("/content/" <> c) + } + NotFound(_) -> "/404" + PostsList(ByCategory(cat)) -> "/category/" <> cat + PostsList(ByTag(tag)) -> "/tagged/" <> tag + PostsList(AnyFieldContains(q)) -> "/#!/search/" <> q } - case val { - False -> { - console.log("Clearing local storage") - storage.clear(session) - } - True -> { - // Keeping local storage, updating time - let now = utils.now() |> int.to_string - case storage.set_item(session, "time", now) { - Ok(_) -> { - console.log("Updated local storage time") - } - Error(e) -> { - console.error( - "Error updating local storage time: " <> string.inspect(e), - ) - } - } - } - } - let initial_path = case storage.get_item(session, "last"), window.get_hash() { - Ok(path), _ -> { - // We have a last path in local storage. Return it. Hash will be set to it later. - path - } - Error(..), Ok("") | Error(..), Error(..) -> { - // No last path in local storage, so we set the hash to "/" - dom.set_hash("/") - let assert Ok(..) = storage.set_item(session, "last", "/") + attribute.href(url) +} - "/" - } - Error(..), Ok(f) -> { - // From the hash, we set the last path in local storage - let assert Ok(..) = storage.set_item(session, "last", f) - // and return the hash - f +fn init(appdata: site_json.SiteJSON) -> #(Model, Effect(Msg)) { + let route = case modem.initial_uri() { + Ok(uri) -> parse_route(uri) + Error(_) -> Index + } + let chilp_model = widget.init(Chilp) + let effect = + modem.init(fn(uri) { + uri + |> parse_route + |> UserNavigatedTo + }) + let model = Model(appdata, route:, chilp_model:) + let effect = case appdata.config.posts.comments { + v4_1.CommentsGithubStored(..) -> effect + v4_1.CommentsDisabled -> effect + // This site uses Chilp! Let's smoothen the UX by prefetching some of the posts in the background! + v4_1.CommentsMastodonStored -> { + appdata.content + |> dict.values + |> list.shuffle + |> list.filter(keeping: fn(c) { + case c { + site_json.Post(mastodon_comments:, ..) -> { + case mastodon_comments { + option.Some(..) -> True + _ -> False + } + } + _ -> False + } + }) + |> list.map(fn(post) { + let assert site_json.Post(mastodon_comments: option.Some(status), ..) = + post + let widget_ = + widget.new( + instance: status.instance, + post_id: status.id, + chilp_model:, + ) + widget.force(chilp_model:, on: widget_) + }) + |> list.shuffle + |> list.append([effect], _) + |> effect.batch } } - console.log("Initial path: " <> initial_path) - let model = - Model(initial_path, None, dict.new(), Ok(Nil), dict.new(), session, False) - #(model, effects) + #(model, effect) } -// Effect handlers -------------------------------------------------------------- -/// On url change: (Obviously) is triggered on url change, this is useful for intercepting the url hash change on in-site-navigation, that Cynthia uses. -fn on_url_change(uri: Uri) -> Msg { - pottery.destroy_comment_box() - console.log("URL changed to: " <> uri.to_string(uri)) - let assert Ok(#(_, d)) = - uri - |> uri.to_string - |> string.split_once("#") - messages.UserNavigateTo(d) -} +// UPDATE ---------------------------------------------------------------------- -/// Fetches data from server side -fn fetch_all( - on_response handle_response: fn(Result(configtype.CompleteData, rsvp.Error)) -> - msg, -) -> Effect(msg) { - let url = utils.phone_home_url() <> "site.json" - let decoder = configtype.complete_data_decoder() - let handler = rsvp.expect_json(decoder, handle_response) - console.log("Fetching site.json...") - rsvp.get(url, handler) +type Msg { + UserNavigatedTo(route: Route) + Chilp(widget.ChilpMsg) } -// UPDATE ---------------------------------------------------------------------- - fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { - // Update session storage with the current time - let now = utils.now() |> int.to_string - case storage.set_item(model.sessionstore, "time", now) { - Ok(_) -> { - Nil - } - Error(e) -> { - console.error( - "Error updating session storage time: " <> string.inspect(e), - ) - } - } case msg { - TriggerCheckForHashChange -> { - #(model, check_for_hash_change(model)) - } - SafeTimePassed -> { - #(Model(..model, safetimepassed: True), check_for_hash_change(model)) - } - UserNavigateTo(path) -> { - dom.set_hash(path) - let other = - model.other - |> dict.delete("search_term") - case storage.set_item(model.sessionstore, "last", path) { - Ok(_) -> { - console.log("Stored last path: " <> path) - } - Error(e) -> { - console.error("Error storing last path: " <> string.inspect(e)) - } - } - #(Model(..model, path:, other:), effect.none()) - } - messages.UserSearchTerm(search_term) -> { - let path = "!/search/" <> search_term - let computed_menus = model.computed_menus - let complete_data = model.complete_data - let status = model.status - let safetimepassed = model.safetimepassed - let other = - model.other - |> dict.insert("search_term", dynamic.from(search_term)) - dom.set_hash(path) - let sessionstore = model.sessionstore - #( - Model( - path:, - complete_data:, - computed_menus:, - status:, - other:, - sessionstore:, - safetimepassed:, - ), - effect.none(), - ) - } - ApiReturnedData(data) -> { - case data { - Error(e) -> { - let error_message = - "Cynthia Client failed " - <> case e { - rsvp.UnhandledResponse(s) -> - "to handle the server's response: '" <> string.inspect(s) <> "'" - rsvp.HttpError(_) | rsvp.NetworkError -> "to connect to server." - rsvp.JsonError(s) -> - "to decode response: '" <> string.inspect(s) <> "'" - _ -> " to load this site." - } - #(Model(..model, status: Error(error_message)), effect.none()) - } - Ok(new) -> { - console.log("Succesfully decoded new data, parsing into model...") - case new.comment_repo { - Some(..) -> - global.set_interval(300, pottery.comment_box_forced_styles) - None -> global.set_timeout(30_000, fn() { Nil }) - } - let computed_menus = compute_menus(new.content, model) - case convert_configurable(new.other_vars) { - Ok(dict_of_configurables) -> { - console.log("Succesfully unjsonified configurable variables.") - // I thought this was already done, but I see what is going on here, still gonna commit. This _should_ be a dynamic, not a fucken string. - let other = dict.merge(model.other, dict_of_configurables) - let status = Ok(Nil) - let complete_data = - Some(configtype.CompleteData(..new, other_vars: [])) - console.log("Updated model.") - #( - Model(..model, complete_data:, computed_menus:, status:, other:), - effect.none(), - ) - } - Error(mess) -> { - #(Model(..model, status: Error(mess)), effect.none()) - } - } - } - } - } - // This also shows pretty well how to store booleans in the model.other dict: Use results. - messages.UserOnGitHubLayoutToggleMenu -> { - let other = case dict.get(model.other, "github-layout menu open") { - Ok(..) -> { - // is open, so close it - dict.delete(model.other, "github-layout menu open") - } - Error(..) -> { - // is closed, so open it - dict.insert( - model.other, - "github-layout menu open", - dynamic.from(None), - ) - } - } - #(Model(..model, other:), effect.none()) - } - messages.CindyToggleMenu1 -> { - let other = case dict.get(model.other, "cindy menu 1 open") { - Ok(..) -> { - // is open, so close it - dict.delete(model.other, "cindy menu 1 open") - } - Error(..) -> { - // is closed, so open it - dict.insert(model.other, "cindy menu 1 open", dynamic.from(None)) - } - } - #(Model(..model, other:), effect.none()) + UserNavigatedTo(route:) -> { + let model = Model(..model, route:) + #(model, effect.none()) } - messages.UserOnDocumentationLayoutToggleSidebar -> { - let other = case dict.get(model.other, "documentation-sidebar-open") { - Ok(..) -> { - // is open, so close it - dict.delete(model.other, "documentation-sidebar-open") - } - Error(..) -> { - // is closed, so open it - dict.insert( - model.other, - "documentation-sidebar-open", - dynamic.from(None), - ) - } - } - #(Model(..model, other:), effect.none()) + Chilp(chilp_msg) -> { + let #(chilp_model, chilp_effects) = + widget.update(chilp_msg, model.chilp_model, browse_to) + #(Model(..model, chilp_model:), chilp_effects) } } } -/// ----------------------------------------------------------------------------------------- -/// Helper function to convert configurable variables into `Result(Dict(String,Dynamic))`'s, -/// allowing usage in `model.other` -/// ----------------------------------------------------------------------------------------- -fn convert_configurable(from: List(#(String, List(String)))) { - let defined = configurable_variables.typecontrolled - let res = - result.all( - list.map(from, fn(item) { - let #(keyname, probable_value): #(String, List(String)) = item - use found_type <- result.try( - list.last(probable_value) - |> result.replace_error( - "Invalid value at " - <> keyname - <> ", something might have gone wrong encoding this value at the server side.", - ), - ) - let defined_type = case list.key_find(defined, keyname) { - Ok(m) -> m - Error(_) -> found_type - } - // Check if a convertible is found - let might_rewrite_the_story: Result(#(String, List(String)), String) = case - bool.or( - bool.and( - bool.or( - defined_type == configurable_variables.var_bitstring, - defined_type == configurable_variables.var_string, - ), - bool.or( - defined_type == configurable_variables.var_bitstring, - defined_type == configurable_variables.var_string, - ), - ), - bool.and( - bool.or( - defined_type == configurable_variables.var_int, - defined_type == configurable_variables.var_float, - ), - bool.or( - defined_type == configurable_variables.var_int, - defined_type == configurable_variables.var_float, - ), - ), - ), - found_type, - defined_type, - probable_value - { - False, _, _, _ -> { - // Type is not found to be convertible, return as-is - Ok(#(found_type, probable_value)) - } - _, "integer", "float", [intstr, ..] -> { - use in <- result.try(result.replace_error( - int.parse(intstr), - "Could not parse number in " <> keyname, - )) - let flstr = in |> int.to_float |> float.to_string - Ok(#("float", [flstr, "float"])) - } - _, "float", "integer", [flstr, ..] -> { - // This is a convertible something, and the conversion required is from float to integer, we can just do that. - use fl <- result.try(result.replace_error( - float.parse(flstr), - "Could not parse number in " <> keyname, - )) - let in = int.to_string(float.truncate(fl)) - Ok(#("integer", [in, "integer"])) - } - _, "string", "bits", [text, ..] -> { - // This is a convertible something, and the conversion required is from string to bitstring, we can just do that. - Ok( - #(configurable_variables.var_bitstring, [ - bit_array.base64_encode(bit_array.from_string(text), True), - configurable_variables.var_bitstring, - ]), - ) - } - - _, "bits", "string", [bits64base, ..] -> { - // This is a convertible something, and the conversion required is from bitstring to string, we can do that if the bitstring is correct. - use bits <- result.try(result.replace_error( - bit_array.base64_decode(bits64base), - "Failed to decode base64 to bitstring for " <> keyname, - )) - use str <- result.try(result.replace_error( - bit_array.to_string(bits), - "Failed to convert bitstring to string for " <> keyname, - )) - Ok( - #(configurable_variables.var_string, [ - str, - configurable_variables.var_string, - ]), - ) - } - // For the other kinds, we don't know how to convert, so again, return as-is - // We won't realistically reach here. - _, _, _, _ -> Ok(#(found_type, probable_value)) - } - - use might_rewrite_the_story <- result.try(might_rewrite_the_story) - // and this is why it _might_ rewrite the story - let #(found_type, probable_value) = might_rewrite_the_story - use <- bool.guard( - { found_type != defined_type }, - Error( - "Expected a " - <> defined_type - <> " at " - <> keyname - <> " but found a " - <> found_type - <> " instead!", - ), - ) - - // Rename keynames - let or_keyname = keyname - let keyname = "config_" <> keyname - - case found_type, probable_value { - "integer", [num, ..] -> { - use integer <- result.try(result.replace_error( - int.parse(num), - "Could not parse number in " <> or_keyname, - )) - - Ok(#(keyname, dynamic.from(integer))) - } - - "float", [num, ..] -> { - use number <- result.try(result.replace_error( - float.parse(num), - "Could not parse number in " <> or_keyname, - )) - - Ok(#(keyname, dynamic.from(number))) - } - - "boolean", [wether, ..] -> { - let b = case wether { - "True" -> Ok(True) - "False" -> Ok(False) - _ -> Error("Could not parse boolean value in " <> or_keyname) - } - use b <- result.try(b) - Ok(#(keyname, dynamic.from(b))) - } - - "bits", [base64, ..] -> { - use bits <- result.try(result.replace_error( - bit_array.base64_decode(base64), - "Could not decode base64 in " <> or_keyname, - )) - Ok(#(keyname, dynamic.from(bits))) - } - - "string", [text, ..] -> { - // Strings or base64 strings are easiest, since they're verbatim - Ok(#(keyname, dynamic.from(text))) - } - - "datetime", _values -> { - // TODO: Implement datetime parsing later (non-blocking for releases) - Error("Datetime decoding not yet implemented in " <> or_keyname) - } - - "date", _values -> { - // TODO: Implement date parsing later (non-blocking for releases) - Error("Date decoding not yet implemented in " <> or_keyname) - } - - "time", values -> { - use new_values <- result.try(result.replace_error( - result.all(list.map(values, int.parse)), - "Could not parse times in " <> or_keyname, - )) - case new_values { - [hours, minutes, seconds, milis] -> { - let c = - dynamic.from(model_type.Time( - hours:, - minutes:, - seconds:, - milis:, - )) - Ok(#(keyname, c)) - } - _ -> Error("Could not parse times in " <> or_keyname) - } - } - _, _ -> - Error("Could not decode configurable variable '" <> or_keyname) - } - }), - ) - use res <- result.try(res) - Ok(dict.from_list(res)) -} - -/// Helper function to compute menus -------------------------------------------------------- -fn compute_menus(content: List(contenttypes.Content), model: Model) { - let menu_s_available = - content - |> list.filter_map(fn(alls) { - case alls.data { - contenttypes.PageData(soms, _) -> Ok(soms) - _ -> Error(Nil) +fn browse_to(url: String) { + use dispatch <- effect.from + case url |> string.starts_with("/") { + // Local! Weird that it'd use this function but glad to catch! + True -> { + case rsvp.parse_relative_uri(url) { + Ok(d) -> dispatch(UserNavigatedTo(d |> parse_route)) + _ -> ffi.browse(url) } - }) - |> list.flatten() - |> list.unique() - |> list.sort(int.compare) - add_each_menu(menu_s_available, model.computed_menus, content) -} - -// This is actually where the real magic happens -fn add_each_menu( - next: List(Int), - gotten: dict.Dict(Int, List(model_type.MenuItem)), - items: List(contenttypes.Content), -) -> dict.Dict(Int, List(model_type.MenuItem)) { - case next { - [] -> gotten - [current_menu, ..rest] -> { - let hits: List(model_type.MenuItem) = - list.filter_map(items, fn(item) -> Result(model_type.MenuItem, Nil) { - case item.data { - contenttypes.PageData(m, _) -> { - case m |> list.contains(current_menu) { - True -> { - Ok(model_type.MenuItem(name: item.title, to: item.permalink)) - } - False -> Error(Nil) - } - } - _ -> Error(Nil) - } - }) - |> list.sort(fn(itema, itemb) { - let a = houdini.escape(utils.js_trim(odysseus.unescape(itema.name))) - let b = houdini.escape(utils.js_trim(odysseus.unescape(itemb.name))) - utils.compare_so_natural(a, b) - }) - dict.insert(gotten, current_menu, hits) - |> add_each_menu(rest, _, items) + } + False -> { + ffi.browse_prompt(url) } } } -@external(javascript, "./cynthia_websites_mini_client/version_ffi.ts", "my_own_version") -pub fn version() -> String +fn view(model: Model) -> Element(Msg) { + let href = href(_, model) + html.a([href(Index)], [element.text("house?")]) +} diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/configtype.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/configtype.gleam deleted file mode 100644 index 47757fc..0000000 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/configtype.gleam +++ /dev/null @@ -1,246 +0,0 @@ -import cynthia_websites_mini_client/contenttypes.{type Content} -import gleam/dict -import gleam/dynamic/decode -import gleam/json -import gleam/list -import gleam/option.{type Option, None, Some} - -pub type CompleteData { - CompleteData( - global_theme: String, - global_theme_dark: String, - global_colour: String, - global_site_name: String, - global_site_description: String, - server_port: Option(Int), - server_host: Option(String), - comment_repo: Option(String), - git_integration: Bool, - crawlable_context: Bool, - sitemap: Option(String), - other_vars: List(#(String, List(String))), - content: List(Content), - ) -} - -pub fn encode_complete_data_for_client(complete_data: CompleteData) -> json.Json { - let CompleteData( - global_theme:, - global_theme_dark:, - global_colour:, - global_site_name:, - global_site_description:, - server_port: _, - server_host: _, - comment_repo:, - git_integration:, - other_vars:, - content:, - crawlable_context:, - sitemap:, - ) = complete_data - json.object([ - #("global_theme", json.string(global_theme)), - #("global_theme_dark", json.string(global_theme_dark)), - #("global_colour", json.string(global_colour)), - #("global_site_name", json.string(global_site_name)), - #("global_site_description", json.string(global_site_description)), - #("git_integration", json.bool(git_integration)), - #("crawlable_context", json.bool(crawlable_context)), - #("sitemap", case sitemap { - None -> json.null() - Some(value) -> json.string(value) - }), - #("comment_repo", case comment_repo { - None -> json.null() - Some(value) -> json.string(value) - }), - #( - "configurable_variables", - json.array(other_vars, fn(item) -> json.Json { - json.object([#(item.0, json.array(item.1, json.string))]) - }), - ), - #("content", json.array(content, contenttypes.encode_content)), - ]) -} - -pub fn complete_data_decoder() -> decode.Decoder(CompleteData) { - use global_theme <- decode.field("global_theme", decode.string) - use global_theme_dark <- decode.field("global_theme_dark", decode.string) - use global_colour <- decode.field("global_colour", decode.string) - use global_site_name <- decode.field("global_site_name", decode.string) - use git_integration <- decode.optional_field( - "git_integration", - default_shared_cynthia_config_global_only.git_integration, - decode.bool, - ) - use global_site_description <- decode.field( - "global_site_description", - decode.string, - ) - use server_port <- decode.optional_field( - "server_port", - None, - decode.optional(decode.int), - ) - use server_host <- decode.optional_field( - "server_host", - None, - decode.optional(decode.string), - ) - use comment_repo <- decode.field( - "comment_repo", - decode.optional(decode.string), - ) - use content <- decode.field( - "content", - decode.list(contenttypes.content_decoder()), - ) - use other_vars <- decode.field("configurable_variables", { - decode.list(decode.dict(decode.string, decode.list(decode.string))) - |> decode.map(list.fold(_, dict.new(), dict.merge)) - }) - - use crawlable_context <- decode.optional_field( - "crawlable_context", - default_shared_cynthia_config_global_only.crawlable_context, - decode.bool, - ) - use sitemap <- decode.optional_field( - "sitemap", - default_shared_cynthia_config_global_only.sitemap, - decode.optional(decode.string), - ) - - let other_vars = dict.to_list(other_vars) - - decode.success(CompleteData( - global_theme:, - global_theme_dark:, - global_colour:, - global_site_name:, - global_site_description:, - server_port:, - server_host:, - comment_repo:, - git_integration:, - crawlable_context:, - sitemap:, - other_vars:, - content:, - )) -} - -pub type SharedCynthiaConfigGlobalOnly { - SharedCynthiaConfigGlobalOnly( - global_theme: String, - global_theme_dark: String, - global_colour: String, - global_site_name: String, - global_site_description: String, - server_port: Option(Int), - server_host: Option(String), - comment_repo: Option(String), - /// [True] - /// Wether or not to enable git integration for the site. - git_integration: Bool, - /// [False] - /// Wether or not to insert json-ld+context into the HTML - /// to make the site crawlable by search engines or readable by LLMs. - crawlable_context: Bool, - /// [True] - /// Wether or not to create a sitemap.xml file for the site. - /// This is useful for search engines to index the site. - /// This is separate from the crawlable_context setting, as no content needs to be rendered or served for the sitemap.xml file. - sitemap: Option(String), - other_vars: List(#(String, List(String))), - ) -} - -pub const default_shared_cynthia_config_global_only: SharedCynthiaConfigGlobalOnly = SharedCynthiaConfigGlobalOnly( - global_theme: "autumn", - global_theme_dark: "night", - global_colour: "#FFFFFF", - global_site_name: "My Site", - global_site_description: "A big site on a mini Cynthia!", - server_port: None, - server_host: None, - comment_repo: None, - git_integration: True, - crawlable_context: False, - sitemap: Some("https://example.com"), - other_vars: [], -) - -pub fn merge( - orig: SharedCynthiaConfigGlobalOnly, - content: List(Content), -) -> CompleteData { - CompleteData( - global_theme: orig.global_theme, - global_theme_dark: orig.global_theme_dark, - global_colour: orig.global_colour, - global_site_name: orig.global_site_name, - global_site_description: orig.global_site_description, - server_port: orig.server_port, - server_host: orig.server_host, - comment_repo: orig.comment_repo, - git_integration: orig.git_integration, - crawlable_context: orig.crawlable_context, - sitemap: orig.sitemap, - other_vars: orig.other_vars, - content:, - ) -} - -pub const ootb_index = "{#hello-world} -# Hello, World - -1. Numbered lists -2. Images: ![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/cindy_dual.gleam similarity index 98% rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/cindy_dual.gleam rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/cindy_dual.gleam index 5460659..d9ddc0b 100644 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/cindy_dual.gleam +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/cindy_dual.gleam @@ -391,7 +391,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { False -> "hover:bg-base-300/50 transition-colors duration-200" } }), - attribute.href(utils.phone_home_url() <> "#" <> current_item.to), + attribute.href(current_item.to), ], [html.text(current_item.name)], ), @@ -423,7 +423,7 @@ pub fn menu_2(from model: model_type.Model) -> List(Element(messages.Msg)) { False -> "hover:bg-base-300/50 transition-colors duration-200" } }), - attribute.href(utils.phone_home_url() <> "#" <> a.to), + attribute.href(a.to), ], [html.text(a.name)], ), diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/cindy_landing.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/cindy_landing.gleam similarity index 100% rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/cindy_landing.gleam rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/cindy_landing.gleam diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/cindy_simple.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/cindy_simple.gleam similarity index 99% rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/cindy_simple.gleam rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/cindy_simple.gleam index 9218f86..dc199d3 100644 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/cindy_simple.gleam +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/cindy_simple.gleam @@ -389,7 +389,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { False -> "hover:bg-base-300/50 transition-colors duration-200" } }), - attribute.href(utils.phone_home_url() <> "#" <> a.to), + attribute.href(a.to), ], [html.text(a.name)], ), diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/documentation.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/documentation.gleam similarity index 98% rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/documentation.gleam rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/documentation.gleam index 32c19d5..508d097 100644 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/documentation.gleam +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/documentation.gleam @@ -429,9 +429,7 @@ fn documentation_common( attribute.class( "flex items-center gap-2 px-4 py-2 rounded-md hover:bg-base-300/50 text-base-content/80 hover:text-base-content", ), - attribute.href( - utils.phone_home_url() <> "#" <> item.to, - ), + attribute.href(item.to), ], [ html.span( @@ -473,9 +471,7 @@ fn documentation_common( attribute.class( "flex items-center gap-2 px-4 py-2 rounded-md hover:bg-base-300/50 text-base-content/80 hover:text-base-content", ), - attribute.href( - utils.phone_home_url() <> "#" <> item.to, - ), + attribute.href(item.to), ], [ html.div( @@ -619,7 +615,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { False -> "flex items-center px-3 py-2 rounded-md hover:bg-base-300/50 text-base-content/80 hover:text-base-content" }), - attribute.href(utils.phone_home_url() <> "#" <> to), + attribute.href(to), event.on_click(messages.UserOnDocumentationLayoutToggleSidebar), ], [ diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/frutiger.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/frutiger.gleam similarity index 99% rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/frutiger.gleam rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/frutiger.gleam index 0fa59f8..fb3a3a9 100644 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/frutiger.gleam +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/frutiger.gleam @@ -229,7 +229,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { "text-base-content/80 hover:bg-base-300/30 border-transparent hover:border-base-content/10" }, ), - attribute.href(utils.phone_home_url() <> "#" <> item.1), + attribute.href(item.1), ], [html.text(item.0)], ), @@ -244,7 +244,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { /// @param content The main content to display /// @param menu Navigation menu items /// @param header Page or post header content -/// @param variables Dictionary with page/post metadata +/// @param variables Dictionary with page/post metadata fn frutiger_common( content: Element(messages.Msg), menu: List(Element(messages.Msg)), diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/github_layout.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/github_layout.gleam similarity index 99% rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/github_layout.gleam rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/github_layout.gleam index 3e90ec9..caf46cb 100644 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/github_layout.gleam +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/github_layout.gleam @@ -549,9 +549,7 @@ fn github_common( "hover:bg-base-200 text-base-content/80" }, ), - attribute.href( - utils.phone_home_url() <> "#" <> item.1, - ), + attribute.href(item.1), event.on_click( messages.UserOnGitHubLayoutToggleMenu, ), @@ -757,7 +755,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { "border-transparent text-base-content/70 hover:border-base-300/60" }, ), - attribute.href(utils.phone_home_url() <> "#" <> item.1), + attribute.href(item.1), ], [ html.div([attribute.class("flex items-center gap-1.5")], [ diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/minimalist.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/minimalist.gleam similarity index 99% rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/minimalist.gleam rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/minimalist.gleam index 3f04bc2..f9b991a 100644 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/minimalist.gleam +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/minimalist.gleam @@ -345,7 +345,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { False -> "text-base-content/70 hover:text-base-content" }, ), - attribute.href(utils.phone_home_url() <> "#" <> item.1), + attribute.href(item.1), ], [html.text(item.0)], ), diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/oceanic_layout.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/oceanic_layout.gleam similarity index 99% rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/oceanic_layout.gleam rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/oceanic_layout.gleam index 9f7162a..bf93751 100644 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/oceanic_layout.gleam +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/oceanic_layout.gleam @@ -522,7 +522,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { True -> "active" False -> "" }), - attribute.href(utils.phone_home_url() <> "#" <> item.1), + attribute.href(item.1), ], [html.text(item.0)], ), @@ -571,7 +571,7 @@ pub fn menu_2(from model: model_type.Model) -> List(Element(messages.Msg)) { False -> "btn btn-sm btn-outline btn-primary" // Outline button for inactive }), - attribute.href(utils.phone_home_url() <> "#" <> item.1), + attribute.href(item.1), ], [html.text(item.0)], ), diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/pastels.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/pastels.gleam similarity index 99% rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/pastels.gleam rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/pastels.gleam index 80a1521..017aeb5 100644 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/pastels.gleam +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/pastels.gleam @@ -224,7 +224,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { False -> "text-base-content/70 hover:bg-base-200/50" }, ), - attribute.href(utils.phone_home_url() <> "#" <> item.1), + attribute.href(item.1), ], [html.text(item.0)], ), @@ -239,7 +239,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { /// @param content The main content to display /// @param menu Navigation menu items /// @param header Page or post header content -/// @param variables Dictionary with page/post metadata +/// @param variables Dictionary with page/post metadata fn pastels_common( content: Element(messages.Msg), menu: List(Element(messages.Msg)), diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/sepia.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/sepia.gleam similarity index 99% rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/sepia.gleam rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/sepia.gleam index 0dfeb9c..5a00ebe 100644 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/sepia.gleam +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/sepia.gleam @@ -246,7 +246,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { "text-base-content/70 hover:text-primary hover:italic" }, ), - attribute.href(utils.phone_home_url() <> "#" <> item.to), + attribute.href(item.to), ], [html.text(item.name)], ), @@ -261,7 +261,7 @@ pub fn menu_1(from model: model_type.Model) -> List(Element(messages.Msg)) { /// @param content The main content to display /// @param menu Navigation menu items /// @param header Page or post header content -/// @param variables Dictionary with page/post metadata +/// @param variables Dictionary with page/post metadata fn sepia_common( content: Element(messages.Msg), menu: List(Element(messages.Msg)), diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/messages.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/messages.gleam deleted file mode 100644 index 2fd1b92..0000000 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/messages.gleam +++ /dev/null @@ -1,15 +0,0 @@ -import cynthia_websites_mini_client/configtype -import rsvp - -/// Msg is the parent type for all the possible messages -/// that can be sent in the client -pub type Msg { - ApiReturnedData(Result(configtype.CompleteData, rsvp.Error)) - UserNavigateTo(String) - UserSearchTerm(String) - UserOnGitHubLayoutToggleMenu - UserOnDocumentationLayoutToggleSidebar - CindyToggleMenu1 - SafeTimePassed - TriggerCheckForHashChange -} diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/model_type.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/model_type.gleam deleted file mode 100644 index fd72589..0000000 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/model_type.gleam +++ /dev/null @@ -1,59 +0,0 @@ -import cynthia_websites_mini_client/configtype -import gleam/dict.{type Dict} -import gleam/dynamic -import gleam/dynamic/decode -import gleam/option.{type Option} -import plinth/javascript/storage - -pub type Model { - Model( - /// Where are we - path: String, - /// Complete data that makes up the site. This is all that the server serves up. - complete_data: Option(configtype.CompleteData), - /// Menu's stored readily for themes to pick up. - /// Structure: - /// Dict(which_menu: Int, List(#(to, from))) - computed_menus: Dict(Int, List(MenuItem)), - /// Status - /// Allows us to trigger the error page from the update function, without the need for more variants of Model. - /// - /// Normally this is `Ok(Nil)` - /// On error this is `Error(error_message: String)` - status: Result(Nil, String), - /// Other variables - /// This stores for example the current search term - other: Dict(String, dynamic.Dynamic), - /// Session storage - sessionstore: storage.Storage, - /// Safe time passed -- equals 200ms after initial load - /// This is to allow for any hash changes that might have happened during the initial load to be caught and acted upon. - /// - /// This replaces the previous `ticks` variable which was a count of ticks (50ms each) since load. Ticks >= 4 was considered safe time passed. - /// Ticks led to unnecessary re-renders, which affected mobile performance negatively. - safetimepassed: Bool, - ) -} - -pub type MenuItem { - MenuItem( - /// The name of the link - name: String, - /// The path to the link - to: String, - ) -} - -/// Configurable variable value type 'Time', can be decoded with `time_decoder` in this same module. -pub type Time { - Time(hours: Int, minutes: Int, seconds: Int, milis: Int) -} - -/// Decodes the configurable variable value type 'Time' -pub fn time_decoder() -> decode.Decoder(Time) { - use hours <- decode.field("hours", decode.int) - use minutes <- decode.field("minutes", decode.int) - use seconds <- decode.field("seconds", decode.int) - use milis <- decode.field("milis", decode.int) - decode.success(Time(hours:, minutes:, seconds:, milis:)) -} diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pageloader/postlistloader.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/postlistloader.gleam similarity index 97% rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/pageloader/postlistloader.gleam rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/postlistloader.gleam index 6c4380b..5151813 100644 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pageloader/postlistloader.gleam +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/postlistloader.gleam @@ -124,7 +124,7 @@ fn postlist_to_html( html.li([attribute.class("list-row p-10")], [ html.a( [ - attribute.href(utils.phone_home_url() <> "#" <> post.permalink), + attribute.href(post.permalink), attribute.class("post__link"), ], [ @@ -165,7 +165,7 @@ fn postlist_to_html( html.li([attribute.class("list-row p-10")], [ html.a( [ - attribute.href(utils.phone_home_url() <> "#" <> page.permalink), + attribute.href(page.permalink), attribute.class("post__link"), ], [ diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery.gleam index 6a17b75..61971b8 100644 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery.gleam +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery.gleam @@ -1,137 +1,25 @@ -import cynthia_websites_mini_client/configtype -import cynthia_websites_mini_client/contenttypes -import cynthia_websites_mini_client/dom -import cynthia_websites_mini_client/messages -import cynthia_websites_mini_client/model_type.{type Model} -import cynthia_websites_mini_client/pottery/djotparse -import cynthia_websites_mini_client/pottery/molds -import cynthia_websites_mini_client/pottery/paints -import cynthia_websites_mini_client/utils -import gleam/dict -import gleam/dynamic -import gleam/list -import gleam/option -import gleam/result -import gleam/string -import lustre/attribute.{attribute} -import lustre/element.{type Element} -import lustre/element/html +// let comment_color_scheme = case dom.get_color_scheme() { +// "dark" -> "github-dark" +// _ -> "github-light" +// } -pub fn render_content( - model: Model, - content: contenttypes.Content, -) -> Element(messages.Msg) { - let assert Ok(def) = paints.get_sytheme(model) - - let #(into, output, variables) = case content.data { - contenttypes.PageData(_, hide_metadata_block) -> { - let mold = case content.layout { - "default" | "theme" | "" -> molds.into(def.layout, "page", model) - layout -> molds.into(layout, "page", model) - } - - let description = - content.description - |> parse_html("descr.dj") - |> element.to_string - let variables = - dict.new() - |> dict.insert("title", content.title |> dynamic.from) - |> dict.insert("description_html", description |> dynamic.from) - |> dict.insert("description", content.description |> dynamic.from) - |> dict.insert( - "hide_metadata_block", - hide_metadata_block |> dynamic.from, - ) - #(mold, parse_html(content.inner_plain, content.filename), variables) - } - contenttypes.PostData(category:, date_published:, date_updated:, tags:) -> { - let mold = case content.layout { - "default" | "theme" | "" -> molds.into(def.layout, "post", model) - layout -> molds.into(layout, "post", model) - } - let description = - content.description - |> parse_html("descr.dj") - |> element.to_string - let variables = - dict.new() - |> dict.insert("title", dynamic.from(content.title)) - |> dict.insert("description_html", description |> dynamic.from) - |> dict.insert("description", content.description |> dynamic.from) - |> dict.insert("date_published", date_published |> dynamic.from) - |> dict.insert("date_modified", date_updated |> dynamic.from) - |> dict.insert("category", category |> dynamic.from) - |> dict.insert("tags", tags |> dynamic.from) - #(mold, parse_html(content.inner_plain, content.filename), variables) - } - } - // Other stuff should be added to vars here, like site metadata, ~menu links~, etc. EDIT: Menu links go in their own thing. - let site_name = - model.complete_data - |> option.map(fn(a) { a.global_site_name }) - |> option.to_result(Nil) - |> result.unwrap("My Site Name") - let considered_output = - { - let default = [output] - case content.data, model { - contenttypes.PostData(..), - model_type.Model( - complete_data: option.Some(configtype.CompleteData( - comment_repo: option.Some(repo), - .., - )), - .., - ) - if repo != "" - -> { - let comment_color_scheme = case dom.get_color_scheme() { - "dark" -> "github-dark" - _ -> "github-light" - } - - list.append(default, [ - html.script( - [ - attribute("async", ""), - attribute("crossorigin", "anonymous"), - attribute("theme", comment_color_scheme), - attribute("issue-term", content.permalink), - attribute("repo", repo), - attribute( - "return-url", - utils.phone_home_url() <> "#" <> model.path, - ), - attribute.src("https://utteranc.es/client.js"), - ], - " -", - ), - ]) - } - _, _ -> default - } - } - |> html.div([attribute.class("contents")], _) - html.div( - [ - { - utils.set_theme_body(def.daisy_ui_theme_name) - attribute("data-theme", def.daisy_ui_theme_name) - }, - attribute.class("contents"), - ], - { - [ - into( - considered_output, - variables |> dict.insert("global_site_name", dynamic.from(site_name)), - ), - ] - }, - ) -} +// list.append(default, [ +// html.script( +// [ +// attribute("async", ""), +// attribute("crossorigin", "anonymous"), +// attribute("theme", comment_color_scheme), +// attribute("issue-term", content.permalink), +// attribute("repo", repo), +// attribute( +// "return-url", +// model.path, +// ), +// attribute.src("https://utteranc.es/client.js"), +// ], +// " +// ", +// ), pub fn parse_html(inner: String, filename: String) -> Element(messages.Msg) { case filename |> string.split(".") |> list.last { @@ -154,9 +42,3 @@ pub fn parse_html(inner: String, filename: String) -> Element(messages.Msg) { ]) } } - -@external(javascript, "./dom.ts", "destroy_comment_box") -pub fn destroy_comment_box() -> Nil - -@external(javascript, "./dom.ts", "apply_styles_to_comment_box") -pub fn comment_box_forced_styles() -> Nil diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/djotparse.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/djotparse.gleam deleted file mode 100644 index 5bc1aba..0000000 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/djotparse.gleam +++ /dev/null @@ -1,1026 +0,0 @@ -//// Djot is parsed using jot, then converted to lustre elements using jotkey/jot_to_lustre's logic, slightly modified to fit Cynthia's needs. - -import cynthia_websites_mini_client/utils -import gleam/dict.{type Dict} -import gleam/int -import gleam/list -import gleam/option.{None, Some} -import gleam/string -import jot.{ - type Container, type Destination, type Document, type Inline, Code, Codeblock, - Emphasis, Heading, Image, Linebreak, Link, Paragraph, Reference, Strong, Text, - Url, -} -import lustre/attribute -import lustre/element.{type Element} -import lustre/element/html - -pub fn entry_to_conversion(djot: String) -> List(Element(msg)) { - let preprocessed = preprocess_djot_extensions(djot) - let parsed = jot.parse(preprocessed) - document_to_lustre(parsed) -} - -pub fn preprocess_djot_extensions(djot: String) -> String { - djot - // Normalize line endings - |> string.replace("\r\n", "\n") - |> string.replace("\r", "\n") - // Convert escaped exclamation marks - |> string.replace("\\!", "!") - // Remove leading/trailing whitespace - |> string.trim() - // - // Now that we've normalized the input, we can preprocess it further - // - // Process heading attributes first to ensure IDs are attached correctly - |> preprocess_heading_attributes - // Fix multiline images - |> preprocess_multiline_images - // Preprocess tables BEFORE autolinks so autolinks don't break table structure - |> preprocess_tables - // Preprocess autolinks - |> preprocess_autolinks - // Preprocess ordered lists - |> preprocess_ordered_lists - // Preprocess blockquotes - |> preprocess_blockquotes - // Preprocess task lists - |> preprocess_task_lists - // And then out it goes! -} - -fn document_to_lustre(document: Document) -> List(Element(msg)) { - let elements = containers_to_lustre(document.content, document.references, []) - - // Add footnotes section if any footnotes exist - let elements_with_footnotes = case dict.size(document.footnotes) > 0 { - True -> { - let footnotes_section = - create_footnotes_section(document.footnotes, document.references) - list.append(elements, [footnotes_section]) - } - False -> elements - } - - list.reverse(elements_with_footnotes) -} - -fn containers_to_lustre( - containers containers: List(Container), - refs refs: Dict(String, String), - elements elements: List(Element(msg)), -) -> List(Element(msg)) { - case containers { - [] -> elements - [container, ..rest] -> { - let elements = container_to_lustre(elements, container, refs) - containers_to_lustre(rest, refs, elements) - } - } -} - -fn container_to_lustre( - elements: List(Element(msg)), - container: Container, - refs: Dict(String, String), -) { - let element = case container { - Paragraph(attrs, inlines) -> { - // Regular paragraph - let in_a_list = case refs |> dict.get("am I in a list?") { - Ok(..) -> True - _ -> False - } - - html.p( - attributes_to_lustre(attrs, [ - case in_a_list { - False -> attribute.class("mb-2") - True -> attribute.class("whitespace-nowrap") - }, - ]), - inlines_to_lustre([], inlines, refs), - ) - } - Heading(attrs, level, inlines) -> { - // Clean heading text to remove {#id} markup - let clean_inlines = clean_heading_text(inlines) - - case level { - 1 -> - html.h1( - attributes_to_lustre(attrs, [ - attribute.class("text-4xl font-bold text-accent"), - ]), - inlines_to_lustre([], clean_inlines, refs), - ) - 2 -> - html.h2( - attributes_to_lustre(attrs, [ - attribute.class("text-3xl font-bold text-accent"), - ]), - inlines_to_lustre([], clean_inlines, refs), - ) - 3 -> - html.h3( - attributes_to_lustre(attrs, [ - attribute.class("text-2xl font-bold text-accent"), - ]), - inlines_to_lustre([], clean_inlines, refs), - ) - 4 -> - html.h4( - attributes_to_lustre(attrs, [ - attribute.class("text-xl font-bold text-accent"), - ]), - inlines_to_lustre([], clean_inlines, refs), - ) - 5 -> - html.h5( - attributes_to_lustre(attrs, [ - attribute.class("text-lg font-bold text-accent"), - ]), - inlines_to_lustre([], clean_inlines, refs), - ) - _ -> - html.h6( - attributes_to_lustre(attrs, [ - attribute.class("font-bold text-accent"), - ]), - inlines_to_lustre([], clean_inlines, refs), - ) - } - } - Codeblock(attrs, language, content) -> { - html.pre( - attributes_to_lustre(attrs, [ - attribute.class( - "bg-neutral text-neutral-content pl-4 block ml-2 mr-2 overflow-x-auto break-none whitespace-pre-wrap font-mono border-dotted border-2 border-neutral-content rounded-lg", - ), - ]), - [ - html.code( - case language { - Some(lang) -> [ - attribute.class("language-" <> lang), - attribute.attribute("data-language", lang), - ] - None -> [] - } - |> list.append([ - attribute.class( - "bg-neutral text-neutral-content p-1 rounded-lg", - ), - ]), - [ - case language { - Some(lang) -> - html.div( - [ - attribute.class( - "text-xs text-neutral-content opacity-70 mb-2", - ), - ], - [html.text(string.uppercase(lang))], - ) - None -> element.none() - }, - html.text(content), - ] - |> list.filter(fn(el) { el != element.none() }), - ), - ], - ) - } - jot.BulletList(layout:, style: _, items:) -> { - html.ul( - [ - attribute.class( - "list-disc" - <> { - " leading-" - <> case layout { - jot.Loose -> "loose" - jot.Tight -> "tight" - } - }, - ), - ], - list.map(items, fn(item) { - html.li( - [], - containers_to_lustre( - containers: item, - refs: refs |> dict.insert("am I in a list?", "yes."), - elements: [], - ), - ) - }), - ) - } - // Raw blocks are perfect for our preprocessed HTML - jot.RawBlock(content:) -> - element.unsafe_raw_html( - "div", - "div", - [attribute.class("djot-processed-content")], - content, - ) - jot.ThematicBreak -> - html.hr([ - attribute.class("w-48 h-1 mx-auto my-4 border-0 rounded-sm md:my-10"), - ]) - } - [element, ..elements] -} - -fn inlines_to_lustre( - elements: List(Element(msg)), - inlines: List(Inline), - refs: Dict(String, String), -) -> List(Element(msg)) { - case inlines { - [] -> list.reverse(elements) - [inline, ..rest] -> { - let new_elements = inline_to_lustre([], inline, refs) - inlines_to_lustre(list.append(new_elements, elements), rest, refs) - } - } -} - -fn inline_to_lustre( - elements: List(Element(msg)), - inline: Inline, - refs: Dict(String, String), -) { - case inline { - Linebreak -> [html.br([])] - Text(text) -> [html.text(text)] - Strong(inlines) -> { - [ - html.strong( - [attribute.class("font-bold")], - inlines_to_lustre(elements, inlines, refs), - ), - ] - } - Emphasis(inlines) -> { - [ - html.em( - [attribute.class("italic")], - inlines_to_lustre(elements, inlines, refs), - ), - ] - } - Link(text, destination) -> { - [ - case destination { - Url(url) -> { - html.a( - [ - attribute.class("text-info underline"), - attribute.href({ - case - string.starts_with(url, "/") - && !string.starts_with(url, utils.phone_home_url() <> "#") - { - True -> utils.phone_home_url() <> "#" <> url - False -> url - } - }), - ], - inlines_to_lustre(elements, text, refs), - ) - } - Reference(ref_id) -> { - case dict.get(refs, ref_id) { - Ok(url) -> { - html.a( - [ - attribute.class("text-info underline"), - attribute.href({ - case - string.starts_with(url, "/") - && !string.starts_with( - url, - utils.phone_home_url() <> "#", - ) - { - True -> utils.phone_home_url() <> "#" <> url - False -> url - } - }), - ], - inlines_to_lustre(elements, text, refs), - ) - } - Error(_) -> { - html.span([attribute.class("text-error")], [ - html.text("[Reference not found: " <> ref_id <> "]"), - ]) - } - } - } - }, - ] - } - Image(text, destination) -> { - [ - html.img([ - attribute.src(destination_attribute(destination, refs)), - attribute.alt(take_inline_text(text, "")), - ]), - ] - } - Code(content) -> { - [ - html.code( - [ - attribute.class( - "bg-neutral text-neutral-content p-1 rounded-lg mt-4 mb-4", - ), - ], - [html.text(content)], - ), - ] - } - jot.Footnote(reference: reference) -> { - [ - html.a( - [ - attribute.href("#fn:" <> reference), - attribute.id("fnref:" <> reference), - attribute.class("text-info text-xs align-super"), - attribute.attribute("role", "doc-noteref"), - ], - [html.text("[" <> reference <> "]")], - ), - ] - } - jot.MathDisplay(content: content) -> { - [ - element.unsafe_raw_html( - "div", - "div", - [attribute.class("math-display my-4 text-center overflow-x-auto")], - "\\[" <> content <> "\\]", - ), - ] - } - jot.MathInline(content: content) -> { - [ - element.unsafe_raw_html( - "span", - "span", - [attribute.class("math-inline")], - "\\(" <> content <> "\\)", - ), - ] - } - jot.NonBreakingSpace -> [html.text(" ")] - } -} - -fn destination_attribute(destination: Destination, refs: Dict(String, String)) { - case destination { - Url(url) -> url - Reference(id) -> - case dict.get(refs, id) { - Ok(url) -> url - Error(Nil) -> "" - } - } -} - -fn take_inline_text(inlines: List(Inline), acc: String) -> String { - case inlines { - [] -> acc - [first, ..rest] -> - case first { - Text(text) | Code(text) -> take_inline_text(rest, acc <> text) - Strong(inlines) | Emphasis(inlines) -> - take_inline_text(list.append(inlines, rest), acc) - Link(nested, _) | Image(nested, _) -> { - let acc = take_inline_text(nested, acc) - take_inline_text(rest, acc) - } - Linebreak -> { - take_inline_text(rest, acc) - } - jot.Footnote(reference: reference) -> "[" <> reference <> "]" - jot.MathDisplay(content: content) -> content - jot.MathInline(content: content) -> content - jot.NonBreakingSpace -> - // Non-breaking space. - " " - } - } -} - -fn attributes_to_lustre(attributes: Dict(String, String), lustre_attributes) { - attributes - |> dict.to_list - |> list.sort(fn(a, b) { string.compare(a.0, b.0) }) - |> list.fold(lustre_attributes, fn(lustre_attributes, pair) { - [attribute.attribute(pair.0, pair.1), ..lustre_attributes] - }) -} - -fn create_footnotes_section( - footnotes: Dict(String, List(Container)), - refs: Dict(String, String), -) -> Element(msg) { - case dict.size(footnotes) > 0 { - True -> { - html.section( - [attribute.class("footnotes mt-8 pt-4 border-t border-neutral-content")], - [ - html.h2([attribute.class("text-xl font-bold text-accent mb-4")], [ - html.text("Footnotes"), - ]), - html.ol( - [attribute.class("list-decimal list-inside space-y-2")], - footnotes - |> dict.to_list - |> list.map(fn(footnote) { - let #(id, containers) = footnote - html.li( - [attribute.id("fn:" <> id), attribute.class("text-sm")], - list.append(containers_to_lustre(containers, refs, []), [ - html.a( - [ - attribute.href("#fnref:" <> id), - attribute.class("text-info ml-2"), - attribute.attribute("role", "doc-backlink"), - ], - [html.text("↩")], - ), - ]), - ) - }), - ), - ], - ) - } - False -> element.none() - } -} - -fn preprocess_tables(djot: String) -> String { - let lines = string.split(djot, "\n") - process_table_lines(lines, False, []) - |> string.join("\n") -} - -fn process_table_lines( - lines: List(String), - in_table: Bool, - table_buffer: List(String), -) -> List(String) { - case lines { - [] -> - case in_table { - True -> [convert_table_to_raw(list.reverse(table_buffer))] - False -> [] - } - - [line, ..rest] -> { - let trimmed = string.trim(line) - let is_table_line = string.contains(line, "|") && trimmed != "" - let is_separator = - string.contains(line, "|") && string.contains(line, "-") - - case in_table, is_table_line || is_separator { - True, True -> process_table_lines(rest, True, [line, ..table_buffer]) - - True, False -> { - // End of table - process accumulated buffer - case list.reverse(table_buffer) { - [] -> [line, ..process_table_lines(rest, False, [])] - table_lines -> [ - convert_table_to_raw(table_lines), - line, - ..process_table_lines(rest, False, []) - ] - } - } - - False, True -> { - // Start of new table - process_table_lines(rest, True, [line]) - } - - False, False -> [line, ..process_table_lines(rest, False, [])] - } - } - } -} - -fn convert_table_to_raw(lines: List(String)) -> String { - case lines { - [] -> "" - [single_line] -> single_line - lines -> { - // Find the separator line (contains both | and - and looks like a separator) - let separator_index = - list.index_fold(lines, None, fn(acc, line, index) { - case acc { - Some(_) -> acc - None -> { - let trimmed = string.trim(line) - let has_pipes = string.contains(line, "|") - let has_dashes = string.contains(line, "-") - // A separator should be mostly dashes and pipes with minimal other content - let is_likely_separator = - has_pipes - && has_dashes - && { - trimmed - |> string.to_graphemes - |> list.all(fn(char) { - char == "|" || char == "-" || char == " " || char == ":" - }) - } - - case is_likely_separator { - True -> Some(index) - False -> None - } - } - } - }) - - case separator_index { - None -> string.join(lines, "\n") - // No valid separator found - Some(sep_index) -> { - // Split into header, separator, and rows - let header_lines = list.take(lines, sep_index) - let remaining = list.drop(lines, sep_index + 1) - - case header_lines { - [] -> string.join(lines, "\n") - // No header - _ -> { - // Use the last header line if there are multiple - let actual_header = case list.reverse(header_lines) { - [last_header, ..] -> last_header - [] -> "" - // Should not happen since header_lines is not empty - } - - let header_cells = - actual_header - |> string.split("|") - |> list.map(string.trim) - |> list.filter(fn(cell) { cell != "" }) - - // Validate that we have at least some header cells - case list.length(header_cells) { - 0 -> string.join(lines, "\n") - // No valid header cells - _ -> { - let data_rows = - remaining - |> list.map(fn(row) { - row - |> string.split("|") - |> list.map(string.trim) - |> list.filter(fn(cell) { cell != "" }) - }) - |> list.filter(fn(row) { list.length(row) > 0 }) - - let header_elements = { - list.map(header_cells, fn(cell) { - html.th( - [attribute.class("px-4 py-2 text-left font-bold")], - entry_to_conversion(cell), - ) - }) - } - let row_elements = { - list.map(data_rows, fn(row) { - html.tr([], { - list.map(row, fn(cell) { - html.td( - [ - attribute.class( - "px-4 py-2 border-t border-neutral-content", - ), - ], - entry_to_conversion(cell), - ) - }) - }) - }) - } - - html.table( - [ - attribute.class( - "table table-zebra w-full my-4 border border-neutral-content", - ), - ], - [ - html.thead( - [attribute.class("bg-neutral text-neutral-content")], - [html.tr([], header_elements)], - ), - html.tbody([], row_elements), - ], - ) - |> element_to_raw_djotstring - } - } - } - } - } - } - } - } -} - -fn preprocess_blockquotes(djot: String) -> String { - // Process blockquotes as groups, not individual lines - let lines = string.split(djot, "\n") - process_blockquote_lines(lines, [], [], False) - |> string.join("\n") -} - -fn process_blockquote_lines( - lines: List(String), - processed: List(String), - blockquote_buffer: List(String), - in_blockquote: Bool, -) -> List(String) { - case lines { - [] -> { - // If we have a blockquote buffer at the end, process it - case in_blockquote { - True -> { - let blockquote = - convert_blockquote_to_raw(list.reverse(blockquote_buffer)) - list.reverse([blockquote, ..processed]) - } - False -> list.reverse(processed) - } - } - - [line, ..rest] -> { - let trimmed = string.trim(line) - let is_blockquote_line = string.starts_with(trimmed, "> ") - - case in_blockquote, is_blockquote_line { - // Continue collecting blockquote lines - True, True -> { - let content = string.drop_start(trimmed, 2) |> string.trim() - process_blockquote_lines( - rest, - processed, - [content, ..blockquote_buffer], - True, - ) - } - - // End of blockquote - True, False -> { - let blockquote = - convert_blockquote_to_raw(list.reverse(blockquote_buffer)) - process_blockquote_lines(rest, [blockquote, ..processed], [], False) - } - - // Start of blockquote - False, True -> { - let content = string.drop_start(trimmed, 2) |> string.trim() - process_blockquote_lines(rest, processed, [content], True) - } - - // Regular line, not in blockquote - False, False -> { - process_blockquote_lines(rest, [line, ..processed], [], False) - } - } - } - } -} - -fn convert_blockquote_to_raw(lines: List(String)) -> String { - let content = - lines - |> list.map(fn(line) { - case line { - // Any line without content should be an empty line broken. - "" -> "\n" - " " -> "\n" - "\\" -> "" - _ -> line - } - }) - |> string.join("\n") - - html.blockquote( - [ - attribute.class( - "border-l-4 border-accent border-dotted pl-4 bg-secondary bg-opacity-10 mb-4 mt-4", - ), - ], - entry_to_conversion(content), - ) - |> element_to_raw_djotstring -} - -/// Converts a Lustre element to a raw block Djot representation. -fn element_to_raw_djotstring(elm: element.Element(a)) { - "\n```=html\n" <> { elm |> element.to_string } <> "\n```\n" -} - -fn preprocess_task_lists(djot: String) -> String { - djot - |> string.split("\n") - |> list.map(fn(line) { - let trimmed = string.trim(line) - case string.starts_with(trimmed, "- [ ] ") { - True -> { - let content = string.drop_start(trimmed, 6) - { - html.div([attribute.class("flex items-center mb-2")], [ - html.input([ - attribute.type_("checkbox"), - attribute.disabled(True), - attribute.class("mr-2 accent-primary"), - ]), - html.span([], entry_to_conversion(content)), - ]) - } - |> element_to_raw_djotstring - } - False -> - case - string.starts_with(trimmed, "- [x] ") - || string.starts_with(trimmed, "- [X] ") - { - True -> { - let content = string.drop_start(trimmed, 6) - { - html.div([attribute.class("flex items-center mb-2")], [ - html.input([ - attribute.type_("checkbox"), - attribute.checked(True), - attribute.disabled(True), - attribute.class("mr-2 accent-primary"), - ]), - html.span([], entry_to_conversion(content)), - ]) - } - |> element_to_raw_djotstring - } - False -> line - } - } - }) - |> string.join("\n") -} - -fn preprocess_autolinks(djot: String) -> String { - // Convert to [url](url) format for proper Djot parsing - djot - |> string.replace(" string.replace(" string.replace(" string.replace(" process_autolink_markers() -} - -fn process_autolink_markers(input: String) -> String { - case string.split_once(input, "🔗AUTOLINK🔗") { - Ok(#(before, after)) -> { - case string.split_once(after, ">") { - Ok(#(url, rest)) -> - before - <> "[" - <> url - <> "](" - <> url - <> ")" - <> process_autolink_markers(rest) - Error(_) -> before <> "<" <> after - // Restore if no closing > - } - } - Error(_) -> input - // No markers found - } -} - -fn preprocess_ordered_lists(djot: String) -> String { - let lines = string.split(djot, "\n") - process_ordered_list_lines(lines, False, []) - |> string.join("\n") -} - -fn process_ordered_list_lines( - lines: List(String), - in_list: Bool, - list_buffer: List(String), -) -> List(String) { - case lines { - [] -> - case in_list { - True -> [convert_ordered_list_to_raw(list.reverse(list_buffer))] - False -> [] - } - - [line, ..rest] -> { - let trimmed = string.trim(line) - let is_list_item = is_ordered_list_item(trimmed) - - case in_list, is_list_item { - True, True -> - process_ordered_list_lines(rest, True, [line, ..list_buffer]) - - True, False -> [ - convert_ordered_list_to_raw(list.reverse(list_buffer)), - line, - ..process_ordered_list_lines(rest, False, []) - ] - - False, True -> process_ordered_list_lines(rest, True, [line]) - - False, False -> [line, ..process_ordered_list_lines(rest, False, [])] - } - } - } -} - -fn is_ordered_list_item(line: String) -> Bool { - case string.split_once(line, ". ") { - Ok(#(num_str, _)) -> { - case int.parse(string.trim(num_str)) { - Ok(_) -> True - Error(_) -> False - } - } - Error(_) -> False - } -} - -fn convert_ordered_list_to_raw(lines: List(String)) -> String { - case lines { - [] -> "" - _ -> { - let list_items = - lines - |> list.map(fn(line) { - case string.split_once(string.trim(line), ". ") { - Ok(#(_, content)) -> html.li([], entry_to_conversion(content)) - Error(_) -> html.li([], [html.text(line)]) - } - }) - - html.ol([attribute.class("list-decimal mb-4")], list_items) - |> element_to_raw_djotstring - } - } -} - -fn preprocess_multiline_images(djot: String) -> String { - string.split(djot, "\n") - |> process_multiline_image_lines([], "") - |> string.join("\n") -} - -fn process_multiline_image_lines( - lines: List(String), - processed: List(String), - buffer: String, -) -> List(String) { - case lines { - [] -> list.reverse(processed) - - [line, ..rest] -> { - let has_image_start = string.contains(line, "![") - let has_image_end = string.contains(line, ")") - let has_incomplete_image = has_image_start && !has_image_end - - case buffer { - // No buffer yet - "" -> { - case has_incomplete_image { - // Start collecting a multiline image - True -> process_multiline_image_lines(rest, processed, line) - - // Normal line - False -> - process_multiline_image_lines(rest, [line, ..processed], "") - } - } - - // Continue collecting an existing multiline image - _ -> { - // If we have a buffer, we're in the middle of processing a multiline image - // We need to combine all lines until we find the closing parenthesis - case has_image_end { - // Complete the image and add to processed - True -> { - let complete_line = buffer <> " " <> line - process_multiline_image_lines( - rest, - [complete_line, ..processed], - "", - ) - } - // Continue buffering - False -> { - process_multiline_image_lines( - rest, - processed, - buffer <> " " <> line, - ) - } - } - } - } - } - } -} - -// Process heading attributes like {#id} before headings -fn preprocess_heading_attributes(djot: String) -> String { - let lines = string.split(djot, "\n") - let processed = process_heading_attribute_lines(lines, []) - string.join(processed, "\n") -} - -fn process_heading_attribute_lines( - lines: List(String), - processed: List(String), -) -> List(String) { - case lines { - [] -> list.reverse(processed) - - [line, ..rest] -> { - // Check for attribute pattern {#something} followed by heading - let is_attribute = - string.starts_with(string.trim(line), "{#") - && string.contains(line, "}") - - case is_attribute, rest { - True, [next, ..next_rest] -> { - let next_trimmed = string.trim(next) - // Check if next line is a heading - case - string.starts_with(next_trimmed, "# ") - || string.starts_with(next_trimmed, "## ") - || string.starts_with(next_trimmed, "### ") - || string.starts_with(next_trimmed, "#### ") - || string.starts_with(next_trimmed, "##### ") - || string.starts_with(next_trimmed, "###### ") - { - True -> { - // Extract ID from {#id} - case string.split_once(line, "{#") { - Ok(#(_, with_id)) -> { - case string.split_once(with_id, "}") { - Ok(#(id, _)) -> { - let modified_heading = next <> " {#" <> id <> "}" - process_heading_attribute_lines(next_rest, [ - modified_heading, - ..processed - ]) - } - Error(_) -> - process_heading_attribute_lines(rest, [line, ..processed]) - } - } - Error(_) -> - process_heading_attribute_lines(rest, [line, ..processed]) - } - } - - False -> process_heading_attribute_lines(rest, [line, ..processed]) - } - } - - _, _ -> process_heading_attribute_lines(rest, [line, ..processed]) - } - } - } -} - -// Clean heading text by removing any {#id} attributes -fn clean_heading_text(inlines: List(Inline)) -> List(Inline) { - inlines - |> list.map(fn(inline) { - case inline { - Text(text) -> { - // Remove {#id} pattern from the text - case string.split_once(text, " {#") { - Ok(#(content, _)) -> Text(content) - Error(_) -> inline - } - } - _ -> inline - } - }) -} diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/ownit.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/ownit.gleam deleted file mode 100644 index a341bba..0000000 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/ownit.gleam +++ /dev/null @@ -1,243 +0,0 @@ -//// # Ownit layout -//// -//// Custom layout for Cynthia Mini. -//// Allows to create own templates in Handlebars. -//// -//// Ownit is a unique layout in the sense that, it does not contain a layout, it's merely a wrap around Handlebars to allow own templates to be used in Cynthia Mini. -//// -//// ## Writing templates for ownit -//// -//// Writing templates for ownit can be done in the [Handlebars](https://handlebarsjs.com/) language. -//// Your template should be stored under `[variables] -> ownit_template` as a `"string"` or as a `{ path = "filename.hbs" }` or `{ url = "some-site.com/name.hbs" }` url. -//// -//// ### Available context variables: -//// -//// - `body`: Contains the content body, for example the text from your blog post. -//// etc: More to come! - -import cynthia_websites_mini_client/messages -import cynthia_websites_mini_client/model_type -import cynthia_websites_mini_client/pottery/oven -import gleam/dict.{type Dict} -import gleam/dynamic -import gleam/dynamic/decode.{type Dynamic} -import gleam/javascript/array.{type Array} -import gleam/list -import gleam/option.{None} -import gleam/result -import lustre/element.{type Element} - -pub fn main( - from content: Element(messages.Msg), - with variables: Dict(String, Dynamic), - store model: model_type.Model, - is_post is_post: Bool, -) { - let #( - title, - description, - site_name, - category, - date_modified, - date_published, - tags, - ): #(String, String, String, String, String, String, Array(String)) = case - is_post - { - True -> { - let assert Ok(title) = - dict.get(variables, "title") - |> result.unwrap(dynamic.from(None)) - |> decode.run(decode.string) - let assert Ok(description) = - dict.get(variables, "description_html") - |> result.unwrap(dynamic.from(None)) - |> decode.run(decode.string) - let assert Ok(site_name) = - dict.get(variables, "global_site_name") - |> result.unwrap(dynamic.from(None)) - |> decode.run(decode.string) - let assert Ok(category) = - dict.get(variables, "category") - |> result.unwrap(dynamic.from(None)) - |> decode.run(decode.string) - let assert Ok(date_modified) = - dict.get(variables, "date_modified") - |> result.unwrap(dynamic.from(None)) - |> decode.run(decode.string) - let assert Ok(date_published) = - dict.get(variables, "date_published") - |> result.unwrap(dynamic.from(None)) - |> decode.run(decode.string) - let assert Ok(tags) = - dict.get(variables, "tags") - |> result.unwrap(dynamic.from([])) - |> decode.run(decode.list(decode.string)) - let tags = tags |> array.from_list - #( - title, - description, - site_name, - category, - date_modified, - date_published, - tags, - ) - } - False -> { - let assert Ok(title) = - dict.get(variables, "title") - |> result.unwrap(dynamic.from(None)) - |> decode.run(decode.string) - let assert Ok(description) = - dict.get(variables, "description_html") - |> result.unwrap(dynamic.from(None)) - |> decode.run(decode.string) - let assert Ok(site_name) = - dict.get(variables, "global_site_name") - |> result.unwrap(dynamic.from(None)) - |> decode.run(decode.string) - let category = "" - let date_modified = "" - let date_published = "" - let tags = [] |> array.from_list - #( - title, - description, - site_name, - category, - date_modified, - date_published, - tags, - ) - } - } - - let menu_map = fn(item: model_type.MenuItem) { - let to = case item.to { - "/" <> _ -> { - // If the link starts with a slash, we assume it's a local link. - "#" <> item.to - } - "!" <> _ -> { - // If the link starts with an exclamation mark, we assume it's a local link. - "#" <> item.to - } - _ -> { - // Otherwise, we keep the link as is. - item.to - } - } - - [item.name, to] |> array.from_list - } - - let menu_1_items = { - dict.get(model.computed_menus, 1) - |> result.unwrap([]) - |> list.map(menu_map) - |> array.from_list - } - let menu_2_items = { - dict.get(model.computed_menus, 2) - |> result.unwrap([]) - |> list.map(menu_map) - |> array.from_list - } - let menu_3_items = { - dict.get(model.computed_menus, 3) - |> result.unwrap([]) - |> list.map(menu_map) - |> array.from_list - } - - case get_template(model) { - Ok(template) -> { - case - { - OwnitCtx( - content: content |> element.to_string(), - is_post:, - title:, - description:, - site_name:, - category:, - date_modified:, - date_published:, - tags:, - menu_1_items:, - menu_2_items:, - menu_3_items:, - ) - |> context_into_template_run(template, _) - } - { - Ok(html_) -> element.unsafe_raw_html("div", "div", [], html_) - Error(_) -> - oven.error( - "Could not parse context into the Handlebars template from the configurated variable at 'ownit_template'.", - recoverable: True, - ) - } - } - Error(error_message) -> { - oven.error(error_message, recoverable: False) - } - } -} - -fn get_template(model: model_type.Model) { - use template_string_dynamic <- result.try(result.replace_error( - dict.get(model.other, "config_ownit_template"), - "An error occurred while loading the Handlebars template from the configurated variable at 'ownit_template'.", - )) - use template_string <- result.try(result.replace_error( - decode.run(template_string_dynamic, decode.string), - "An error occurred while trying to decode the Handlebars template from the configurated variable at 'ownit_template'.", - )) - compile_template_string(template_string) - |> result.replace_error( - "Could not compile the Handlebars template from the configurated variable at 'ownit_template'.", - ) -} - -/// Context sent into Handlebars template, obviously needs to be generated first. Is translated into an Ecmascript object by FFI. -type OwnitCtx { - OwnitCtx( - /// JS: string - content: String, - /// JS: boolean - is_post: Bool, - /// JS: string - title: String, - /// JS: string - description: String, - /// JS: string - site_name: String, - /// JS: string - category: String, - /// JS: string - date_modified: String, - /// JS: string - date_published: String, - /// JS: string[] - tags: Array(String), - /// JS: [string, string][] - menu_1_items: Array(Array(String)), - /// JS: [string, string][] - menu_2_items: Array(Array(String)), - /// JS: [string, string][] - menu_3_items: Array(Array(String)), - ) -} - -@external(javascript, "./ownit_ffi", "compile_template_string") -fn compile_template_string(in: String) -> Result(CompiledTemplate, Nil) - -type CompiledTemplate - -@external(javascript, "./ownit_ffi", "context_into_template_run") -fn context_into_template_run( - template: CompiledTemplate, - context: OwnitCtx, -) -> Result(String, Nil) diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/ownit_ffi.ts b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/ownit_ffi.ts deleted file mode 100644 index 00e201e..0000000 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/ownit_ffi.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Ownit layout FFI module -// Ownit is the only layout that has it's own FFI implementations, since Gleam doesn't have any direct bindings to Handlebars.js -// And well, I'd like to support full Handlebars :shrug: - -import Handlebars from "handlebars"; -import { Ok, Error } from "../../../../prelude"; - -export function compile_template_string(template_string: string) { - try { - return new Ok(Handlebars.compile(template_string)); - } catch (e) { - console.error("Error while compiling Handlebars template string:", e); - return new Error(null); - } -} - -export function context_into_template_run( - template: HandlebarsTemplateDelegate, - ctx_record: any, -) { - const ctx = turn_gleam_record_into_js_object(ctx_record); - try { - return new Ok(template(ctx)); - } catch (e) { - console.error("Error while running Handlebars template with context:", e); - return new Error(null); - } -} - -interface context { - body: string; - is_post: boolean; - title: string; - description: string; - site_name: string; - category: string; - date_modified: string; - date_published: string; - tags: string[]; - menu_1_items: [string, string][]; - menu_2_items: [string, string][]; - menu_3_items: [string, string][]; -} - -function turn_gleam_record_into_js_object(record: any): context { - return { - body: record.content, - is_post: record.is_post, - title: record.title, - description: record.description, - site_name: record.site_name, - category: record.category, - date_modified: record.date_modified, - date_published: record.date_published, - tags: record.tags, - menu_1_items: record.menu_1_items || [], - menu_2_items: record.menu_2_items || [], - menu_3_items: record.menu_3_items || [], - }; -} diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/jsonld.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/jsonld.gleam- similarity index 100% rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/jsonld.gleam rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/jsonld.gleam- diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/sitemap.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/sitemap.gleam- similarity index 100% rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/sitemap.gleam rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/sitemap.gleam- diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/ui.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/ui.gleam deleted file mode 100644 index d9e4b73..0000000 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/ui.gleam +++ /dev/null @@ -1,19 +0,0 @@ -pub const footer = "Made into this website with Cynthia Mini" - -/// The entire of the 404 page. -pub fn notfoundbody() -> String { - "
-
-
-

404!

-

Uh-oh, that page cannot be found.

-
- - -
-
-
-
- " - <> footer -} diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/utils.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/utils.gleam deleted file mode 100644 index 9255da7..0000000 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/utils.gleam +++ /dev/null @@ -1,82 +0,0 @@ -import gleam/float -import gleam/http -import gleam/http/request.{type Request} -import gleam/order -import gleam/string -import gleam/time/timestamp -import plinth/browser/window - -pub fn phone_home() -> Request(String) { - request.new() - |> request.set_scheme({ - let origin = window.origin() - case origin { - "http://" <> _ -> http.Http - "https://" <> _ -> http.Https - _ -> http.Https - } - }) - |> request.set_host(get_window_host()) -} - -pub fn phone_home_url() -> String { - let origin = window.origin() - let host = get_window_host() - - case origin { - "http://" <> _ -> "http://" <> host - "https://" <> _ -> "https://" <> host - _ -> "https://" <> host - } - <> window.pathname() - |> phone_home_lessener -} - -fn phone_home_lessener(in: String) -> String { - case string.ends_with(in, "//") { - True -> phone_home_lessener(in |> string.drop_end(1)) - False -> - case string.ends_with(in, "index.html") { - True -> phone_home_lessener(in |> string.replace("index.html", "")) - False -> - case string.ends_with(in, "index.html/") { - True -> phone_home_lessener(in |> string.replace("index.html", "")) - False -> in - } - } - } -} - -@external(javascript, "./utils_ffi.ts", "getWindowHost") -pub fn get_window_host() -> String - -pub fn now() -> Int { - let now = - timestamp.system_time() - |> timestamp.to_unix_seconds - |> float.truncate - now -} - -/// A natural compare -pub fn compare_so_natural(a: String, b: String) -> order.Order { - case compares(a, b) { - "eq" -> order.Eq - "lt" -> order.Lt - "gt" -> order.Gt - _ -> panic as "compare_so_natural failed?? This should never happen" - } -} - -@external(javascript, "./utils_ffi.ts", "compares") -fn compares(a: String, b: String) -> String - -/// A js FFI implementation of string.trim, to also handle stuff like nbsp or emsp -@external(javascript, "./utils_ffi.ts", "trims") -pub fn js_trim(a: String) -> String - -@external(javascript, "./utils_ffi.ts", "set_theme_body") -pub fn set_theme_body(themename: String) -> Nil - -@external(javascript, "./utils_ffi.ts", "whatever_timestamp_to_unix_millis") -pub fn whatever_timestamp_to_unix_millis(ts: String) -> Int diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/utils_ffi.ts b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/utils_ffi.ts deleted file mode 100644 index 686df06..0000000 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/utils_ffi.ts +++ /dev/null @@ -1,39 +0,0 @@ -export function getWindowHost() { - return window.location.host; -} - -export function compares(a: string, b: string): string { - const result = a.localeCompare(b, undefined, { numeric: true }); - if (result < 0) { - return "lt"; - } else if (result > 0) { - return "gt"; - } else { - return "eq"; - } -} - -export function trims(str: string) { - return str.trim(); -} - -export function set_theme_body(themename: string) { - document.body.setAttribute("data-theme", themename); -} - -export function whatever_timestamp_to_unix_millis(ts: string | number): number { - if (typeof ts === "number") { - // assume it's already unix millis - return ts; - } else if (typeof ts === "string") { - // try to parse as ISO 8601 string - const parsed = Date.parse(ts); - if (!isNaN(parsed)) { - return parsed; - } else { - return 0; - } - } else { - return 0; - } -} diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/version_ffi.ts b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/version_ffi.ts deleted file mode 100644 index d403743..0000000 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/version_ffi.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { version } from "package.json"; -export function my_own_version(): string { - return version; -} diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/view.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/view.gleam deleted file mode 100644 index 625e402..0000000 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/view.gleam +++ /dev/null @@ -1,154 +0,0 @@ -import cynthia_websites_mini_client/contenttypes -import cynthia_websites_mini_client/dom -import cynthia_websites_mini_client/messages.{type Msg} -import cynthia_websites_mini_client/model_type.{type Model} -import cynthia_websites_mini_client/pageloader/postlistloader -import cynthia_websites_mini_client/pottery -import cynthia_websites_mini_client/pottery/oven -import cynthia_websites_mini_client/utils -import gleam/list -import gleam/option.{None, Some} -import gleam/result -import houdini -import lustre/attribute -import lustre/element.{type Element} -import lustre/element/html -import odysseus - -pub fn main(model: Model) -> Element(Msg) { - case model.status { - Ok(_) -> { - case model.complete_data { - None -> initial_view() - Some(complete_data) -> { - let content = - complete_data.content - |> list.find(fn(content) { { content.permalink == model.path } }) - |> result.lazy_unwrap(fn() { - contenttypes.Content( - filename: "notfound.dj", - title: "Page not found", - description: model.path, - layout: "theme", - permalink: "404", - inner_plain: "# 404!\n\nThe page you are looking for does not exist.", - data: contenttypes.PageData([], True), - ) - }) - let content = case model.path { - "!" <> a -> { - let #(tit, desc) = case content.filename { - "notfound.dj" -> #(None, None) - _ -> #(Some(content.title), Some(content.description)) - } - case a { - "/category/" <> category -> { - let title = - option.unwrap(tit, "Posts in category: " <> category) - let description = - option.unwrap( - desc, - "A postlist of all posts in the category: " <> category, - ) - - contenttypes.Content( - title:, - description:, - layout: "default", - permalink: model.path, - filename: "postlist.html", - data: contenttypes.PageData([], False), - inner_plain: postlistloader.postlist_by_category( - model, - category, - ) - |> element.to_string, - ) - } - "/tag/" <> tag -> { - let title = option.unwrap(tit, "Posts with tag: " <> tag) - let description = - option.unwrap( - desc, - "A postlist of all posts tagged with " <> tag, - ) - contenttypes.Content( - title:, - description:, - layout: "default", - permalink: model.path, - filename: "postlist.html", - data: contenttypes.PageData([], True), - inner_plain: postlistloader.postlist_by_tag(model, tag) - |> element.to_string, - ) - } - "/search/" <> search_term -> { - let title = - option.unwrap(tit, "Search results for: " <> search_term) - let description = option.unwrap(desc, "") - contenttypes.Content( - title:, - description:, - layout: "default", - permalink: model.path, - filename: "postlist.html", - data: contenttypes.PageData([], False), - inner_plain: postlistloader.postlist_by_search_term( - model, - search_term, - ) - |> element.to_string, - ) - } - _ -> { - let title = option.unwrap(tit, "All posts") - let description = "A postlist of all posts." - contenttypes.Content( - title:, - description:, - layout: "default", - permalink: model.path, - filename: "postlist.html", - data: contenttypes.PageData([], False), - inner_plain: postlistloader.postlist_all(model) - |> element.to_string, - ) - } - } - } - _ -> content - } - let assert Ok(_) = - dom.push_title( - houdini.escape(utils.js_trim(odysseus.unescape(content.title))), - ) - pottery.render_content(model, content) - } - } - } - Error(error_message) -> oven.error(error_message, True) - } -} - -pub fn initial_view() -> Element(Msg) { - let assert Ok(_) = dom.push_title("Cynthia Mini: Loading...") - html.div( - [ - attribute.class( - "absolute mr-auto ml-auto right-0 left-0 bottom-[40VH] top-[40VH] w-fit h-fit", - ), - ], - [ - html.div([attribute.class("card bg-primary text-primary-content w-96")], [ - html.div([attribute.class("card-body")], [ - html.h2([attribute.class("card-title")], [html.text("Cynthia Mini")]), - html.p([], [html.text("Loading the page you want...")]), - html.div([attribute.class("card-actions justify-end")], [ - html.span([attribute.class("loading loading-bars loading-lg")], []), - ]), - ]), - ]), - ], - ) -} diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/site_json.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/site_json.gleam new file mode 100644 index 0000000..4893ad1 --- /dev/null +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/site_json.gleam @@ -0,0 +1,140 @@ +//// Site.json gleam type format and en/decoder. + +import cynthia_websites_mini_shared/config/v4_1 +import cynthia_websites_mini_shared/config/v4_1/decodes +import gleam/dict +import gleam/dynamic/decode +import gleam/option + +/// This is the content of site.json, factually the entire site +pub type SiteJSON { + SiteJSON( + config: v4_1.V4p1Mini, + // Slug, or a random number if not set and the content + content: dict.Dict(String, Content), + ) +} + +pub fn site_json_decoder() -> decode.Decoder(SiteJSON) { + use config <- decode.field("config", decodes.v4p1_mini_dynamic()) + use content: dict.Dict(String, Content) <- decode.field( + "content", + decode.dict(decode.string, { + use variant <- decode.field("type", decode.string) + case variant { + "page" -> { + use title <- decode.field("title", decode.string) + use description <- decode.field("description", decode.string) + use layout <- decode.field("layout", decode.optional(decode.string)) + use content <- decode.field("content", decode.string) + use in_menus <- decode.field("in_menus", decode.list(decode.int)) + use hide_meta_block <- decode.field("hide_meta_block", decode.bool) + decode.success(Page( + title:, + description:, + layout:, + content:, + in_menus:, + hide_meta_block:, + )) + } + "post" -> { + use title <- decode.field("title", decode.string) + use description <- decode.field("description", decode.string) + use layout <- decode.field("layout", decode.optional(decode.string)) + use content <- decode.field("content", decode.string) + use date_published <- decode.field("date_published", decode.string) + use date_updated <- decode.field("date_updated", decode.string) + use category <- decode.field("category", decode.string) + use tags <- decode.field("tags", decode.list(decode.string)) + use mastodon_comments <- field_or( + field: "mastodon-comments", + decoder: decode.optional({ + use instance <- decode.field("instance", decode.string) + use id <- decode.field("id", decode.string) + decode.success(MastodonStatus(instance:, id:)) + }), + otherwise: option.None, + ) + + decode.success(Post( + title:, + description:, + layout:, + content:, + date_published:, + date_updated:, + category:, + tags:, + mastodon_comments:, + )) + } + _ -> + decode.failure( + Page("failure", "failure", option.None, "Failure", [], False), + "Content", + ) + } + }), + ) + decode.success(SiteJSON(config:, content:)) +} + +fn field_or( + field field: String, + decoder field_decoder: decode.Decoder(t), + otherwise default: t, + next next: fn(t) -> decode.Decoder(final), +) -> decode.Decoder(final) { + use val <- decode.optional_field( + field, + option.None, + decode.optional(field_decoder), + ) + next(val |> option.unwrap(default)) +} + +pub type Content { + Page( + /// Page title + title: String, + /// Description, converted to HTML beforehand. + description: String, + /// Layout or default + layout: option.Option(String), + /// Page content, converted to HTML beforehand. + content: String, + /// In which menus this page should appear + in_menus: List(Int), + /// Hide the block with title and description for a page. + hide_meta_block: Bool, + ) + Post( + /// Page title + title: String, + /// Description, converted to HTML beforehand. + description: String, + /// Layout or default + layout: option.Option(String), + /// Page content, converted to HTML beforehand. + content: String, + /// Date string -- But it's unchecked + /// Stores the date on which the post was published. + date_published: String, + /// Date string -- But it's unchecked + /// # Date updated + /// Stores the date on which the post was last updated. + date_updated: String, + /// Category this post belongs to + category: String, + /// Tags that belong to this post + tags: List(String), + /// Mastodon instance and post id to link to for comments. + mastodon_comments: option.Option(MastodonStatus), + ) +} + +/// Mastodon instance and post id to link to for comments. +pub type MastodonStatus { + MastodonStatus(instance: String, id: String) +} diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4.gleam new file mode 100644 index 0000000..4e65fdc --- /dev/null +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4.gleam @@ -0,0 +1,59 @@ +//// Cynthia Mini v4 Config format + +import cynthia_websites_mini_shared/config/v4_1 +import gleam/string + +pub type V4mini { + V4mini( + global: V4miniGlobal, + integrations: V4miniIntegrations, + posts: V4miniPosts, + ) +} + +pub type V4miniGlobal { + V4miniGlobal( + theme: String, + theme_dark: String, + colour: String, + site_name: String, + site_description: String, + ) +} + +pub type V4miniIntegrations { + V4miniIntegrations(git: Bool, sitemap: String, crawlable_context: Bool) +} + +pub type V4miniPosts { + V4miniPosts(comment_repo: String) +} + +pub fn upgrade(in: V4mini) -> v4_1.V4p1Mini { + v4_1.V4p1Mini( + global: v4_1.V4p1MiniGlobal( + site_description: in.global.site_description, + theme: in.global.theme, + theme_dark: in.global.theme_dark, + site_name: in.global.site_name, + ), + integrations: v4_1.V4p1MiniIntegrations( + git: in.integrations.git, + sitemap: in.integrations.sitemap, + crawlable_context: in.integrations.crawlable_context, + ), + posts: v4_1.V4p1MiniPosts(comments: { + case in.posts.comment_repo |> string.lowercase { + "" -> v4_1.CommentsDisabled + "mastodon" -> v4_1.CommentsMastodonStored + _ -> { + case in.posts.comment_repo |> string.split_once("/") { + Ok(#(username, repositoryname)) -> + v4_1.CommentsGithubStored(username:, repositoryname:) + _ -> v4_1.CommentsDisabled + } + } + } + }), + ) +} diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4/decodes.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4/decodes.gleam new file mode 100644 index 0000000..4b1a184 --- /dev/null +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4/decodes.gleam @@ -0,0 +1,101 @@ +import cynthia_websites_mini_shared/config/v4 +import gleam/dynamic/decode +import plinth/javascript/console +import tom + +pub fn v4mini_dynamic() -> decode.Decoder(v4.V4mini) { + use global <- decode.field("global", { + use theme <- decode.field("theme", decode.string) + use theme_dark <- decode.field("theme_dark", decode.string) + use colour <- decode.field("colour", decode.string) + use site_name <- decode.field("site_name", decode.string) + use site_description <- decode.field("site_description", decode.string) + decode.success(v4.V4miniGlobal( + theme:, + theme_dark:, + colour:, + site_name:, + site_description:, + )) + }) + use integrations <- decode.field("integrations", { + use git <- decode.field("git", decode.bool) + use sitemap <- decode.field("sitemap", decode.string) + use crawlable_context <- decode.field("crawlable_context", decode.bool) + decode.success(v4.V4miniIntegrations(git:, sitemap:, crawlable_context:)) + }) + use posts <- decode.field("posts", { + use comment_repo <- decode.field("comment_repo", decode.string) + decode.success(v4.V4miniPosts(comment_repo:)) + }) + decode.success(v4.V4mini(global:, integrations:, posts:)) +} + +pub fn v4mini_toml(toml_source: String) -> Result(v4.V4mini, Nil) { + case tom.parse(toml_source) { + Ok(toml) -> { + v4.V4mini( + global: { + let theme = tom.get_string(toml, ["global", "theme"]) |> unsafe_unwrap + + v4.V4miniGlobal( + theme:, + theme_dark: tom.get_string(toml, ["global", "theme", "dark"]) + |> unsafe_unwrap, + site_name: tom.get_string(toml, ["global", "site_name"]) + |> unsafe_unwrap, + site_description: tom.get_string(toml, [ + "global", + "site_description", + ]) + |> unsafe_unwrap, + colour: tom.get_string(toml, ["global", "colour"]) + |> unsafe_unwrap, + ) + }, + integrations: v4.V4miniIntegrations( + git: tom.get_bool(toml, [ + "integrations", + "git", + ]) + |> unsafe_unwrap, + sitemap: tom.get_string(toml, [ + "integrations", + "sitemap", + ]) + |> unsafe_unwrap, + crawlable_context: tom.get_bool(toml, [ + "integrations", + "crawlable_context", + ]) + |> unsafe_unwrap, + ), + posts: v4.V4miniPosts(comment_repo: { + tom.get_string(toml, [ + "posts", + "comment_repo", + ]) + |> unsafe_unwrap + }), + ) + |> Ok + } + // We don't propogate upwards, we give back a Error value but inform here and then exit upstream. + Error(_) -> { + console.log("Could not parse TOML!") + Error(Nil) + } + } +} + +fn unsafe_unwrap(v: Result(s, _)) { + case v { + Ok(a) -> a + Error(_) -> { + let d = + "Encountered invalid value in legacy config, Cynthia Mini won't try to recover for this in legacy configs." + console.error(d) + panic as d + } + } +} diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1.gleam new file mode 100644 index 0000000..db9e30a --- /dev/null +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1.gleam @@ -0,0 +1,49 @@ +//// Cynthia v4.1 [external] Config format + +pub type V4p1Mini { + V4p1Mini( + global: V4p1MiniGlobal, + integrations: V4p1MiniIntegrations, + posts: V4p1MiniPosts, + ) +} + +pub type V4p1MiniGlobal { + V4p1MiniGlobal( + theme: String, + theme_dark: String, + site_name: String, + site_description: String, + ) +} + +pub type V4p1MiniIntegrations { + V4p1MiniIntegrations(git: Bool, sitemap: String, crawlable_context: Bool) +} + +pub type V4p1MiniPosts { + V4p1MiniPosts(comments: V4p1MiniPostsComments) +} + +pub type V4p1MiniPostsComments { + CommentsMastodonStored + CommentsGithubStored(username: String, repositoryname: String) + CommentsDisabled +} + +pub fn new() -> V4p1Mini { + V4p1Mini( + global: V4p1MiniGlobal( + theme: "autumn", + theme_dark: "night", + site_name: "My Site", + site_description: "A big site on a mini Cynthia!", + ), + integrations: V4p1MiniIntegrations( + git: True, + sitemap: "", + crawlable_context: False, + ), + posts: V4p1MiniPosts(comments: CommentsDisabled), + ) +} diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1/decodes.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1/decodes.gleam new file mode 100644 index 0000000..85aa898 --- /dev/null +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1/decodes.gleam @@ -0,0 +1,194 @@ +import cynthia_websites_mini_shared/config/v4 +import cynthia_websites_mini_shared/config/v4/decodes +import cynthia_websites_mini_shared/config/v4_1 +import gleam/dynamic/decode +import gleam/float +import gleam/int +import gleam/option +import gleam/result +import gleam/string +import plinth/javascript/console +import tom + +pub fn v4p1_mini_dynamic() -> decode.Decoder(v4_1.V4p1Mini) { + use global <- decode.field("global", { + use theme <- decode.field("theme", decode.string) + use theme_dark <- decode.field("theme_dark", decode.string) + use site_name <- decode.field("site_name", decode.string) + use site_description <- decode.field("site_description", decode.string) + decode.success(v4_1.V4p1MiniGlobal( + theme:, + theme_dark:, + site_name:, + site_description:, + )) + }) + use integrations <- decode.field("integrations", { + use git <- decode.field("git", decode.bool) + use sitemap <- decode.field("sitemap", decode.string) + use crawlable_context <- decode.field("crawlable_context", decode.bool) + decode.success(v4_1.V4p1MiniIntegrations(git:, sitemap:, crawlable_context:)) + }) + use posts <- decode.field("posts", { + use comments <- decode.field("comments", { + use comments <- field_or( + field: "comments", + decoder: { + use variant <- decode.field("store", decode.string) + case variant |> string.lowercase { + "mastodon" -> decode.success(v4_1.CommentsMastodonStored) + "github" -> { + use username <- decode.field("username", decode.string) + use repositoryname <- decode.field( + "repositoryname", + decode.string, + ) + decode.success(v4_1.CommentsGithubStored( + username:, + repositoryname:, + )) + } + "disabled" | "" -> decode.success(v4_1.CommentsDisabled) + _ -> + decode.failure( + v4_1.CommentsDisabled, + "v4_1.V4p1MiniPostsComments", + ) + } + }, + otherwise: v4_1.CommentsDisabled, + ) + decode.success(comments) + }) + decode.success(v4_1.V4p1MiniPosts(comments:)) + }) + decode.success(v4_1.V4p1Mini(global:, integrations:, posts:)) +} + +fn field_or( + field field: String, + decoder field_decoder: decode.Decoder(t), + otherwise default: t, + next next: fn(t) -> decode.Decoder(final), +) -> decode.Decoder(final) { + use val <- decode.optional_field( + field, + option.None, + decode.optional(field_decoder), + ) + next(val |> option.unwrap(default)) +} + +pub fn vp4p1mini_toml(toml_source: String) { + case tom.parse(toml_source) { + Ok(toml) -> { + let edition = + tom.get_string(toml, ["edition"]) |> result.map(string.lowercase) + let version = + result.or(tom.get_float(toml, ["version"]), { + tom.get_int(toml, ["version"]) |> result.map(int.to_float) + }) + + case edition, version { + Ok("mini"), Ok(4.1) -> { + v4_1.V4p1Mini( + global: { + let theme = + tom.get_string(toml, ["global", "theme", "default"]) + |> result.unwrap({ + tom.get_string(toml, ["global", "theme"]) + |> result.unwrap(v4_1.new().global.theme) + }) + v4_1.V4p1MiniGlobal( + theme:, + theme_dark: result.unwrap( + tom.get_string(toml, ["global", "theme", "dark"]), + theme, + ), + site_name: tom.get_string(toml, ["global", "site_name"]) + |> result.unwrap(v4_1.new().global.site_name), + site_description: tom.get_string(toml, [ + "global", + "site_description", + ]) + |> result.unwrap(v4_1.new().global.site_description), + ) + }, + integrations: v4_1.V4p1MiniIntegrations( + git: tom.get_bool(toml, [ + "integrations", + "git", + ]) + |> result.unwrap(v4_1.new().integrations.git), + sitemap: tom.get_string(toml, [ + "integrations", + "sitemap", + ]) + |> result.unwrap(v4_1.new().integrations.sitemap), + crawlable_context: tom.get_bool(toml, [ + "integrations", + "crawlable_context", + ]) + |> result.unwrap(v4_1.new().integrations.crawlable_context), + ), + posts: v4_1.V4p1MiniPosts(comments: { + case + tom.get_string(toml, ["posts", "comments", "store"]) + |> result.unwrap("disabled") + { + "mastodon" -> v4_1.CommentsMastodonStored + "github" -> { + case + tom.get_string(toml, ["posts", "comments", "username"]), + tom.get_string(toml, ["posts", "comments", "repositoryname"]) + { + Ok(username), Ok(repositoryname) -> + v4_1.CommentsGithubStored(username:, repositoryname:) + _, _ -> v4_1.new().posts.comments + } + } + _ -> v4_1.CommentsDisabled + } + }), + ) + |> Ok + } + Ok("mini"), Ok(4.0) -> { + decodes.v4mini_toml(toml_source) |> result.map(v4.upgrade) + } + Ok(_), Error(_) | Error(_), Ok(_) -> { + console.error("Unknown combination of edition and version.") + Error(Nil) + } + Error(_), Error(_) -> { + console.log("Could not parse TOML!") + Error(Nil) + } + Ok(edition), Ok(version) -> { + console.error( + "Config version " + <> version |> float.to_string() + <> " with edition '" + <> edition + <> "' is NOT supported by this version of Cynthia." + <> "\n Usually this means one of these options:" + <> "\n - it was written for a different edition" + <> "\n - it is invalid" + <> "\n - or this version of cynthia is too old to understand this file." + <> case edition == "mini" { + True -> + "\n\n\n It seems to be that last option, since the edition it is written for, does match 'mini'." + False -> "" + }, + ) + Error(Nil) + } + } + } + // We don't propogate upwards, we give back a Error value but inform here and then exit upstream. + Error(_) -> { + console.log("Could not parse TOML!") + Error(Nil) + } + } +} diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1/encodes.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1/encodes.gleam new file mode 100644 index 0000000..e4fc3e9 --- /dev/null +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1/encodes.gleam @@ -0,0 +1,95 @@ +import cynthia_websites_mini_shared/config/v4_1 +import gleam/bool +import gleam/json + +pub fn v4p1_mini_json(v4p1_mini: v4_1.V4p1Mini) -> json.Json { + json.object([ + #( + "global", + json.object([ + #("theme", json.string(v4p1_mini.global.theme)), + #("theme_dark", json.string(v4p1_mini.global.theme_dark)), + #("site_name", json.string(v4p1_mini.global.site_name)), + #("site_description", json.string(v4p1_mini.global.site_description)), + ]), + ), + #( + "integrations", + json.object([ + #("git", json.bool(v4p1_mini.integrations.git)), + #("sitemap", json.string(v4p1_mini.integrations.sitemap)), + #( + "crawlable_context", + json.bool(v4p1_mini.integrations.crawlable_context), + ), + ]), + ), + #("posts", { + json.object([ + #("comments", case v4p1_mini.posts.comments { + v4_1.CommentsMastodonStored -> + json.object([ + #("store", json.string("mastodon")), + ]) + v4_1.CommentsGithubStored(username:, repositoryname:) -> + json.object([ + #("store", json.string("github")), + #("username", json.string(username)), + #("repositoryname", json.string(repositoryname)), + ]) + v4_1.CommentsDisabled -> + json.object([ + #("store", json.string("disabled")), + ]) + }), + ]) + }), + ]) +} + +pub fn v4p1_mini_toml(v4p1_mini: v4_1.V4p1Mini) -> String { + "# Do not edit these variables! It is set by Cynthia to tell it's config format apart. + config.edition=\"mini\" + config.version=4.1 + [global] + # Theme to use for light mode - default themes: autumn, default + # Theme to use for dark mode - default themes: night, default-dark + theme = { default = \"" + <> v4p1_mini.global.theme + <> "\", dark = \"" + <> v4p1_mini.global.theme_dark + <> "\" } + # Your website's name, displayed in various places + site_name = \"" + <> v4p1_mini.global.site_name + <> "\" + # A brief description of your website + site_description = \"" + <> v4p1_mini.global.site_description + <> "\" + + [integrations] + # Enable git integration for the website + # This will allow Cynthia Mini to detect the git repository + # For example linking to the commit hash in the footer + git = " + <> v4p1_mini.integrations.git |> bool.to_string + <> " + + # Enable sitemap generation + # This will generate a sitemap.xml file in the root of the website + # + # You will need to enter the base URL of your website in the sitemap variable below. + # If your homepage is at \"https://example.com/#/\", then the sitemap variable should be set to \"https://example.com\". + # If you do not want to use a sitemap, set this to \"false\", or leave it empty (\"\"), you can also remove the sitemap variable altogether. + sitemap = \"" + <> v4p1_mini.integrations.sitemap + <> "\" + + # Enable crawlable context (JSON-LD injection) + # This will allow search engines to crawl the website, and makes it + # possible for the website to be indexed by search engine and LLMs. + crawlable_context = " + <> v4p1_mini.integrations.crawlable_context |> bool.to_string + <> "" +} diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/dom.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/ffi.gleam similarity index 58% rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/dom.gleam rename to cynthia_websites_mini_client/src/cynthia_websites_mini_shared/ffi.gleam index b73f03b..13fdfd2 100644 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/dom.gleam +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/ffi.gleam @@ -3,7 +3,7 @@ import plinth/browser/document import plinth/browser/element pub fn push_title(title: String) -> Result(Nil, String) { - use title_element <- result.then( + use title_element <- result.try( document.query_selector("title") |> result.replace_error("No title element found"), ) @@ -22,23 +22,38 @@ pub fn push_title(title: String) -> Result(Nil, String) { Ok(Nil) } +@external(javascript, "./ts_ffi.ts", "my_own_version") +pub fn version() -> String + /// Get the color scheme of the user's system (media query) -@external(javascript, "./dom.ts", "get_color_scheme") +@external(javascript, "./ts_ffi.ts", "get_color_scheme") pub fn get_color_scheme() -> String /// Set the data attribute of an element -@external(javascript, "./dom.ts", "set_data") +@external(javascript, "./ts_ffi.ts", "set_data") pub fn set_data(element: element.Element, key: String, value: String) -> Nil /// Set the hash of the window -@external(javascript, "./dom.ts", "set_hash") +@external(javascript, "./ts_ffi.ts", "set_hash") pub fn set_hash(hash: String) -> Nil /// Get innerhtml of an element -@external(javascript, "./dom.ts", "get_inner_html") +@external(javascript, "./ts_ffi.ts", "get_inner_html") pub fn get_inner_html(element: element.Element) -> String /// jsonify_string /// Convert a string to a JSON safe string -@external(javascript, "./dom.ts", "jsonify_string") +@external(javascript, "./ts_ffi.ts", "jsonify_string") pub fn jsonify_string(str: String) -> Result(String, Nil) + +@external(javascript, "./ts_ffi.ts", "destroy_comment_box") +pub fn destroy_comment_box() -> Nil + +@external(javascript, "./ts_ffi.ts", "apply_styles_to_comment_box") +pub fn comment_box_forced_styles() -> Nil + +@external(javascript, "./ts_ffi.ts", "browse") +pub fn browse(a: String) -> Nil + +@external(javascript, "./ts_ffi.ts", "browse_prompt") +pub fn browse_prompt(s: String) -> Nil diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/dom.ts b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/ts_ffi.ts similarity index 87% rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/dom.ts rename to cynthia_websites_mini_client/src/cynthia_websites_mini_shared/ts_ffi.ts index 289ad3d..959ce36 100644 --- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/dom.ts +++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/ts_ffi.ts @@ -69,3 +69,17 @@ export function jsonify_string(str: string) { return new Error(null); } } + +import { version } from "package.json"; +export function my_own_version(): string { + return version; +} + +export function browse_prompt(l: string) { + if (window.confirm("Leave this page and go to '" + l + "'?")) { + browse(l); + } +} +export function browse(l: string) { + window.location.assign(l); +} diff --git a/cynthia_websites_mini_client/test/cynthia_websites_mini_client_test.gleam b/cynthia_websites_mini_client/test/cynthia_websites_mini_client_test.gleam index 2fa852f..fba3c88 100644 --- a/cynthia_websites_mini_client/test/cynthia_websites_mini_client_test.gleam +++ b/cynthia_websites_mini_client/test/cynthia_websites_mini_client_test.gleam @@ -1,65 +1,13 @@ -import birdie -import cynthia_websites_mini_client/configtype -import cynthia_websites_mini_client/pottery/djotparse import gleeunit -import gleeunit/should -import lustre/element -import lustre/element/html -pub fn main() { +pub fn main() -> Nil { gleeunit.main() } // gleeunit test functions end in `_test` pub fn hello_world_test() { - 1 - |> should.equal(1) -} - -pub fn simple_djot_test() { - "# Hello World\n\nThis is a test paragraph.\n\n- Item 1\n- Item 2" - |> djotparse.entry_to_conversion() - |> html.section([], _) - |> element.to_readable_string - |> birdie.snap(title: "simple_djot_test") -} - -pub fn djot_with_preprocessing_test() { - "# Hello World\n\nThis is a test paragraph.\n\n- [ ] Task item\n- [x] Completed task\n\n> This is a blockquote\n\nAnother paragraph." - |> djotparse.entry_to_conversion() - |> html.section([], _) - |> element.to_readable_string - |> birdie.snap(title: "djot_with_preprocessing_test") -} - -pub fn autolinks_test() { - "External page example, using the theme list, downloading from " - |> djotparse.entry_to_conversion() - |> html.section([], _) - |> element.to_readable_string - |> birdie.snap(title: "autolinks_test") -} - -pub fn links_in_preprocessed_items_test() { - "- [ ] Task with [link](https://example.com)\n- [x] Completed task with [another link](https://test.com)\n\n> Blockquote with [a link](https://blockquote.example)" - |> djotparse.entry_to_conversion() - |> html.section([], _) - |> element.to_readable_string - |> birdie.snap(title: "links_in_preprocessed_items_test") -} - -pub fn ordered_list_with_links_test() { - "1. First item with [link](https://first.com)\n2. Second item with [another link](https://second.com)\n3. Third item with **bold** and [link](https://third.com)" - |> djotparse.entry_to_conversion() - |> html.section([], _) - |> element.to_readable_string - |> birdie.snap(title: "ordered_list_with_links_test") -} + let name = "Joe" + let greeting = "Hello, " <> name <> "!" -pub fn ootb_index_rendering_test() { - configtype.ootb_index - |> djotparse.entry_to_conversion() - |> html.body([], _) - |> element.to_readable_string - |> birdie.snap(title: "ootb_index_rendering_test") + assert greeting == "Hello, Joe!" } diff --git a/cynthia_websites_mini_server/gleam.toml b/cynthia_websites_mini_server/gleam.toml index 573ce7c..6b522c0 100644 --- a/cynthia_websites_mini_server/gleam.toml +++ b/cynthia_websites_mini_server/gleam.toml @@ -18,38 +18,19 @@ typescript_declarations = true [dependencies] # Shared dependencies with the client -gleam_stdlib = "0.59.0" -plinth = ">= 0.5.9 and < 1.0.0" -gleam_javascript = "1.0.0" -gleam_community_colour = "1.4.1" - -gleam_json = "2.3.0" -gleam_http = "3.7.2" -gleam_fetch = "1.3.0" +gleam_stdlib = ">= 0.44.0 and < 2.0.0" +plinth = ">= 0.9.2 and < 1.0.0" +tom = "1.1.1" # Other dependencies argv = "1.0.2" birl = "1.8.0" -conversation = "2.0.1" bungibindies = "1.2.0-rc" cynthia_websites_mini_client = { path = "../cynthia_websites_mini_client" } -edit_distance = "2.0.1" -envoy = "1.0.2" -filepath = "1.1.2" -glam = "2.0.2" -glance = "3.0.0" -gleam_community_ansi = "1.4.3" -gleam_regexp = "1.1.1" -gleam_yielder = "1.1.0" -gleamy_lights = "2.3.0" -javascript_mutable_reference = "1.0.0" -justin = "1.0.1" -ranger = "1.4.0" simplifile = "2.2.1" -tom = "1.1.1" - +gleamy_lights = ">= 2.3.1 and < 3.0.0" +gleam_javascript = ">= 1.0.0 and < 2.0.0" -webls = "1.5.1" [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" birdie = ">= 1.2.7 and < 2.0.0" diff --git a/cynthia_websites_mini_server/manifest.toml b/cynthia_websites_mini_server/manifest.toml index af2f9f2..40927bc 100644 --- a/cynthia_websites_mini_server/manifest.toml +++ b/cynthia_websites_mini_server/manifest.toml @@ -6,47 +6,32 @@ packages = [ { name = "birdie", version = "1.3.0", build_tools = ["gleam"], requirements = ["argv", "edit_distance", "filepath", "glance", "gleam_community_ansi", "gleam_stdlib", "justin", "rank", "simplifile", "term_size", "trie_again"], otp_app = "birdie", source = "hex", outer_checksum = "425916385B6CD82A58F54CC39605262A524B169746FC9AD9C799BC76E88F7AF3" }, { name = "birl", version = "1.8.0", build_tools = ["gleam"], requirements = ["gleam_regexp", "gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "2AC7BA26F998E3DFADDB657148BD5DDFE966958AD4D6D6957DD0D22E5B56C400" }, { name = "bungibindies", version = "1.2.0-rc", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_stdlib"], otp_app = "bungibindies", source = "hex", outer_checksum = "C1A4DD5D0BE282E4A6F007ECE3FE1477E38CFDF1B6043A5ABAB63C53443AC473" }, - { name = "conversation", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "conversation", source = "hex", outer_checksum = "103DF47463B8432AB713D6643DC17244B9C82E2B172A343150805129FE584A2F" }, - { name = "cynthia_websites_mini_client", version = "1.2.0-rc2", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_fetch", "gleam_http", "gleam_javascript", "gleam_json", "gleam_stdlib", "gleam_time", "houdini", "jot", "lustre", "modem", "odysseus", "plinth", "rsvp"], source = "local", path = "../cynthia_websites_mini_client" }, + { name = "cynthia_websites_mini_client", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "plinth", "tom"], source = "local", path = "../cynthia_websites_mini_client" }, { name = "edit_distance", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "edit_distance", source = "hex", outer_checksum = "A1E485C69A70210223E46E63985FA1008B8B2DDA9848B7897469171B29020C05" }, - { name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" }, + { name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" }, { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, { name = "glam", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "4932A2D139AB0389E149396407F89654928D7B815E212BB02F13C66F53B1BBA1" }, { name = "glance", version = "3.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "F3458292AFB4136CEE23142A8727C1270494E7A96978B9B9F9D2C1618583EF3D" }, { name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" }, - { name = "gleam_community_colour", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "386CB9B01B33371538672EEA8A6375A0A0ADEF41F17C86DDCB81C92AD00DA610" }, - { name = "gleam_erlang", version = "0.34.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "0C38F2A128BAA0CEF17C3000BD2097EB80634E239CE31A86400C4416A5D0FDCC" }, - { name = "gleam_fetch", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "2CBF9F2E1C71AEBBFB13A9D5720CD8DB4263EB02FE60C5A7A1C6E17B0151C20C" }, - { name = "gleam_http", version = "3.7.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8A70D2F70BB7CFEB5DF048A2183FFBA91AF6D4CF5798504841744A16999E33D2" }, - { name = "gleam_httpc", version = "4.1.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C670EBD46FC1472AD5F1F74F1D3938D1D0AC1C7531895ED1D4DDCB6F07279F43" }, + { name = "gleam_community_colour", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "E34DD2C896AC3792151EDA939DA435FF3B69922F33415ED3C4406C932FBE9634" }, { name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" }, - { name = "gleam_json", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C55C5C2B318533A8072D221C5E06E5A75711C129E420DD1CE463342106012E5D" }, - { name = "gleam_otp", version = "0.16.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "50DA1539FC8E8FA09924EB36A67A2BBB0AD6B27BCDED5A7EF627057CF69D035E" }, + { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, { name = "gleam_stdlib", version = "0.59.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F8FEE9B35797301994B81AF75508CF87C328FE1585558B0FFD188DC2B32EAA95" }, - { name = "gleam_time", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "0DF3834D20193F0A38D0EB21F0A78D48F2EC276C285969131B86DF8D4EF9E762" }, { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, - { name = "gleamy_lights", version = "2.3.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_community_colour", "gleam_stdlib"], otp_app = "gleamy_lights", source = "hex", outer_checksum = "8A3D43BCA0D935F7CC787F4D0D1771F822B3366114C08B93CC8D00747618499A" }, + { name = "gleamy_lights", version = "2.3.1", build_tools = ["gleam"], requirements = ["envoy", "gleam_community_colour", "gleam_stdlib"], otp_app = "gleamy_lights", source = "hex", outer_checksum = "CD89DD48BBCD8FBB6B8CB84101C70221CBFB901F711C3C7F81F47288EC8074FD" }, { name = "gleeunit", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "A7DD6C07B7DA49A6E28796058AA89E651D233B357D5607006D70619CD89DAAAB" }, - { name = "glexer", version = "2.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "5C235CBDF4DA5203AD5EAB1D6D8B456ED8162C5424FE2309CFFB7EF438B7C269" }, - { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" }, - { name = "javascript_mutable_reference", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "javascript_mutable_reference", source = "hex", outer_checksum = "3EE953EE7FE4FAFD17C16F24184F4C832FE260D761753F28F20D4AC1DA080F03" }, - { name = "jot", version = "5.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "houdini", "splitter"], otp_app = "jot", source = "hex", outer_checksum = "B1A0C91A3D273971D1CA1F644FF0A9CAC8256BDA249CADC927041BF14E7114A6" }, + { name = "glexer", version = "2.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "splitter"], otp_app = "glexer", source = "hex", outer_checksum = "41D8D2E855AEA87ADC94B7AF26A5FEA3C90268D4CF2CCBBD64FD6863714EE085" }, { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" }, - { name = "lustre", version = "5.0.3", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "0BB69D69A9E75E675AA2C32A4A0E5086041D037829FC8AD385BA6A59E45A60A2" }, - { name = "modem", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib", "lustre"], otp_app = "modem", source = "hex", outer_checksum = "EF6B6B187E9D6425DFADA3A1AC212C01C4F34913A135DA2FF9B963EEF324C1F7" }, - { name = "odysseus", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "odysseus", source = "hex", outer_checksum = "6A97DA1075BDDEA8B60F47B1DFFAD49309FA27E73843F13A0AF32EA7087BA11C" }, - { name = "plinth", version = "0.7.1", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "63BB36AACCCCB82FBE46A862CF85CB88EBE4EF280ECDBAC4B6CB042340B9E1D8" }, + { name = "plinth", version = "0.9.2", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "A5A14C590F9820F8447E989B66C73C9DE077FAAB75618D639DFA1F3BCA45F946" }, { name = "pprint", version = "1.0.6", build_tools = ["gleam"], requirements = ["glam", "gleam_stdlib"], otp_app = "pprint", source = "hex", outer_checksum = "4E9B34AE03B2E81D60F230B9BAF1792BE1AC37AFB5564B8DEBEE56BAEC866B7D" }, { name = "ranger", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_yielder"], otp_app = "ranger", source = "hex", outer_checksum = "C8988E8F8CDBD3E7F4D8F2E663EF76490390899C2B2885A6432E942495B3E854" }, { name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" }, - { name = "rsvp", version = "1.0.4", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_fetch", "gleam_http", "gleam_httpc", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "rsvp", source = "hex", outer_checksum = "EFCA7CD53B0A8738C06E136422D1FF080DBB657C89E077F7B9DD20BFACE0A77A" }, { name = "simplifile", version = "2.2.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "C88E0EE2D509F6D86EB55161D631657675AA7684DAB83822F7E59EB93D9A60E3" }, { name = "splitter", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "3DFD6B6C49E61EDAF6F7B27A42054A17CFF6CA2135FF553D0CB61C234D281DD0" }, { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, { name = "tom", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0910EE688A713994515ACAF1F486A4F05752E585B9E3209D8F35A85B234C2719" }, - { name = "trie_again", version = "1.1.3", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "365FE609649F3A098D1D7FC7EA5222EE422F0B3745587BF2AB03352357CA70BB" }, - { name = "webls", version = "1.5.1", build_tools = ["gleam"], requirements = ["birl", "gleam_stdlib"], otp_app = "webls", source = "hex", outer_checksum = "6C78FFCD3BB888725F83FBD0729BDEA1BFEDFBB06544FCA15BF98FBA04F863B0" }, + { name = "trie_again", version = "1.1.4", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "E3BD66B4E126EF567EA8C4944EAB216413392ADF6C16C36047AF79EE5EF13466" }, ] [requirements] @@ -54,29 +39,12 @@ argv = { version = "1.0.2" } birdie = { version = ">= 1.2.7 and < 2.0.0" } birl = { version = "1.8.0" } bungibindies = { version = "1.2.0-rc" } -conversation = { version = "2.0.1" } cynthia_websites_mini_client = { path = "../cynthia_websites_mini_client" } -edit_distance = { version = "2.0.1" } -envoy = { version = "1.0.2" } -filepath = { version = "1.1.2" } -glam = { version = "2.0.2" } -glance = { version = "3.0.0" } -gleam_community_ansi = { version = "1.4.3" } -gleam_community_colour = { version = "1.4.1" } -gleam_fetch = { version = "1.3.0" } -gleam_http = { version = "3.7.2" } -gleam_javascript = { version = "1.0.0" } -gleam_json = { version = "2.3.0" } -gleam_regexp = { version = "1.1.1" } -gleam_stdlib = { version = "0.59.0" } -gleam_yielder = { version = "1.1.0" } -gleamy_lights = { version = "2.3.0" } +gleam_javascript = { version = ">= 1.0.0 and < 2.0.0" } +gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } +gleamy_lights = { version = ">= 2.3.1 and < 3.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } -javascript_mutable_reference = { version = "1.0.0" } -justin = { version = "1.0.1" } -plinth = { version = ">= 0.5.9 and < 1.0.0" } +plinth = { version = ">= 0.9.2 and < 1.0.0" } pprint = { version = ">= 1.0.5 and < 2.0.0" } -ranger = { version = "1.4.0" } simplifile = { version = "2.2.1" } tom = { version = "1.1.1" } -webls = { version = "1.5.1" } diff --git a/cynthia_websites_mini_server/src/cynthia_websites_mini_server.gleam b/cynthia_websites_mini_server/src/cynthia_websites_mini_server.gleam index 34b1484..f8cffa0 100644 --- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server.gleam +++ b/cynthia_websites_mini_server/src/cynthia_websites_mini_server.gleam @@ -1,27 +1,15 @@ import bungibindies import bungibindies/bun -import bungibindies/bun/http/serve.{ServeOptions} import cynthia_websites_mini_client -import cynthia_websites_mini_client/configtype -import cynthia_websites_mini_client/shared/jsonld -import cynthia_websites_mini_client/shared/sitemap -import cynthia_websites_mini_server/config -import cynthia_websites_mini_server/mutable_model_type -import cynthia_websites_mini_server/ssrs -import cynthia_websites_mini_server/utils/files -import cynthia_websites_mini_server/web +import cynthia_websites_mini_shared/config/site_json +import cynthia_websites_mini_shared/ffi import gleam/bool -import gleam/int import gleam/javascript/array -import gleam/javascript/promise -import gleam/json import gleam/list -import gleam/option.{None, Some} +import gleam/option.{type Option, None, Some} import gleam/string -import gleamy_lights/console import gleamy_lights/premixed -import javascript/mutable_reference -import plinth/javascript/global +import plinth/javascript/console import plinth/node/process import simplifile @@ -55,17 +43,13 @@ pub fn main() { <> premixed.text_bright_orange(process.cwd()) <> "!", ) - use m <- promise.await(mutable_model_type.new()) - case process.argv() |> array.to_list() |> list.drop(2) { - ["dynamic", ..] | ["host", ..] -> - dynamic_site_server(m, 60_000) |> promise.resolve - ["preview", ..] -> dynamic_site_server(m, 20) |> promise.resolve - ["pregenerate", ..] | ["static"] -> static_site_server(m) + let args = process.argv() |> array.to_list() |> list.drop(2) + case args { + ["pregenerate", ..] | ["static"] -> start() ["init", ..] | ["initialise", ..] -> { - config.initcfg() - |> promise.resolve + init(args |> list.includes("--force")) } - ["man", ..] | ["help", ..] | ["--help", ..] | ["-h", ..] | [] -> { + _ -> { case process.argv() |> array.to_list() |> list.drop(2) { [] -> console.error("No subcommand given.\n") _ -> Nil @@ -85,25 +69,13 @@ pub fn main() { premixed.text_pink("initialise\n"), ]) <> "\t\t\t\tInitialise the config file then exit\n\n" - // Dynamic: + // Run: <> string.concat([ - premixed.text_pink("\tdynamic"), - " | ", - premixed.text_pink("host\n"), - ]) - <> "\t\t\t\tStart a dynamic website server\n\n" - // Pregenerate: - <> string.concat([ - premixed.text_pink("\tstatic"), + premixed.text_pink("\trun"), " | ", premixed.text_pink("pregenerate\n"), ]) <> "\t\t\t\tGenerate a static website\n\n" - // Preview: - <> premixed.text_pink("\tpreview\n") - <> "\t\t\t\tStart a dynamic website server for previewing\n" - <> "\t\t\t\tthis is the same as dynamic, but with a shorter\n" - <> "\t\t\t\tinterval for the cache\n\n" // Help: <> premixed.text_lilac("\thelp") <> "\n" @@ -114,7 +86,6 @@ pub fn main() { ) <> ".\n", ) - |> promise.resolve } [a, ..] -> console.error( @@ -129,270 +100,136 @@ pub fn main() { <> premixed.text_purple("help") <> "´ to see a list of all subcommands.\n", ) - |> promise.resolve } } -fn dynamic_site_server(mutmodel: mutable_model_type.MutableModel, lease: Int) { - console.info("Cynthia Mini is in dynamic site mode!") - let model = mutmodel |> mutable_reference.get - let conf = model.config - { - let folder = process.cwd() <> "/assets/cynthia-mini" - case simplifile.create_directory_all(folder) { - Ok(..) -> Nil - Error(e) -> { - console.error( - "A problem occurred while creating the ´" - <> folder - <> "´ directory: " - <> premixed.text_error_red(string.inspect(e)), - ) - process.exit(1) - panic as "We should not reach here" - } - } +fn get_context() -> site_json.SiteJSON { + let config = case + { + let global_conf_filepath = + files.path_join([process.cwd(), "/cynthia.toml"]) + let global_conf_filepath_exists = files.file_exist(global_conf_filepath) - case files.file_exist(process.cwd() <> "/assets/cynthia-mini/README.md") { - True -> Nil - False -> { - case - simplifile.write( - process.cwd() <> "/assets/cynthia-mini/README.md", - "# What does this folder do?\n\r\n\rThis folder holds a few files Cynthia Mini serves to the browser to make sure everything works alright.\n\r\n\rThese are usually checked and downloaded if necessary only during start of the server,\n\rso try not to touch them! If you believe one of the files in here might be faulty, delete it, and restart the server.\n\r\n\rHave a nice day! :)", - ) - { - Ok(..) -> Nil - Error(e) -> { - console.error( - "A problem occurred while creating the ´" - <> process.cwd() - <> "/assets/cynthia-mini/README.md" - <> "´ file: " - <> premixed.text_error_red(string.inspect(e)), - ) - process.exit(1) - panic as "We should not reach here" + case global_conf_filepath_exists { + True -> { + Nil + } + // No config was found. Let's look for legacy config or initialise. + False -> { + let global_conf_filepath_legacy = + files.path_join([process.cwd(), "/cynthia-mini.toml"]) + let global_conf_filepath_legacy_exists = + files.file_exist(global_conf_filepath_legacy) + case + global_conf_filepath_legacy_exists, + simplifile.read(global_conf_filepath_legacy) + { + True, Ok(legacy_config) -> { + console.warn( + "A legacy config file was found! Cynthia Mini will attempt to auto-convert it on the go and continue.", + ) + let upgraded_config = + "# This file was upgraded to the universal Cynthia Config format\n# Do not edit these two variables! They are set by Cynthia to tell it's config format apart.\nconfig.edition=\"mini\"\nconfig.version=4.0\n\n" + <> legacy_config + case + simplifile.write( + to: global_conf_filepath, + contents: upgraded_config, + ) + { + Ok(_) -> { + let _ = + simplifile.rename( + at: global_conf_filepath_legacy, + to: global_conf_filepath_legacy <> ".old", + ) + Nil + } + Error(_) -> { + console.error( + "Error: Could not write upgraded config to " + <> global_conf_filepath + <> ". Please check file permissions.", + ) + process.exit(1) + } + } + } + True, Error(_) -> { + console.error( + "Some error happened while trying to read " + <> global_conf_filepath_legacy + <> ".", + ) + process.exit(1) + } + // No config found, and no old config found. + False, _ -> { + init() + get_context() + } } } - Nil } + let e = "Could not read " <> global_conf_filepath + let assert Ok(toml) = simplifile.read(global_conf_filepath) as e + // Call the latest decoder for it and return. If it encounters an older config format it should be able to recognise and convert by itself. + decodes.vp4p1mini_toml(toml) } - } - console.log("Starting server...") - case - bun.serve(ServeOptions( - development: Some(True), - hostname: conf.server_host, - port: conf.server_port, - static_served: ssrs.ssrs(mutmodel), - handler: web.handle_request(_, mutmodel), - id: None, - reuse_port: None, - )) { - Ok(..) -> { - console.log( - "Server started! Running on: " - <> premixed.text_cyan( - "http://" - <> option.unwrap(conf.server_host, "localhost") - <> ":" - <> int.to_string(option.unwrap(conf.server_port, 8080)) - <> "/", - ), - ) - global.set_interval(lease, fn() { - mutable_reference.update(mutmodel, fn(model) { - case model.cached_response { - None -> model - Some(..) -> - // Drops the cached response to keep it updated - mutable_model_type.MutableModelContent( - ..model, - cached_response: None, - ) - } - }) - }) - } - Error(e) -> { - console.error( - "A problem occurred while starting the server: " - <> premixed.text_error_red(string.inspect(e)), - ) + Ok(conf) -> conf + Error(_) -> { process.exit(1) - panic as "We should not reach here" + panic as "Shouldn't be here." } } + let content = { + todo + } - Nil + site_json.SiteJSON(config, content) } -fn static_site_server(mutmodel: mutable_model_type.MutableModel) { - console.info("Cynthia Mini is in pregeneration mode!") - - { - let folder = process.cwd() <> "/assets/cynthia-mini" - case simplifile.create_directory_all(folder) { - Ok(..) -> Nil - Error(e) -> { - console.error( - "A problem occurred while creating the ´" - <> folder - <> "´ directory: " - <> premixed.text_error_red(string.inspect(e)), - ) - process.exit(1) - panic as "We should not reach here" - } - } - - case files.file_exist(process.cwd() <> "/assets/cynthia-mini/README.md") { - True -> Nil - False -> { - case - simplifile.write( - process.cwd() <> "/assets/cynthia-mini/README.md", - "# What does this folder do?\n\r\n\rThis folder holds a few files Cynthia Mini serves to the browser to make sure everything works alright.\n\r\n\rThese are usually checked and downloaded if necessary only during start of the server,\n\rso try not to touch them! If you believe one of the files in here might be faulty, delete it, and restart the server.\n\r\n\rHave a nice day! :)", - ) - { - Ok(..) -> Nil - Error(e) -> { - console.error( - "A problem occurred while creating the ´" - <> process.cwd() - <> "/assets/cynthia-mini/README.md" - <> "´ file: " - <> premixed.text_error_red(string.inspect(e)), - ) - process.exit(1) - panic as "We should not reach here" - } - } - Nil - } - } - } - - use complete_data <- promise.await(config.load()) +import cynthia_websites_mini_shared/config/v4_1/decodes - // Generate JSON representations - let complete_data_json = - complete_data |> configtype.encode_complete_data_for_client - let res_string = complete_data_json |> json.to_string - let res_jsonld = jsonld.generate_jsonld(complete_data) - let opt_sitemap = sitemap.generate_sitemap(complete_data) +pub fn create_html(json: site_json.SiteJSON, path: String) { + " + + - let outdir = process.cwd() <> "/out" - case simplifile.create_directory_all(outdir) { - Ok(..) -> Nil - Error(e) -> { - console.error( - "A problem occurred while creating the ´out´ directory: " - <> premixed.text_error_red(string.inspect(e)), - ) - process.exit(1) - panic as "We should not reach here" - } - } - case simplifile.write(to: outdir <> "/site.json", contents: res_string) { - Ok(..) -> Nil - Error(e) -> { - console.error( - "A problem occurred while creating the ´site.json´ file: " - <> premixed.text_error_red(string.inspect(e)), - ) - process.exit(1) - panic as "We should not reach here" - } - } - case - simplifile.write( - to: outdir <> "/index.html", - contents: ssrs.index_html(mutable_reference.get(mutmodel)), - ) - { - Ok(..) -> Nil - Error(e) -> { - console.error( - "A problem occurred while creating the ´index.html´ file: " - <> premixed.text_error_red(string.inspect(e)), - ) - process.exit(1) - panic as "We should not reach here" - } - } - case - simplifile.copy_directory( - at: process.cwd() <> "/assets/", - to: outdir <> "/assets/", - ) - { - Ok(..) -> Nil - Error(e) -> { - console.error( - "A problem occurred while copying the assets directory: " - <> premixed.text_error_red(string.inspect(e)), - ) - process.exit(1) - panic as "We should not reach here" - } - } - case simplifile.is_file(outdir <> "/site.json") { - Ok(True) -> Nil - _ -> { - console.error( - "An unknown problem occurred while creating the ´site.json´ file.", - ) - process.exit(1) - panic as "We should not reach here" - } - } - case simplifile.is_file(outdir <> "/index.html") { - Ok(True) -> Nil - _ -> { - console.error( - "An unknown problem occurred while creating the ´index.html´ file.", - ) - process.exit(1) - panic as "We should not reach here" - } - } - case opt_sitemap { - None -> Nil - Some(res_sitemap) -> { - case - simplifile.write(to: outdir <> "/sitemap.xml", contents: res_sitemap) - { - Ok(..) -> Nil - Error(e) -> { - console.error( - "A problem occurred while creating the ´sitemap.xml´ file: " - <> premixed.text_error_red(string.inspect(e)), - ) - process.exit(1) - panic as "We should not reach here" - } - } - } - } + +<<site hosted by Cynthia mini>> + "/> + "/> +" <> case model.cached_jsonld { + Some(jsonld) -> + "" + None -> " " + } <> " + + + + + + + +
+ " <> first_view <> " +
+ " <> footer(True, json.config.integrations.git) <> " + + +" +} - console.info( - premixed.text_ok_green("Site pregeneration complete!") - <> " Serve files from " - <> premixed.text_orange(outdir <> "/") - <> " and you should have a site running!", - ) - promise.resolve(Nil) +fn init(forced: Bool) { + todo } diff --git a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config.gleam b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config.gleam deleted file mode 100644 index e2afaa7..0000000 --- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config.gleam +++ /dev/null @@ -1,711 +0,0 @@ -import bungibindies/bun -import bungibindies/bun/spawn -import cynthia_websites_mini_client/configtype -import cynthia_websites_mini_client/contenttypes -import cynthia_websites_mini_server/config/v4 -import cynthia_websites_mini_server/utils/files -import cynthia_websites_mini_server/utils/prompts -import gleam/bool -import gleam/dynamic/decode -import gleam/fetch -import gleam/float -import gleam/http/request -import gleam/int -import gleam/javascript/promise.{type Promise} -import gleam/json -import gleam/list -import gleam/option.{None, Some} -import gleam/result -import gleam/string -import gleamy_lights/premixed -import plinth/javascript/console -import plinth/node/fs -import plinth/node/process -import simplifile -import tom - -/// # Config.load() -/// Loads the configuration from the `cynthia.toml` file and the content from the `content` directory. -/// Then saves the configuration to the database. -pub fn load() -> Promise(configtype.CompleteData) { - use global_config <- promise.await(capture_config()) - use content_list <- promise.await(content_getter()) - let content = case content_list { - Ok(lis) -> lis - Error(msg) -> { - console.error("Error: There was an error getting content:\n" <> msg) - process.exit(1) - panic as "We should not reach here" - } - } - - let complete_data = configtype.merge(global_config, content) - complete_data - |> promise.resolve -} - -pub fn capture_config() { - let global_conf_filepath = files.path_join([process.cwd(), "/cynthia.toml"]) - let global_conf_filepath_exists = files.file_exist(global_conf_filepath) - - case global_conf_filepath_exists { - True -> Nil - False -> { - let global_conf_filepath_legacy = - files.path_join([process.cwd(), "/cynthia-mini.toml"]) - let global_conf_filepath_legacy_exists = - files.file_exist(global_conf_filepath_legacy) - case - global_conf_filepath_legacy_exists, - simplifile.read(global_conf_filepath_legacy) - { - True, Ok(legacy_config) -> { - console.warn( - "A legacy config file was found! Cynthia Mini will attempt to auto-convert it on the go and continue.", - ) - let upgraded_config = - "# This file was upgraded to the universal Cynthia Config format\n# Do not edit these two variables! They are set by Cynthia to tell it's config format apart.\nconfig.edition=\"mini\"\nconfig.version=4\n\n" - <> legacy_config - case - simplifile.write( - to: global_conf_filepath, - contents: upgraded_config, - ) - { - Ok(_) -> { - let _ = - simplifile.rename( - at: global_conf_filepath_legacy, - to: global_conf_filepath_legacy <> ".old", - ) - Nil - } - Error(_) -> { - console.error( - "Error: Could not write upgraded config to " - <> global_conf_filepath - <> ". Please check file permissions.", - ) - process.exit(1) - } - } - } - True, Error(_) -> { - console.error( - "Some error happened while trying to read " - <> global_conf_filepath_legacy - <> ".", - ) - process.exit(1) - } - False, _ -> { - dialog_initcfg() - process.exit(0) - } - } - } - } - let global_conf_content_sync = - simplifile.read(global_conf_filepath) |> result.unwrap("") - let m = case parse_config_format(global_conf_content_sync) { - // Correct config format: mini-4 - Ok(#("mini", 4)) -> v4.parse_mini() - - // Erronous config format outcomes - Ok(#(c, d)) -> promise_error_unknown_config_format(c, d) - Error(_) -> promise_error_cannot_read_config_format() - } - use parse_configtoml_result <- promise.await(m) - - let global_config = case parse_configtoml_result { - Ok(config) -> config - Error(why) -> { - premixed.text_error_red("Error: Could not load cynthia.toml: " <> why) - |> console.error - process.exit(1) - panic as "We should not reach here" - } - } - global_config - |> promise.resolve() -} - -fn promise_error_unknown_config_format( - edition: String, - version: Int, -) -> Promise(Result(a, String)) { - let err = - "Config version " - <> version |> int.to_string() - <> " with edition '" - <> edition - <> "' is NOT supported by this version of Cynthia." - <> "\n Usually this means one of these options:" - <> "\n - it was written for a different edition" - <> "\n - it is invalid" - <> "\n - or this version of cynthia is too old to understand this file." - <> case edition == "mini" { - True -> - "\n\n\n It seems to be that last option, since the edition it is written for, does match 'mini'." - False -> "" - } - promise.resolve(Error(err)) -} - -fn promise_error_cannot_read_config_format() { - promise.resolve(Error( - "Cannot properly read config.edition and/or config.version, Cynthia doesn't know how to parse this file anymore!", - )) -} - -fn parse_config_format(toml_str: String) -> Result(#(String, Int), Nil) { - use d <- result.try(tom.parse(toml_str) |> result.replace_error(Nil)) - use edition <- result.try( - tom.get_string(d, ["config", "edition"]) |> result.replace_error(Nil), - ) - use version <- result.try( - tom.get_int(d, ["config", "version"]) |> result.replace_error(Nil), - ) - Ok(#(edition, version)) -} - -fn content_getter() -> promise.Promise( - Result(List(contenttypes.Content), String), -) { - let promises: List(Promise(Result(contenttypes.Content, String))) = { - fn(file) { - file - |> string.replace(".meta.json", "") - |> files.path_normalize() - } - |> fn(value) { - list.map( - list.filter( - result.unwrap( - simplifile.get_files(files.path_join([process.cwd() <> "/content"])), - [], - ), - fn(file) { file |> string.ends_with(".meta.json") }, - ), - value, - ) - } - |> list.map(get_inner_and_meta) - } - promise.map(promise.await_list(promises), result.all) -} - -fn get_inner_and_meta( - file: String, -) -> Promise(Result(contenttypes.Content, String)) { - use meta_json <- promise.try_await( - simplifile.read(file <> ".meta.json") - |> result.replace_error( - "FS error while reading ´" <> file <> ".meta.json´.", - ) - |> promise.resolve, - ) - // Sometimes stuff is saved somewhere else, like in a different file path or maybe somewhere on the web, of course Cynthia Mini can still find those files! - // ...However, we first need to know there is an "external" file somewhere, we do that by checking the 'path' field. - // The extension before .meta.json is still used to parse the content. - let possibly_extern = - json.parse(meta_json, { - use path <- decode.optional_field("path", "", decode.string) - decode.success(path) - }) - |> result.unwrap("") - |> string.to_option - use permalink <- promise.try_await( - json.parse(meta_json, { - use path <- decode.optional_field("permalink", "", decode.string) - decode.success(path) - }) - |> result.replace_error("Could not decode permalink for ´" <> file <> "´") - |> promise.resolve, - ) - - use inner_plain <- promise.try_await({ - // This case also check if the permalink starts with "!", in which case it is a content list. - // Content lists will be generated on the client side, and their pre-given content - // will be discarded, so loading it in from anywhere would be a waste of resources. - case string.starts_with(permalink, "!"), possibly_extern { - True, _ -> promise.resolve(Ok("")) - False, None -> { - promise.resolve( - simplifile.read(file) - |> result.replace_error("FS error while reading ´" <> file <> "´."), - ) - } - False, Some(p) -> get_ext(p) - } - }) - - // Now, conversion to Djot for markdown files done in-place: - let converted: Result(#(String, String), String) = case - string.ends_with(file, "markdown") - |> bool.or( - string.ends_with(file, "md") |> bool.or(string.ends_with(file, "mdown")), - ) - { - True -> { - // If the file is external, we need to write it to a temporary file first. - let wri = case possibly_extern { - Some(..) -> { - simplifile.write(file, inner_plain) - |> result.replace_error( - "There was an error while writing the external content to '" - <> file |> premixed.text_bright_yellow() - <> "'.", - ) - } - None -> Ok(Nil) - } - use _ <- result.try(wri) - - use pandoc_path <- result.try(result.replace_error( - bun.which("pandoc"), - "There is a markdown file in Cynthia's content folder, but to convert that to Djot and display it, you need to have Pandoc installed on the PATH, which it is not!", - )) - let pandoc_child = - spawn.sync(spawn.OptionsToSubprocess( - [pandoc_path, file, "-f", "gfm", "-t", "djot"], - cwd: Some(process.cwd()), - env: None, - stderr: Some(spawn.Pipe), - stdout: Some(spawn.Pipe), - )) - let pandoc_child = case - { - let assert spawn.SyncSubprocess(asserted_sync_child) = pandoc_child - spawn.success(asserted_sync_child) - } - { - True -> Ok(pandoc_child) - False -> { - Error( - "There was an error while trying to convert '" - <> file |> premixed.text_bright_yellow() - <> "' to Djot: \n" - <> result.unwrap(spawn.stderr(pandoc_child), "") - <> "\n\nMake sure you have at least Pandoc 3.7.0 installed on your system, earlier versions may not work correctly.", - ) - } - } - use pandoc_child <- result.try(pandoc_child) - let new_inner_plain: Result(String, String) = - spawn.stdout(pandoc_child) - |> result.replace_error("") - use new_inner_plain <- result.try(new_inner_plain) - - // If the file was external, we need delete the temporary file. - let re = case possibly_extern { - Some(..) -> { - simplifile.delete(file) - |> result.replace_error( - "There was an error while deleting the temporary file '" - <> file |> premixed.text_bright_yellow() - <> "'.", - ) - } - None -> Ok(Nil) - } - - use _ <- result.try(re) - - Ok(#(new_inner_plain, file <> ".dj")) - } - - False -> { - Ok(#(inner_plain, file)) - } - } - - let metadata = case converted { - Ok(#(inner_plain, file)) -> { - let decoder = contenttypes.content_decoder_and_merger(inner_plain, file) - json.parse(meta_json, decoder) - |> result.map_error(fn(e) { - "Some error decoding metadata for ´" - <> file |> premixed.text_magenta() - <> "´: " - <> string.inspect(e) - }) - } - Error(l) -> Error(l) - } - - promise.resolve(metadata) -} - -/// Gets external content, beit by file path or by http(s) url. -fn get_ext(path: String) -> promise.Promise(Result(String, String)) { - case string.starts_with(string.lowercase(path), "http") { - True -> { - let start = bun.nanoseconds() - console.log( - "Downloading external content ´" <> premixed.text_blue(path) <> "´...", - ) - - let assert Ok(req) = request.to(path) - use resp <- promise.try_await( - promise.map(fetch.send(req), fn(e) { - result.replace_error( - e, - "Error while downloading external content ´" - <> path - <> "´: " - <> string.inspect(e), - ) - }), - ) - use resp <- promise.try_await( - promise.map(fetch.read_text_body(resp), fn(e) { - result.replace_error( - e, - "Error while reading external content ´" - <> path - <> "´: " - <> string.inspect(e), - ) - }), - ) - let end = bun.nanoseconds() - let duration_ms = { end -. start } /. 1_000_000.0 - case resp.status { - 200 -> { - console.log( - "Downloaded external content ´" - <> premixed.text_blue(path) - <> "´ in " - <> int.to_string(duration_ms |> float.truncate) - <> "ms!", - ) - Ok(resp.body) - } - _ -> { - Error( - "Error while downloading external content ´" - <> path - <> "´: " - <> string.inspect(resp.status), - ) - } - } - |> promise.resolve - } - False -> { - // Is a file path - promise.resolve( - simplifile.read(path) - |> result.replace_error( - "FS error while reading external content file ´" <> path <> "´.", - ), - ) - } - } -} - -fn dialog_initcfg() { - console.log("No Cynthia Mini configuration found...") - case - prompts.for_confirmation( - "CynthiaMini can create \n" - <> premixed.text_orange(process.cwd() <> "/cynthia.toml") - <> "\n ...and some sample content.\n" - <> premixed.text_magenta( - "Do you want to initialise new config at this location?", - ), - True, - ) - { - False -> { - console.error("No Cynthia Mini configuration found... Exiting.") - process.exit(1) - panic as "We should not reach here" - } - True -> initcfg() - } -} - -const brand_new_config = "# Do not edit these variables! It is set by Cynthia to tell it's config format apart. -config.edition=\"mini\" -config.version=4 -[global] -# Theme to use for light mode - default themes: autumn, default -theme = \"autumn\" -# Theme to use for dark mode - default themes: night, default-dark -theme_dark = \"night\" -# For some browsers, this will change the colour of UI elements such as the address bar -# and the status bar on mobile devices. -# This is a hex colour, e.g. #FFFFFF -colour = \"#FFFFFF\" -# Your website's name, displayed in various places -site_name = \"My Site\" -# A brief description of your website -site_description = \"A big site on a mini Cynthia!\" - -[server] -# Port number for the web server -port = 8080 -# Host address for the web server -host = \"localhost\" - -[integrations] -# Enable git integration for the website -# This will allow Cynthia Mini to detect the git repository -# For example linking to the commit hash in the footer -git = true - -# Enable sitemap generation -# This will generate a sitemap.xml file in the root of the website -# -# You will need to enter the base URL of your website in the sitemap variable below. -# If your homepage is at \"https://example.com/#/\", then the sitemap variable should be set to \"https://example.com\". -# If you do not want to use a sitemap, set this to \"false\", or leave it empty (\"\"), you can also remove the sitemap variable altogether. -sitemap = \"\" - -# Enable crawlable context (JSON-LD injection) -# This will allow search engines to crawl the website, and makes it -# possible for the website to be indexed by search engine and LLMs. -crawlable_context = false - -[variables] -# You can define your own variables here, which can be used in templates. - -## ownit_template -## -## Use this to define your own template for the 'ownit' layout. -## -## The template will be used for the 'ownit' layout, which is used for pages and posts. -## You can use the following variables in the template: -## - body: string (The main HTML content) -## - is_post: boolean (True if the current item is a post, false if it's a page) -## - title: string (The title of the page or post) -## - description: string (The description of the page or post) -## - site_name: string (The global site name) -## - category: string (The category of the post, empty for pages) -## - date_modified: string (The last modification date of the post, empty for pages) -## - date_published: string (The publication date of the post, empty for pages) -## - tags: string[] (An array of tags for the post, empty for pages) -## - menu_1_items: [string, string][] (Array of menu items for menu 1, e.g., [[\"Home\", \"/\"], [\"About\", \"/about\"]]) -## - menu_2_items: [string, string][] (Array of menu items for menu 2) -## - menu_3_items: [string, string][] (Array of menu items for menu 3) -ownit_template = \"\"\" -
-

{{ 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..f9169c6 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,7 +3,6 @@ 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/utils/files.{client_css, client_js} @@ -15,7 +14,6 @@ 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 @@ -28,48 +26,6 @@ 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) { response.new() |> response.set_body(index_html(model)) @@ -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/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" } }