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.
-
- Contact
-
- mailto:info@example.com
-
- or see
-
- https://example.com/readme.md
-
- .
-
- Visit https://no-brackets.example/path and http://another.example/query?x=1.
-
- 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
-
- Quote line 1 with ref Quote line 2.
- Outside paragraph with
-
- https://outer.example/path
-
- .
-
- This is a test paragraph.
- Task item Completed task This is a blockquote
- Another paragraph.
-
- Inline code like
-
- Regular text with
-
- https://linked.example
-
- after code.
-
- Unclosed [link(https://bad.example
-
- Mismatched **bold and _italic
-
-
-
- https://ok.example
-
- Task with link Completed task with another link Blockquote with a link
- A tricky
-
- link
-
- parens). Also
-
- https://example.com/trail
-
- , and a sentence ending link
-
- https://end.example
-
- .
-
- Paragraph one with
-
- https://one.example
-
-
- Paragraph two with
-
- https://two.example/path?x=1
-
- and
-
- brackets
-
- .
-
- Parent 1
-- Child with
-
- link
-
-
-- Child with *
-
- bold
-
- * and
-
- italic
-
-
- Parent 2
- Ordered child with another Second ordered child
- Paragraph with Windows newlines.
-
- Item A
-
- Item B
-
- Hello World
-
-
- curl https://api.example.com
-
- should not autolink.
-
-
-
- # A fenced code block containing an URL
-wget https://downloads.example.com/archive.tar.gz
-
-
-
-
-
- Title
-
-
-
-
Numbered lists
Images:
Also quote blocks!
-StrawmelonJuice
- A task list: -
-Task 1
Task 2
Task 3
- A bullet list: -
-- Point 1 -
-- Point 2 -
-
-
-
- MYFILE.BASH
-
- echo "Code blocks!"
-// - StrawmelonJuice
-
-
-
- - A small table: -
-Column 1 | Column 2 |
|---|---|
Value 1 | Value 2 |
Numbered lists
Images:
Also quote blocks!
-StrawmelonJuice
- A task list: -
-Task 1
Task 2
Task 3
- A bullet list: -
-- Point 1 -
-- Point 2 -
-
-
-
- MYFILE.BASH
-
- echo "Code blocks!"
-// - StrawmelonJuice
-
-
-
- - A small table: -
-Column 1 | Column 2 |
|---|---|
Value 1 | Value 2 |
First item with link
Second item with another link
Third item with *bold* and link
First item with *bold* and link
Second with italic and https://second.example
Third with inline code and another
- This is a test paragraph. -
-- Item 1 -
-- Item 2 -
-Task todo
Task done
Another with link
Done with docs
tag. Then it can be pasted into the template.
- //
- Ok("txt") -> html.pre([], [html.text(inner)])
- // Anything else is wrapped in a tag with a red color. Then it can be pasted into the template. This shows that the file type is not supported.
- _ ->
- html.div([], [
- html.text("Unsupported file type: "),
- html.text(filename),
- html.pre([attribute.class("text-red-500")], [
- html.text(string.inspect(inner)),
- ]),
- ])
- }
-}
-
-@external(javascript, "./dom.ts", "destroy_comment_box")
-pub fn destroy_comment_box() -> Nil
-
-@external(javascript, "./dom.ts", "apply_styles_to_comment_box")
-pub fn comment_box_forced_styles() -> Nil
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/djotparse.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/djotparse.gleam
deleted file mode 100644
index 5bc1aba..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/djotparse.gleam
+++ /dev/null
@@ -1,1026 +0,0 @@
-//// Djot is parsed using jot, then converted to lustre elements using jotkey/jot_to_lustre's logic, slightly modified to fit Cynthia's needs.
-
-import cynthia_websites_mini_client/utils
-import gleam/dict.{type Dict}
-import gleam/int
-import gleam/list
-import gleam/option.{None, Some}
-import gleam/string
-import jot.{
- type Container, type Destination, type Document, type Inline, Code, Codeblock,
- Emphasis, Heading, Image, Linebreak, Link, Paragraph, Reference, Strong, Text,
- Url,
-}
-import lustre/attribute
-import lustre/element.{type Element}
-import lustre/element/html
-
-pub fn entry_to_conversion(djot: String) -> List(Element(msg)) {
- let preprocessed = preprocess_djot_extensions(djot)
- let parsed = jot.parse(preprocessed)
- document_to_lustre(parsed)
-}
-
-pub fn preprocess_djot_extensions(djot: String) -> String {
- djot
- // Normalize line endings
- |> string.replace("\r\n", "\n")
- |> string.replace("\r", "\n")
- // Convert escaped exclamation marks
- |> string.replace("\\!", "!")
- // Remove leading/trailing whitespace
- |> string.trim()
- //
- // Now that we've normalized the input, we can preprocess it further
- //
- // Process heading attributes first to ensure IDs are attached correctly
- |> preprocess_heading_attributes
- // Fix multiline images
- |> preprocess_multiline_images
- // Preprocess tables BEFORE autolinks so autolinks don't break table structure
- |> preprocess_tables
- // Preprocess autolinks
- |> preprocess_autolinks
- // Preprocess ordered lists
- |> preprocess_ordered_lists
- // Preprocess blockquotes
- |> preprocess_blockquotes
- // Preprocess task lists
- |> preprocess_task_lists
- // And then out it goes!
-}
-
-fn document_to_lustre(document: Document) -> List(Element(msg)) {
- let elements = containers_to_lustre(document.content, document.references, [])
-
- // Add footnotes section if any footnotes exist
- let elements_with_footnotes = case dict.size(document.footnotes) > 0 {
- True -> {
- let footnotes_section =
- create_footnotes_section(document.footnotes, document.references)
- list.append(elements, [footnotes_section])
- }
- False -> elements
- }
-
- list.reverse(elements_with_footnotes)
-}
-
-fn containers_to_lustre(
- containers containers: List(Container),
- refs refs: Dict(String, String),
- elements elements: List(Element(msg)),
-) -> List(Element(msg)) {
- case containers {
- [] -> elements
- [container, ..rest] -> {
- let elements = container_to_lustre(elements, container, refs)
- containers_to_lustre(rest, refs, elements)
- }
- }
-}
-
-fn container_to_lustre(
- elements: List(Element(msg)),
- container: Container,
- refs: Dict(String, String),
-) {
- let element = case container {
- Paragraph(attrs, inlines) -> {
- // Regular paragraph
- let in_a_list = case refs |> dict.get("am I in a list?") {
- Ok(..) -> True
- _ -> False
- }
-
- html.p(
- attributes_to_lustre(attrs, [
- case in_a_list {
- False -> attribute.class("mb-2")
- True -> attribute.class("whitespace-nowrap")
- },
- ]),
- inlines_to_lustre([], inlines, refs),
- )
- }
- Heading(attrs, level, inlines) -> {
- // Clean heading text to remove {#id} markup
- let clean_inlines = clean_heading_text(inlines)
-
- case level {
- 1 ->
- html.h1(
- attributes_to_lustre(attrs, [
- attribute.class("text-4xl font-bold text-accent"),
- ]),
- inlines_to_lustre([], clean_inlines, refs),
- )
- 2 ->
- html.h2(
- attributes_to_lustre(attrs, [
- attribute.class("text-3xl font-bold text-accent"),
- ]),
- inlines_to_lustre([], clean_inlines, refs),
- )
- 3 ->
- html.h3(
- attributes_to_lustre(attrs, [
- attribute.class("text-2xl font-bold text-accent"),
- ]),
- inlines_to_lustre([], clean_inlines, refs),
- )
- 4 ->
- html.h4(
- attributes_to_lustre(attrs, [
- attribute.class("text-xl font-bold text-accent"),
- ]),
- inlines_to_lustre([], clean_inlines, refs),
- )
- 5 ->
- html.h5(
- attributes_to_lustre(attrs, [
- attribute.class("text-lg font-bold text-accent"),
- ]),
- inlines_to_lustre([], clean_inlines, refs),
- )
- _ ->
- html.h6(
- attributes_to_lustre(attrs, [
- attribute.class("font-bold text-accent"),
- ]),
- inlines_to_lustre([], clean_inlines, refs),
- )
- }
- }
- Codeblock(attrs, language, content) -> {
- html.pre(
- attributes_to_lustre(attrs, [
- attribute.class(
- "bg-neutral text-neutral-content pl-4 block ml-2 mr-2 overflow-x-auto break-none whitespace-pre-wrap font-mono border-dotted border-2 border-neutral-content rounded-lg",
- ),
- ]),
- [
- html.code(
- case language {
- Some(lang) -> [
- attribute.class("language-" <> lang),
- attribute.attribute("data-language", lang),
- ]
- None -> []
- }
- |> list.append([
- attribute.class(
- "bg-neutral text-neutral-content p-1 rounded-lg",
- ),
- ]),
- [
- case language {
- Some(lang) ->
- html.div(
- [
- attribute.class(
- "text-xs text-neutral-content opacity-70 mb-2",
- ),
- ],
- [html.text(string.uppercase(lang))],
- )
- None -> element.none()
- },
- html.text(content),
- ]
- |> list.filter(fn(el) { el != element.none() }),
- ),
- ],
- )
- }
- jot.BulletList(layout:, style: _, items:) -> {
- html.ul(
- [
- attribute.class(
- "list-disc"
- <> {
- " leading-"
- <> case layout {
- jot.Loose -> "loose"
- jot.Tight -> "tight"
- }
- },
- ),
- ],
- list.map(items, fn(item) {
- html.li(
- [],
- containers_to_lustre(
- containers: item,
- refs: refs |> dict.insert("am I in a list?", "yes."),
- elements: [],
- ),
- )
- }),
- )
- }
- // Raw blocks are perfect for our preprocessed HTML
- jot.RawBlock(content:) ->
- element.unsafe_raw_html(
- "div",
- "div",
- [attribute.class("djot-processed-content")],
- content,
- )
- jot.ThematicBreak ->
- html.hr([
- attribute.class("w-48 h-1 mx-auto my-4 border-0 rounded-sm md:my-10"),
- ])
- }
- [element, ..elements]
-}
-
-fn inlines_to_lustre(
- elements: List(Element(msg)),
- inlines: List(Inline),
- refs: Dict(String, String),
-) -> List(Element(msg)) {
- case inlines {
- [] -> list.reverse(elements)
- [inline, ..rest] -> {
- let new_elements = inline_to_lustre([], inline, refs)
- inlines_to_lustre(list.append(new_elements, elements), rest, refs)
- }
- }
-}
-
-fn inline_to_lustre(
- elements: List(Element(msg)),
- inline: Inline,
- refs: Dict(String, String),
-) {
- case inline {
- Linebreak -> [html.br([])]
- Text(text) -> [html.text(text)]
- Strong(inlines) -> {
- [
- html.strong(
- [attribute.class("font-bold")],
- inlines_to_lustre(elements, inlines, refs),
- ),
- ]
- }
- Emphasis(inlines) -> {
- [
- html.em(
- [attribute.class("italic")],
- inlines_to_lustre(elements, inlines, refs),
- ),
- ]
- }
- Link(text, destination) -> {
- [
- case destination {
- Url(url) -> {
- html.a(
- [
- attribute.class("text-info underline"),
- attribute.href({
- case
- string.starts_with(url, "/")
- && !string.starts_with(url, utils.phone_home_url() <> "#")
- {
- True -> utils.phone_home_url() <> "#" <> url
- False -> url
- }
- }),
- ],
- inlines_to_lustre(elements, text, refs),
- )
- }
- Reference(ref_id) -> {
- case dict.get(refs, ref_id) {
- Ok(url) -> {
- html.a(
- [
- attribute.class("text-info underline"),
- attribute.href({
- case
- string.starts_with(url, "/")
- && !string.starts_with(
- url,
- utils.phone_home_url() <> "#",
- )
- {
- True -> utils.phone_home_url() <> "#" <> url
- False -> url
- }
- }),
- ],
- inlines_to_lustre(elements, text, refs),
- )
- }
- Error(_) -> {
- html.span([attribute.class("text-error")], [
- html.text("[Reference not found: " <> ref_id <> "]"),
- ])
- }
- }
- }
- },
- ]
- }
- Image(text, destination) -> {
- [
- html.img([
- attribute.src(destination_attribute(destination, refs)),
- attribute.alt(take_inline_text(text, "")),
- ]),
- ]
- }
- Code(content) -> {
- [
- html.code(
- [
- attribute.class(
- "bg-neutral text-neutral-content p-1 rounded-lg mt-4 mb-4",
- ),
- ],
- [html.text(content)],
- ),
- ]
- }
- jot.Footnote(reference: reference) -> {
- [
- html.a(
- [
- attribute.href("#fn:" <> reference),
- attribute.id("fnref:" <> reference),
- attribute.class("text-info text-xs align-super"),
- attribute.attribute("role", "doc-noteref"),
- ],
- [html.text("[" <> reference <> "]")],
- ),
- ]
- }
- jot.MathDisplay(content: content) -> {
- [
- element.unsafe_raw_html(
- "div",
- "div",
- [attribute.class("math-display my-4 text-center overflow-x-auto")],
- "\\[" <> content <> "\\]",
- ),
- ]
- }
- jot.MathInline(content: content) -> {
- [
- element.unsafe_raw_html(
- "span",
- "span",
- [attribute.class("math-inline")],
- "\\(" <> content <> "\\)",
- ),
- ]
- }
- jot.NonBreakingSpace -> [html.text(" ")]
- }
-}
-
-fn destination_attribute(destination: Destination, refs: Dict(String, String)) {
- case destination {
- Url(url) -> url
- Reference(id) ->
- case dict.get(refs, id) {
- Ok(url) -> url
- Error(Nil) -> ""
- }
- }
-}
-
-fn take_inline_text(inlines: List(Inline), acc: String) -> String {
- case inlines {
- [] -> acc
- [first, ..rest] ->
- case first {
- Text(text) | Code(text) -> take_inline_text(rest, acc <> text)
- Strong(inlines) | Emphasis(inlines) ->
- take_inline_text(list.append(inlines, rest), acc)
- Link(nested, _) | Image(nested, _) -> {
- let acc = take_inline_text(nested, acc)
- take_inline_text(rest, acc)
- }
- Linebreak -> {
- take_inline_text(rest, acc)
- }
- jot.Footnote(reference: reference) -> "[" <> reference <> "]"
- jot.MathDisplay(content: content) -> content
- jot.MathInline(content: content) -> content
- jot.NonBreakingSpace ->
- // Non-breaking space.
- " "
- }
- }
-}
-
-fn attributes_to_lustre(attributes: Dict(String, String), lustre_attributes) {
- attributes
- |> dict.to_list
- |> list.sort(fn(a, b) { string.compare(a.0, b.0) })
- |> list.fold(lustre_attributes, fn(lustre_attributes, pair) {
- [attribute.attribute(pair.0, pair.1), ..lustre_attributes]
- })
-}
-
-fn create_footnotes_section(
- footnotes: Dict(String, List(Container)),
- refs: Dict(String, String),
-) -> Element(msg) {
- case dict.size(footnotes) > 0 {
- True -> {
- html.section(
- [attribute.class("footnotes mt-8 pt-4 border-t border-neutral-content")],
- [
- html.h2([attribute.class("text-xl font-bold text-accent mb-4")], [
- html.text("Footnotes"),
- ]),
- html.ol(
- [attribute.class("list-decimal list-inside space-y-2")],
- footnotes
- |> dict.to_list
- |> list.map(fn(footnote) {
- let #(id, containers) = footnote
- html.li(
- [attribute.id("fn:" <> id), attribute.class("text-sm")],
- list.append(containers_to_lustre(containers, refs, []), [
- html.a(
- [
- attribute.href("#fnref:" <> id),
- attribute.class("text-info ml-2"),
- attribute.attribute("role", "doc-backlink"),
- ],
- [html.text("↩")],
- ),
- ]),
- )
- }),
- ),
- ],
- )
- }
- False -> element.none()
- }
-}
-
-fn preprocess_tables(djot: String) -> String {
- let lines = string.split(djot, "\n")
- process_table_lines(lines, False, [])
- |> string.join("\n")
-}
-
-fn process_table_lines(
- lines: List(String),
- in_table: Bool,
- table_buffer: List(String),
-) -> List(String) {
- case lines {
- [] ->
- case in_table {
- True -> [convert_table_to_raw(list.reverse(table_buffer))]
- False -> []
- }
-
- [line, ..rest] -> {
- let trimmed = string.trim(line)
- let is_table_line = string.contains(line, "|") && trimmed != ""
- let is_separator =
- string.contains(line, "|") && string.contains(line, "-")
-
- case in_table, is_table_line || is_separator {
- True, True -> process_table_lines(rest, True, [line, ..table_buffer])
-
- True, False -> {
- // End of table - process accumulated buffer
- case list.reverse(table_buffer) {
- [] -> [line, ..process_table_lines(rest, False, [])]
- table_lines -> [
- convert_table_to_raw(table_lines),
- line,
- ..process_table_lines(rest, False, [])
- ]
- }
- }
-
- False, True -> {
- // Start of new table
- process_table_lines(rest, True, [line])
- }
-
- False, False -> [line, ..process_table_lines(rest, False, [])]
- }
- }
- }
-}
-
-fn convert_table_to_raw(lines: List(String)) -> String {
- case lines {
- [] -> ""
- [single_line] -> single_line
- lines -> {
- // Find the separator line (contains both | and - and looks like a separator)
- let separator_index =
- list.index_fold(lines, None, fn(acc, line, index) {
- case acc {
- Some(_) -> acc
- None -> {
- let trimmed = string.trim(line)
- let has_pipes = string.contains(line, "|")
- let has_dashes = string.contains(line, "-")
- // A separator should be mostly dashes and pipes with minimal other content
- let is_likely_separator =
- has_pipes
- && has_dashes
- && {
- trimmed
- |> string.to_graphemes
- |> list.all(fn(char) {
- char == "|" || char == "-" || char == " " || char == ":"
- })
- }
-
- case is_likely_separator {
- True -> Some(index)
- False -> None
- }
- }
- }
- })
-
- case separator_index {
- None -> string.join(lines, "\n")
- // No valid separator found
- Some(sep_index) -> {
- // Split into header, separator, and rows
- let header_lines = list.take(lines, sep_index)
- let remaining = list.drop(lines, sep_index + 1)
-
- case header_lines {
- [] -> string.join(lines, "\n")
- // No header
- _ -> {
- // Use the last header line if there are multiple
- let actual_header = case list.reverse(header_lines) {
- [last_header, ..] -> last_header
- [] -> ""
- // Should not happen since header_lines is not empty
- }
-
- let header_cells =
- actual_header
- |> string.split("|")
- |> list.map(string.trim)
- |> list.filter(fn(cell) { cell != "" })
-
- // Validate that we have at least some header cells
- case list.length(header_cells) {
- 0 -> string.join(lines, "\n")
- // No valid header cells
- _ -> {
- let data_rows =
- remaining
- |> list.map(fn(row) {
- row
- |> string.split("|")
- |> list.map(string.trim)
- |> list.filter(fn(cell) { cell != "" })
- })
- |> list.filter(fn(row) { list.length(row) > 0 })
-
- let header_elements = {
- list.map(header_cells, fn(cell) {
- html.th(
- [attribute.class("px-4 py-2 text-left font-bold")],
- entry_to_conversion(cell),
- )
- })
- }
- let row_elements = {
- list.map(data_rows, fn(row) {
- html.tr([], {
- list.map(row, fn(cell) {
- html.td(
- [
- attribute.class(
- "px-4 py-2 border-t border-neutral-content",
- ),
- ],
- entry_to_conversion(cell),
- )
- })
- })
- })
- }
-
- html.table(
- [
- attribute.class(
- "table table-zebra w-full my-4 border border-neutral-content",
- ),
- ],
- [
- html.thead(
- [attribute.class("bg-neutral text-neutral-content")],
- [html.tr([], header_elements)],
- ),
- html.tbody([], row_elements),
- ],
- )
- |> element_to_raw_djotstring
- }
- }
- }
- }
- }
- }
- }
- }
-}
-
-fn preprocess_blockquotes(djot: String) -> String {
- // Process blockquotes as groups, not individual lines
- let lines = string.split(djot, "\n")
- process_blockquote_lines(lines, [], [], False)
- |> string.join("\n")
-}
-
-fn process_blockquote_lines(
- lines: List(String),
- processed: List(String),
- blockquote_buffer: List(String),
- in_blockquote: Bool,
-) -> List(String) {
- case lines {
- [] -> {
- // If we have a blockquote buffer at the end, process it
- case in_blockquote {
- True -> {
- let blockquote =
- convert_blockquote_to_raw(list.reverse(blockquote_buffer))
- list.reverse([blockquote, ..processed])
- }
- False -> list.reverse(processed)
- }
- }
-
- [line, ..rest] -> {
- let trimmed = string.trim(line)
- let is_blockquote_line = string.starts_with(trimmed, "> ")
-
- case in_blockquote, is_blockquote_line {
- // Continue collecting blockquote lines
- True, True -> {
- let content = string.drop_start(trimmed, 2) |> string.trim()
- process_blockquote_lines(
- rest,
- processed,
- [content, ..blockquote_buffer],
- True,
- )
- }
-
- // End of blockquote
- True, False -> {
- let blockquote =
- convert_blockquote_to_raw(list.reverse(blockquote_buffer))
- process_blockquote_lines(rest, [blockquote, ..processed], [], False)
- }
-
- // Start of blockquote
- False, True -> {
- let content = string.drop_start(trimmed, 2) |> string.trim()
- process_blockquote_lines(rest, processed, [content], True)
- }
-
- // Regular line, not in blockquote
- False, False -> {
- process_blockquote_lines(rest, [line, ..processed], [], False)
- }
- }
- }
- }
-}
-
-fn convert_blockquote_to_raw(lines: List(String)) -> String {
- let content =
- lines
- |> list.map(fn(line) {
- case line {
- // Any line without content should be an empty line broken.
- "" -> "\n"
- " " -> "\n"
- "\\" -> ""
- _ -> line
- }
- })
- |> string.join("\n")
-
- html.blockquote(
- [
- attribute.class(
- "border-l-4 border-accent border-dotted pl-4 bg-secondary bg-opacity-10 mb-4 mt-4",
- ),
- ],
- entry_to_conversion(content),
- )
- |> element_to_raw_djotstring
-}
-
-/// Converts a Lustre element to a raw block Djot representation.
-fn element_to_raw_djotstring(elm: element.Element(a)) {
- "\n```=html\n" <> { elm |> element.to_string } <> "\n```\n"
-}
-
-fn preprocess_task_lists(djot: String) -> String {
- djot
- |> string.split("\n")
- |> list.map(fn(line) {
- let trimmed = string.trim(line)
- case string.starts_with(trimmed, "- [ ] ") {
- True -> {
- let content = string.drop_start(trimmed, 6)
- {
- html.div([attribute.class("flex items-center mb-2")], [
- html.input([
- attribute.type_("checkbox"),
- attribute.disabled(True),
- attribute.class("mr-2 accent-primary"),
- ]),
- html.span([], entry_to_conversion(content)),
- ])
- }
- |> element_to_raw_djotstring
- }
- False ->
- case
- string.starts_with(trimmed, "- [x] ")
- || string.starts_with(trimmed, "- [X] ")
- {
- True -> {
- let content = string.drop_start(trimmed, 6)
- {
- html.div([attribute.class("flex items-center mb-2")], [
- html.input([
- attribute.type_("checkbox"),
- attribute.checked(True),
- attribute.disabled(True),
- attribute.class("mr-2 accent-primary"),
- ]),
- html.span([], entry_to_conversion(content)),
- ])
- }
- |> element_to_raw_djotstring
- }
- False -> line
- }
- }
- })
- |> string.join("\n")
-}
-
-fn preprocess_autolinks(djot: String) -> String {
- // Convert to [url](url) format for proper Djot parsing
- djot
- |> string.replace(" string.replace(" string.replace(" string.replace(" process_autolink_markers()
-}
-
-fn process_autolink_markers(input: String) -> String {
- case string.split_once(input, "🔗AUTOLINK🔗") {
- Ok(#(before, after)) -> {
- case string.split_once(after, ">") {
- Ok(#(url, rest)) ->
- before
- <> "["
- <> url
- <> "]("
- <> url
- <> ")"
- <> process_autolink_markers(rest)
- Error(_) -> before <> "<" <> after
- // Restore if no closing >
- }
- }
- Error(_) -> input
- // No markers found
- }
-}
-
-fn preprocess_ordered_lists(djot: String) -> String {
- let lines = string.split(djot, "\n")
- process_ordered_list_lines(lines, False, [])
- |> string.join("\n")
-}
-
-fn process_ordered_list_lines(
- lines: List(String),
- in_list: Bool,
- list_buffer: List(String),
-) -> List(String) {
- case lines {
- [] ->
- case in_list {
- True -> [convert_ordered_list_to_raw(list.reverse(list_buffer))]
- False -> []
- }
-
- [line, ..rest] -> {
- let trimmed = string.trim(line)
- let is_list_item = is_ordered_list_item(trimmed)
-
- case in_list, is_list_item {
- True, True ->
- process_ordered_list_lines(rest, True, [line, ..list_buffer])
-
- True, False -> [
- convert_ordered_list_to_raw(list.reverse(list_buffer)),
- line,
- ..process_ordered_list_lines(rest, False, [])
- ]
-
- False, True -> process_ordered_list_lines(rest, True, [line])
-
- False, False -> [line, ..process_ordered_list_lines(rest, False, [])]
- }
- }
- }
-}
-
-fn is_ordered_list_item(line: String) -> Bool {
- case string.split_once(line, ". ") {
- Ok(#(num_str, _)) -> {
- case int.parse(string.trim(num_str)) {
- Ok(_) -> True
- Error(_) -> False
- }
- }
- Error(_) -> False
- }
-}
-
-fn convert_ordered_list_to_raw(lines: List(String)) -> String {
- case lines {
- [] -> ""
- _ -> {
- let list_items =
- lines
- |> list.map(fn(line) {
- case string.split_once(string.trim(line), ". ") {
- Ok(#(_, content)) -> html.li([], entry_to_conversion(content))
- Error(_) -> html.li([], [html.text(line)])
- }
- })
-
- html.ol([attribute.class("list-decimal mb-4")], list_items)
- |> element_to_raw_djotstring
- }
- }
-}
-
-fn preprocess_multiline_images(djot: String) -> String {
- string.split(djot, "\n")
- |> process_multiline_image_lines([], "")
- |> string.join("\n")
-}
-
-fn process_multiline_image_lines(
- lines: List(String),
- processed: List(String),
- buffer: String,
-) -> List(String) {
- case lines {
- [] -> list.reverse(processed)
-
- [line, ..rest] -> {
- let has_image_start = string.contains(line, "![")
- let has_image_end = string.contains(line, ")")
- let has_incomplete_image = has_image_start && !has_image_end
-
- case buffer {
- // No buffer yet
- "" -> {
- case has_incomplete_image {
- // Start collecting a multiline image
- True -> process_multiline_image_lines(rest, processed, line)
-
- // Normal line
- False ->
- process_multiline_image_lines(rest, [line, ..processed], "")
- }
- }
-
- // Continue collecting an existing multiline image
- _ -> {
- // If we have a buffer, we're in the middle of processing a multiline image
- // We need to combine all lines until we find the closing parenthesis
- case has_image_end {
- // Complete the image and add to processed
- True -> {
- let complete_line = buffer <> " " <> line
- process_multiline_image_lines(
- rest,
- [complete_line, ..processed],
- "",
- )
- }
- // Continue buffering
- False -> {
- process_multiline_image_lines(
- rest,
- processed,
- buffer <> " " <> line,
- )
- }
- }
- }
- }
- }
- }
-}
-
-// Process heading attributes like {#id} before headings
-fn preprocess_heading_attributes(djot: String) -> String {
- let lines = string.split(djot, "\n")
- let processed = process_heading_attribute_lines(lines, [])
- string.join(processed, "\n")
-}
-
-fn process_heading_attribute_lines(
- lines: List(String),
- processed: List(String),
-) -> List(String) {
- case lines {
- [] -> list.reverse(processed)
-
- [line, ..rest] -> {
- // Check for attribute pattern {#something} followed by heading
- let is_attribute =
- string.starts_with(string.trim(line), "{#")
- && string.contains(line, "}")
-
- case is_attribute, rest {
- True, [next, ..next_rest] -> {
- let next_trimmed = string.trim(next)
- // Check if next line is a heading
- case
- string.starts_with(next_trimmed, "# ")
- || string.starts_with(next_trimmed, "## ")
- || string.starts_with(next_trimmed, "### ")
- || string.starts_with(next_trimmed, "#### ")
- || string.starts_with(next_trimmed, "##### ")
- || string.starts_with(next_trimmed, "###### ")
- {
- True -> {
- // Extract ID from {#id}
- case string.split_once(line, "{#") {
- Ok(#(_, with_id)) -> {
- case string.split_once(with_id, "}") {
- Ok(#(id, _)) -> {
- let modified_heading = next <> " {#" <> id <> "}"
- process_heading_attribute_lines(next_rest, [
- modified_heading,
- ..processed
- ])
- }
- Error(_) ->
- process_heading_attribute_lines(rest, [line, ..processed])
- }
- }
- Error(_) ->
- process_heading_attribute_lines(rest, [line, ..processed])
- }
- }
-
- False -> process_heading_attribute_lines(rest, [line, ..processed])
- }
- }
-
- _, _ -> process_heading_attribute_lines(rest, [line, ..processed])
- }
- }
- }
-}
-
-// Clean heading text by removing any {#id} attributes
-fn clean_heading_text(inlines: List(Inline)) -> List(Inline) {
- inlines
- |> list.map(fn(inline) {
- case inline {
- Text(text) -> {
- // Remove {#id} pattern from the text
- case string.split_once(text, " {#") {
- Ok(#(content, _)) -> Text(content)
- Error(_) -> inline
- }
- }
- _ -> inline
- }
- })
-}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds.gleam
index 3ed7186..28a8096 100644
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds.gleam
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds.gleam
@@ -1,6 +1,6 @@
// Imports
import cynthia_websites_mini_client/messages
-import cynthia_websites_mini_client/model_type
+import cynthia_websites_mini_client/model_messages
import cynthia_websites_mini_client/pottery/oven
import gleam/bool
import gleam/dict.{type Dict}
@@ -23,7 +23,7 @@ import cynthia_websites_mini_client/pottery/molds/pastels
pub fn into(
layout layout: String,
for theme_type: String,
- using model: model_type.Model,
+ using model: model_messages.Model,
) -> fn(Element(messages.Msg), Dict(String, decode.Dynamic)) ->
element.Element(messages.Msg) {
let #(v, is_post) = case theme_type {
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/ownit.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/ownit.gleam
deleted file mode 100644
index a341bba..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/ownit.gleam
+++ /dev/null
@@ -1,243 +0,0 @@
-//// # Ownit layout
-////
-//// Custom layout for Cynthia Mini.
-//// Allows to create own templates in Handlebars.
-////
-//// Ownit is a unique layout in the sense that, it does not contain a layout, it's merely a wrap around Handlebars to allow own templates to be used in Cynthia Mini.
-////
-//// ## Writing templates for ownit
-////
-//// Writing templates for ownit can be done in the [Handlebars](https://handlebarsjs.com/) language.
-//// Your template should be stored under `[variables] -> ownit_template` as a `"string"` or as a `{ path = "filename.hbs" }` or `{ url = "some-site.com/name.hbs" }` url.
-////
-//// ### Available context variables:
-////
-//// - `body`: Contains the content body, for example the text from your blog post.
-//// etc: More to come!
-
-import cynthia_websites_mini_client/messages
-import cynthia_websites_mini_client/model_type
-import cynthia_websites_mini_client/pottery/oven
-import gleam/dict.{type Dict}
-import gleam/dynamic
-import gleam/dynamic/decode.{type Dynamic}
-import gleam/javascript/array.{type Array}
-import gleam/list
-import gleam/option.{None}
-import gleam/result
-import lustre/element.{type Element}
-
-pub fn main(
- from content: Element(messages.Msg),
- with variables: Dict(String, Dynamic),
- store model: model_type.Model,
- is_post is_post: Bool,
-) {
- let #(
- title,
- description,
- site_name,
- category,
- date_modified,
- date_published,
- tags,
- ): #(String, String, String, String, String, String, Array(String)) = case
- is_post
- {
- True -> {
- let assert Ok(title) =
- dict.get(variables, "title")
- |> result.unwrap(dynamic.from(None))
- |> decode.run(decode.string)
- let assert Ok(description) =
- dict.get(variables, "description_html")
- |> result.unwrap(dynamic.from(None))
- |> decode.run(decode.string)
- let assert Ok(site_name) =
- dict.get(variables, "global_site_name")
- |> result.unwrap(dynamic.from(None))
- |> decode.run(decode.string)
- let assert Ok(category) =
- dict.get(variables, "category")
- |> result.unwrap(dynamic.from(None))
- |> decode.run(decode.string)
- let assert Ok(date_modified) =
- dict.get(variables, "date_modified")
- |> result.unwrap(dynamic.from(None))
- |> decode.run(decode.string)
- let assert Ok(date_published) =
- dict.get(variables, "date_published")
- |> result.unwrap(dynamic.from(None))
- |> decode.run(decode.string)
- let assert Ok(tags) =
- dict.get(variables, "tags")
- |> result.unwrap(dynamic.from([]))
- |> decode.run(decode.list(decode.string))
- let tags = tags |> array.from_list
- #(
- title,
- description,
- site_name,
- category,
- date_modified,
- date_published,
- tags,
- )
- }
- False -> {
- let assert Ok(title) =
- dict.get(variables, "title")
- |> result.unwrap(dynamic.from(None))
- |> decode.run(decode.string)
- let assert Ok(description) =
- dict.get(variables, "description_html")
- |> result.unwrap(dynamic.from(None))
- |> decode.run(decode.string)
- let assert Ok(site_name) =
- dict.get(variables, "global_site_name")
- |> result.unwrap(dynamic.from(None))
- |> decode.run(decode.string)
- let category = ""
- let date_modified = ""
- let date_published = ""
- let tags = [] |> array.from_list
- #(
- title,
- description,
- site_name,
- category,
- date_modified,
- date_published,
- tags,
- )
- }
- }
-
- let menu_map = fn(item: model_type.MenuItem) {
- let to = case item.to {
- "/" <> _ -> {
- // If the link starts with a slash, we assume it's a local link.
- "#" <> item.to
- }
- "!" <> _ -> {
- // If the link starts with an exclamation mark, we assume it's a local link.
- "#" <> item.to
- }
- _ -> {
- // Otherwise, we keep the link as is.
- item.to
- }
- }
-
- [item.name, to] |> array.from_list
- }
-
- let menu_1_items = {
- dict.get(model.computed_menus, 1)
- |> result.unwrap([])
- |> list.map(menu_map)
- |> array.from_list
- }
- let menu_2_items = {
- dict.get(model.computed_menus, 2)
- |> result.unwrap([])
- |> list.map(menu_map)
- |> array.from_list
- }
- let menu_3_items = {
- dict.get(model.computed_menus, 3)
- |> result.unwrap([])
- |> list.map(menu_map)
- |> array.from_list
- }
-
- case get_template(model) {
- Ok(template) -> {
- case
- {
- OwnitCtx(
- content: content |> element.to_string(),
- is_post:,
- title:,
- description:,
- site_name:,
- category:,
- date_modified:,
- date_published:,
- tags:,
- menu_1_items:,
- menu_2_items:,
- menu_3_items:,
- )
- |> context_into_template_run(template, _)
- }
- {
- Ok(html_) -> element.unsafe_raw_html("div", "div", [], html_)
- Error(_) ->
- oven.error(
- "Could not parse context into the Handlebars template from the configurated variable at 'ownit_template'.",
- recoverable: True,
- )
- }
- }
- Error(error_message) -> {
- oven.error(error_message, recoverable: False)
- }
- }
-}
-
-fn get_template(model: model_type.Model) {
- use template_string_dynamic <- result.try(result.replace_error(
- dict.get(model.other, "config_ownit_template"),
- "An error occurred while loading the Handlebars template from the configurated variable at 'ownit_template'.",
- ))
- use template_string <- result.try(result.replace_error(
- decode.run(template_string_dynamic, decode.string),
- "An error occurred while trying to decode the Handlebars template from the configurated variable at 'ownit_template'.",
- ))
- compile_template_string(template_string)
- |> result.replace_error(
- "Could not compile the Handlebars template from the configurated variable at 'ownit_template'.",
- )
-}
-
-/// Context sent into Handlebars template, obviously needs to be generated first. Is translated into an Ecmascript object by FFI.
-type OwnitCtx {
- OwnitCtx(
- /// JS: string
- content: String,
- /// JS: boolean
- is_post: Bool,
- /// JS: string
- title: String,
- /// JS: string
- description: String,
- /// JS: string
- site_name: String,
- /// JS: string
- category: String,
- /// JS: string
- date_modified: String,
- /// JS: string
- date_published: String,
- /// JS: string[]
- tags: Array(String),
- /// JS: [string, string][]
- menu_1_items: Array(Array(String)),
- /// JS: [string, string][]
- menu_2_items: Array(Array(String)),
- /// JS: [string, string][]
- menu_3_items: Array(Array(String)),
- )
-}
-
-@external(javascript, "./ownit_ffi", "compile_template_string")
-fn compile_template_string(in: String) -> Result(CompiledTemplate, Nil)
-
-type CompiledTemplate
-
-@external(javascript, "./ownit_ffi", "context_into_template_run")
-fn context_into_template_run(
- template: CompiledTemplate,
- context: OwnitCtx,
-) -> Result(String, Nil)
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/ownit_ffi.ts b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/ownit_ffi.ts
deleted file mode 100644
index 00e201e..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/molds/ownit_ffi.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-// Ownit layout FFI module
-// Ownit is the only layout that has it's own FFI implementations, since Gleam doesn't have any direct bindings to Handlebars.js
-// And well, I'd like to support full Handlebars :shrug:
-
-import Handlebars from "handlebars";
-import { Ok, Error } from "../../../../prelude";
-
-export function compile_template_string(template_string: string) {
- try {
- return new Ok(Handlebars.compile(template_string));
- } catch (e) {
- console.error("Error while compiling Handlebars template string:", e);
- return new Error(null);
- }
-}
-
-export function context_into_template_run(
- template: HandlebarsTemplateDelegate,
- ctx_record: any,
-) {
- const ctx = turn_gleam_record_into_js_object(ctx_record);
- try {
- return new Ok(template(ctx));
- } catch (e) {
- console.error("Error while running Handlebars template with context:", e);
- return new Error(null);
- }
-}
-
-interface context {
- body: string;
- is_post: boolean;
- title: string;
- description: string;
- site_name: string;
- category: string;
- date_modified: string;
- date_published: string;
- tags: string[];
- menu_1_items: [string, string][];
- menu_2_items: [string, string][];
- menu_3_items: [string, string][];
-}
-
-function turn_gleam_record_into_js_object(record: any): context {
- return {
- body: record.content,
- is_post: record.is_post,
- title: record.title,
- description: record.description,
- site_name: record.site_name,
- category: record.category,
- date_modified: record.date_modified,
- date_published: record.date_published,
- tags: record.tags,
- menu_1_items: record.menu_1_items || [],
- menu_2_items: record.menu_2_items || [],
- menu_3_items: record.menu_3_items || [],
- };
-}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/oven.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/oven.gleam
deleted file mode 100644
index c821550..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/oven.gleam
+++ /dev/null
@@ -1,69 +0,0 @@
-import cynthia_websites_mini_client/dom
-import cynthia_websites_mini_client/messages
-import lustre/attribute
-import lustre/element.{type Element}
-import lustre/element/html
-import plinth/javascript/console
-
-pub fn error(
- error_message msg: String,
- recoverable recoverable: Bool,
-) -> Element(messages.Msg) {
- console.warn("Error page invoked")
- console.error(msg)
- let assert Ok(_) = dom.push_title("Cynthia Mini: Error!")
- html.div(
- [
- attribute.class(
- "absolute mr-auto ml-auto right-0 left-0 bottom-[40VH] top-[40VH] w-fit h-fit",
- ),
- ],
- [
- html.div([attribute.class("card bg-neutral text-neutral-content w-96")], [
- html.div([attribute.class("card-body items-center text-center")], [
- html.h2([attribute.class("card-title")], [
- html.text("An error occurred"),
- ]),
- html.p(
- [
- attribute.class(
- "border-4 border-accent border-dotted pl-4 bg-secondary bg-opacity-10",
- ),
- ],
- [html.text(msg)],
- ),
- case recoverable {
- True ->
- html.div([attribute.class("card-actions justify-end")], [
- html.a(
- [
- attribute.href("javascript:window.location.reload(1)"),
- attribute.class("btn btn-neutral-300"),
- ],
- [html.text("Refresh")],
- ),
- html.a(
- [
- attribute.class("btn btn-ghost"),
- attribute.href(
- // You might think "oh no, this isn't activating any lustre messages". Think again.
- // We WANT to do a fresh page load here. It's an error, so we
- // can safely assume lustre doesn't know how to get out.
- "javascript:window.history.back(1);javascript:window.location.reload()",
- ),
- ],
- [html.text("Go back")],
- ),
- ])
- False ->
- html.div([attribute.class("btn btn-neutral-300")], [
- element.text(
- "If you know the owner of this site, please contact them about this.",
- ),
- ])
- },
- ]),
- ]),
- ],
- )
-}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/paints.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/paints.gleam
deleted file mode 100644
index ecee5d2..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/pottery/paints.gleam
+++ /dev/null
@@ -1,40 +0,0 @@
-import cynthia_websites_mini_client/configtype
-import cynthia_websites_mini_client/dom
-import cynthia_websites_mini_client/model_type.{type Model}
-import cynthia_websites_mini_client/ui/themes_generated
-import gleam/list
-import gleam/option
-import gleam/result
-import plinth/javascript/console
-
-/// Fetches the corresponding theme and layout to the user's preferred colorscheme
-pub fn get_sytheme(model: Model) {
- let theme = case dom.get_color_scheme() {
- "light" -> {
- model.complete_data
- |> option.map(fn(data) { data.global_theme })
- |> option.to_result(Nil)
- |> result.map_error(fn(_) {
- console.error("Error getting light color scheme from database")
- })
- |> result.unwrap(
- configtype.default_shared_cynthia_config_global_only.global_theme,
- )
- }
- "dark" -> {
- model.complete_data
- |> option.map(fn(data) { data.global_theme_dark })
- |> option.to_result(Nil)
- |> result.map_error(fn(_) {
- console.error("Error getting dark color scheme from database")
- })
- |> result.unwrap(
- configtype.default_shared_cynthia_config_global_only.global_theme,
- )
- }
- _ -> panic as "Invalid color scheme"
- }
-
- themes_generated.themes
- |> list.find(fn(th) { th.name == theme })
-}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/jsonld.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/jsonld.gleam-
similarity index 100%
rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/jsonld.gleam
rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/jsonld.gleam-
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/sitemap.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/sitemap.gleam-
similarity index 100%
rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/sitemap.gleam
rename to cynthia_websites_mini_client/src/cynthia_websites_mini_client/shared/sitemap.gleam-
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/ui.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/ui.gleam
deleted file mode 100644
index d9e4b73..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/ui.gleam
+++ /dev/null
@@ -1,19 +0,0 @@
-pub const footer = "Made into this website with Cynthia Mini"
-
-/// The entire of the 404 page.
-pub fn notfoundbody() -> String {
- "
-
-
- 404!
- Uh-oh, that page cannot be found.
-
-
-
-
-
-
-
- "
- <> footer
-}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/utils.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/utils.gleam
deleted file mode 100644
index 9255da7..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/utils.gleam
+++ /dev/null
@@ -1,82 +0,0 @@
-import gleam/float
-import gleam/http
-import gleam/http/request.{type Request}
-import gleam/order
-import gleam/string
-import gleam/time/timestamp
-import plinth/browser/window
-
-pub fn phone_home() -> Request(String) {
- request.new()
- |> request.set_scheme({
- let origin = window.origin()
- case origin {
- "http://" <> _ -> http.Http
- "https://" <> _ -> http.Https
- _ -> http.Https
- }
- })
- |> request.set_host(get_window_host())
-}
-
-pub fn phone_home_url() -> String {
- let origin = window.origin()
- let host = get_window_host()
-
- case origin {
- "http://" <> _ -> "http://" <> host
- "https://" <> _ -> "https://" <> host
- _ -> "https://" <> host
- }
- <> window.pathname()
- |> phone_home_lessener
-}
-
-fn phone_home_lessener(in: String) -> String {
- case string.ends_with(in, "//") {
- True -> phone_home_lessener(in |> string.drop_end(1))
- False ->
- case string.ends_with(in, "index.html") {
- True -> phone_home_lessener(in |> string.replace("index.html", ""))
- False ->
- case string.ends_with(in, "index.html/") {
- True -> phone_home_lessener(in |> string.replace("index.html", ""))
- False -> in
- }
- }
- }
-}
-
-@external(javascript, "./utils_ffi.ts", "getWindowHost")
-pub fn get_window_host() -> String
-
-pub fn now() -> Int {
- let now =
- timestamp.system_time()
- |> timestamp.to_unix_seconds
- |> float.truncate
- now
-}
-
-/// A natural compare
-pub fn compare_so_natural(a: String, b: String) -> order.Order {
- case compares(a, b) {
- "eq" -> order.Eq
- "lt" -> order.Lt
- "gt" -> order.Gt
- _ -> panic as "compare_so_natural failed?? This should never happen"
- }
-}
-
-@external(javascript, "./utils_ffi.ts", "compares")
-fn compares(a: String, b: String) -> String
-
-/// A js FFI implementation of string.trim, to also handle stuff like nbsp or emsp
-@external(javascript, "./utils_ffi.ts", "trims")
-pub fn js_trim(a: String) -> String
-
-@external(javascript, "./utils_ffi.ts", "set_theme_body")
-pub fn set_theme_body(themename: String) -> Nil
-
-@external(javascript, "./utils_ffi.ts", "whatever_timestamp_to_unix_millis")
-pub fn whatever_timestamp_to_unix_millis(ts: String) -> Int
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/utils_ffi.ts b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/utils_ffi.ts
deleted file mode 100644
index 686df06..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/utils_ffi.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-export function getWindowHost() {
- return window.location.host;
-}
-
-export function compares(a: string, b: string): string {
- const result = a.localeCompare(b, undefined, { numeric: true });
- if (result < 0) {
- return "lt";
- } else if (result > 0) {
- return "gt";
- } else {
- return "eq";
- }
-}
-
-export function trims(str: string) {
- return str.trim();
-}
-
-export function set_theme_body(themename: string) {
- document.body.setAttribute("data-theme", themename);
-}
-
-export function whatever_timestamp_to_unix_millis(ts: string | number): number {
- if (typeof ts === "number") {
- // assume it's already unix millis
- return ts;
- } else if (typeof ts === "string") {
- // try to parse as ISO 8601 string
- const parsed = Date.parse(ts);
- if (!isNaN(parsed)) {
- return parsed;
- } else {
- return 0;
- }
- } else {
- return 0;
- }
-}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/version_ffi.ts b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/version_ffi.ts
deleted file mode 100644
index d403743..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/version_ffi.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import { version } from "package.json";
-export function my_own_version(): string {
- return version;
-}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/view.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_client/view.gleam
deleted file mode 100644
index 625e402..0000000
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/view.gleam
+++ /dev/null
@@ -1,154 +0,0 @@
-import cynthia_websites_mini_client/contenttypes
-import cynthia_websites_mini_client/dom
-import cynthia_websites_mini_client/messages.{type Msg}
-import cynthia_websites_mini_client/model_type.{type Model}
-import cynthia_websites_mini_client/pageloader/postlistloader
-import cynthia_websites_mini_client/pottery
-import cynthia_websites_mini_client/pottery/oven
-import cynthia_websites_mini_client/utils
-import gleam/list
-import gleam/option.{None, Some}
-import gleam/result
-import houdini
-import lustre/attribute
-import lustre/element.{type Element}
-import lustre/element/html
-import odysseus
-
-pub fn main(model: Model) -> Element(Msg) {
- case model.status {
- Ok(_) -> {
- case model.complete_data {
- None -> initial_view()
- Some(complete_data) -> {
- let content =
- complete_data.content
- |> list.find(fn(content) { { content.permalink == model.path } })
- |> result.lazy_unwrap(fn() {
- contenttypes.Content(
- filename: "notfound.dj",
- title: "Page not found",
- description: model.path,
- layout: "theme",
- permalink: "404",
- inner_plain: "# 404!\n\nThe page you are looking for does not exist.",
- data: contenttypes.PageData([], True),
- )
- })
- let content = case model.path {
- "!" <> a -> {
- let #(tit, desc) = case content.filename {
- "notfound.dj" -> #(None, None)
- _ -> #(Some(content.title), Some(content.description))
- }
- case a {
- "/category/" <> category -> {
- let title =
- option.unwrap(tit, "Posts in category: " <> category)
- let description =
- option.unwrap(
- desc,
- "A postlist of all posts in the category: " <> category,
- )
-
- contenttypes.Content(
- title:,
- description:,
- layout: "default",
- permalink: model.path,
- filename: "postlist.html",
- data: contenttypes.PageData([], False),
- inner_plain: postlistloader.postlist_by_category(
- model,
- category,
- )
- |> element.to_string,
- )
- }
- "/tag/" <> tag -> {
- let title = option.unwrap(tit, "Posts with tag: " <> tag)
- let description =
- option.unwrap(
- desc,
- "A postlist of all posts tagged with " <> tag,
- )
- contenttypes.Content(
- title:,
- description:,
- layout: "default",
- permalink: model.path,
- filename: "postlist.html",
- data: contenttypes.PageData([], True),
- inner_plain: postlistloader.postlist_by_tag(model, tag)
- |> element.to_string,
- )
- }
- "/search/" <> search_term -> {
- let title =
- option.unwrap(tit, "Search results for: " <> search_term)
- let description = option.unwrap(desc, "")
- contenttypes.Content(
- title:,
- description:,
- layout: "default",
- permalink: model.path,
- filename: "postlist.html",
- data: contenttypes.PageData([], False),
- inner_plain: postlistloader.postlist_by_search_term(
- model,
- search_term,
- )
- |> element.to_string,
- )
- }
- _ -> {
- let title = option.unwrap(tit, "All posts")
- let description = "A postlist of all posts."
- contenttypes.Content(
- title:,
- description:,
- layout: "default",
- permalink: model.path,
- filename: "postlist.html",
- data: contenttypes.PageData([], False),
- inner_plain: postlistloader.postlist_all(model)
- |> element.to_string,
- )
- }
- }
- }
- _ -> content
- }
- let assert Ok(_) =
- dom.push_title(
- houdini.escape(utils.js_trim(odysseus.unescape(content.title))),
- )
- pottery.render_content(model, content)
- }
- }
- }
- Error(error_message) -> oven.error(error_message, True)
- }
-}
-
-pub fn initial_view() -> Element(Msg) {
- let assert Ok(_) = dom.push_title("Cynthia Mini: Loading...")
- html.div(
- [
- attribute.class(
- "absolute mr-auto ml-auto right-0 left-0 bottom-[40VH] top-[40VH] w-fit h-fit",
- ),
- ],
- [
- html.div([attribute.class("card bg-primary text-primary-content w-96")], [
- html.div([attribute.class("card-body")], [
- html.h2([attribute.class("card-title")], [html.text("Cynthia Mini")]),
- html.p([], [html.text("Loading the page you want...")]),
- html.div([attribute.class("card-actions justify-end")], [
- html.span([attribute.class("loading loading-bars loading-lg")], []),
- ]),
- ]),
- ]),
- ],
- )
-}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/site_json.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/site_json.gleam
new file mode 100644
index 0000000..4893ad1
--- /dev/null
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/site_json.gleam
@@ -0,0 +1,140 @@
+//// Site.json gleam type format and en/decoder.
+
+import cynthia_websites_mini_shared/config/v4_1
+import cynthia_websites_mini_shared/config/v4_1/decodes
+import gleam/dict
+import gleam/dynamic/decode
+import gleam/option
+
+/// This is the content of site.json, factually the entire site
+pub type SiteJSON {
+ SiteJSON(
+ config: v4_1.V4p1Mini,
+ // Slug, or a random number if not set and the content
+ content: dict.Dict(String, Content),
+ )
+}
+
+pub fn site_json_decoder() -> decode.Decoder(SiteJSON) {
+ use config <- decode.field("config", decodes.v4p1_mini_dynamic())
+ use content: dict.Dict(String, Content) <- decode.field(
+ "content",
+ decode.dict(decode.string, {
+ use variant <- decode.field("type", decode.string)
+ case variant {
+ "page" -> {
+ use title <- decode.field("title", decode.string)
+ use description <- decode.field("description", decode.string)
+ use layout <- decode.field("layout", decode.optional(decode.string))
+ use content <- decode.field("content", decode.string)
+ use in_menus <- decode.field("in_menus", decode.list(decode.int))
+ use hide_meta_block <- decode.field("hide_meta_block", decode.bool)
+ decode.success(Page(
+ title:,
+ description:,
+ layout:,
+ content:,
+ in_menus:,
+ hide_meta_block:,
+ ))
+ }
+ "post" -> {
+ use title <- decode.field("title", decode.string)
+ use description <- decode.field("description", decode.string)
+ use layout <- decode.field("layout", decode.optional(decode.string))
+ use content <- decode.field("content", decode.string)
+ use date_published <- decode.field("date_published", decode.string)
+ use date_updated <- decode.field("date_updated", decode.string)
+ use category <- decode.field("category", decode.string)
+ use tags <- decode.field("tags", decode.list(decode.string))
+ use mastodon_comments <- field_or(
+ field: "mastodon-comments",
+ decoder: decode.optional({
+ use instance <- decode.field("instance", decode.string)
+ use id <- decode.field("id", decode.string)
+ decode.success(MastodonStatus(instance:, id:))
+ }),
+ otherwise: option.None,
+ )
+
+ decode.success(Post(
+ title:,
+ description:,
+ layout:,
+ content:,
+ date_published:,
+ date_updated:,
+ category:,
+ tags:,
+ mastodon_comments:,
+ ))
+ }
+ _ ->
+ decode.failure(
+ Page("failure", "failure", option.None, "Failure", [], False),
+ "Content",
+ )
+ }
+ }),
+ )
+ decode.success(SiteJSON(config:, content:))
+}
+
+fn field_or(
+ field field: String,
+ decoder field_decoder: decode.Decoder(t),
+ otherwise default: t,
+ next next: fn(t) -> decode.Decoder(final),
+) -> decode.Decoder(final) {
+ use val <- decode.optional_field(
+ field,
+ option.None,
+ decode.optional(field_decoder),
+ )
+ next(val |> option.unwrap(default))
+}
+
+pub type Content {
+ Page(
+ /// Page title
+ title: String,
+ /// Description, converted to HTML beforehand.
+ description: String,
+ /// Layout or default
+ layout: option.Option(String),
+ /// Page content, converted to HTML beforehand.
+ content: String,
+ /// In which menus this page should appear
+ in_menus: List(Int),
+ /// Hide the block with title and description for a page.
+ hide_meta_block: Bool,
+ )
+ Post(
+ /// Page title
+ title: String,
+ /// Description, converted to HTML beforehand.
+ description: String,
+ /// Layout or default
+ layout: option.Option(String),
+ /// Page content, converted to HTML beforehand.
+ content: String,
+ /// Date string -- But it's unchecked
+ /// Stores the date on which the post was published.
+ date_published: String,
+ /// Date string -- But it's unchecked
+ /// # Date updated
+ /// Stores the date on which the post was last updated.
+ date_updated: String,
+ /// Category this post belongs to
+ category: String,
+ /// Tags that belong to this post
+ tags: List(String),
+ /// Mastodon instance and post id to link to for comments.
+ mastodon_comments: option.Option(MastodonStatus),
+ )
+}
+
+/// Mastodon instance and post id to link to for comments.
+pub type MastodonStatus {
+ MastodonStatus(instance: String, id: String)
+}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4.gleam
new file mode 100644
index 0000000..4e65fdc
--- /dev/null
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4.gleam
@@ -0,0 +1,59 @@
+//// Cynthia Mini v4 Config format
+
+import cynthia_websites_mini_shared/config/v4_1
+import gleam/string
+
+pub type V4mini {
+ V4mini(
+ global: V4miniGlobal,
+ integrations: V4miniIntegrations,
+ posts: V4miniPosts,
+ )
+}
+
+pub type V4miniGlobal {
+ V4miniGlobal(
+ theme: String,
+ theme_dark: String,
+ colour: String,
+ site_name: String,
+ site_description: String,
+ )
+}
+
+pub type V4miniIntegrations {
+ V4miniIntegrations(git: Bool, sitemap: String, crawlable_context: Bool)
+}
+
+pub type V4miniPosts {
+ V4miniPosts(comment_repo: String)
+}
+
+pub fn upgrade(in: V4mini) -> v4_1.V4p1Mini {
+ v4_1.V4p1Mini(
+ global: v4_1.V4p1MiniGlobal(
+ site_description: in.global.site_description,
+ theme: in.global.theme,
+ theme_dark: in.global.theme_dark,
+ site_name: in.global.site_name,
+ ),
+ integrations: v4_1.V4p1MiniIntegrations(
+ git: in.integrations.git,
+ sitemap: in.integrations.sitemap,
+ crawlable_context: in.integrations.crawlable_context,
+ ),
+ posts: v4_1.V4p1MiniPosts(comments: {
+ case in.posts.comment_repo |> string.lowercase {
+ "" -> v4_1.CommentsDisabled
+ "mastodon" -> v4_1.CommentsMastodonStored
+ _ -> {
+ case in.posts.comment_repo |> string.split_once("/") {
+ Ok(#(username, repositoryname)) ->
+ v4_1.CommentsGithubStored(username:, repositoryname:)
+ _ -> v4_1.CommentsDisabled
+ }
+ }
+ }
+ }),
+ )
+}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4/decodes.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4/decodes.gleam
new file mode 100644
index 0000000..4b1a184
--- /dev/null
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4/decodes.gleam
@@ -0,0 +1,101 @@
+import cynthia_websites_mini_shared/config/v4
+import gleam/dynamic/decode
+import plinth/javascript/console
+import tom
+
+pub fn v4mini_dynamic() -> decode.Decoder(v4.V4mini) {
+ use global <- decode.field("global", {
+ use theme <- decode.field("theme", decode.string)
+ use theme_dark <- decode.field("theme_dark", decode.string)
+ use colour <- decode.field("colour", decode.string)
+ use site_name <- decode.field("site_name", decode.string)
+ use site_description <- decode.field("site_description", decode.string)
+ decode.success(v4.V4miniGlobal(
+ theme:,
+ theme_dark:,
+ colour:,
+ site_name:,
+ site_description:,
+ ))
+ })
+ use integrations <- decode.field("integrations", {
+ use git <- decode.field("git", decode.bool)
+ use sitemap <- decode.field("sitemap", decode.string)
+ use crawlable_context <- decode.field("crawlable_context", decode.bool)
+ decode.success(v4.V4miniIntegrations(git:, sitemap:, crawlable_context:))
+ })
+ use posts <- decode.field("posts", {
+ use comment_repo <- decode.field("comment_repo", decode.string)
+ decode.success(v4.V4miniPosts(comment_repo:))
+ })
+ decode.success(v4.V4mini(global:, integrations:, posts:))
+}
+
+pub fn v4mini_toml(toml_source: String) -> Result(v4.V4mini, Nil) {
+ case tom.parse(toml_source) {
+ Ok(toml) -> {
+ v4.V4mini(
+ global: {
+ let theme = tom.get_string(toml, ["global", "theme"]) |> unsafe_unwrap
+
+ v4.V4miniGlobal(
+ theme:,
+ theme_dark: tom.get_string(toml, ["global", "theme", "dark"])
+ |> unsafe_unwrap,
+ site_name: tom.get_string(toml, ["global", "site_name"])
+ |> unsafe_unwrap,
+ site_description: tom.get_string(toml, [
+ "global",
+ "site_description",
+ ])
+ |> unsafe_unwrap,
+ colour: tom.get_string(toml, ["global", "colour"])
+ |> unsafe_unwrap,
+ )
+ },
+ integrations: v4.V4miniIntegrations(
+ git: tom.get_bool(toml, [
+ "integrations",
+ "git",
+ ])
+ |> unsafe_unwrap,
+ sitemap: tom.get_string(toml, [
+ "integrations",
+ "sitemap",
+ ])
+ |> unsafe_unwrap,
+ crawlable_context: tom.get_bool(toml, [
+ "integrations",
+ "crawlable_context",
+ ])
+ |> unsafe_unwrap,
+ ),
+ posts: v4.V4miniPosts(comment_repo: {
+ tom.get_string(toml, [
+ "posts",
+ "comment_repo",
+ ])
+ |> unsafe_unwrap
+ }),
+ )
+ |> Ok
+ }
+ // We don't propogate upwards, we give back a Error value but inform here and then exit upstream.
+ Error(_) -> {
+ console.log("Could not parse TOML!")
+ Error(Nil)
+ }
+ }
+}
+
+fn unsafe_unwrap(v: Result(s, _)) {
+ case v {
+ Ok(a) -> a
+ Error(_) -> {
+ let d =
+ "Encountered invalid value in legacy config, Cynthia Mini won't try to recover for this in legacy configs."
+ console.error(d)
+ panic as d
+ }
+ }
+}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1.gleam
new file mode 100644
index 0000000..db9e30a
--- /dev/null
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1.gleam
@@ -0,0 +1,49 @@
+//// Cynthia v4.1 [external] Config format
+
+pub type V4p1Mini {
+ V4p1Mini(
+ global: V4p1MiniGlobal,
+ integrations: V4p1MiniIntegrations,
+ posts: V4p1MiniPosts,
+ )
+}
+
+pub type V4p1MiniGlobal {
+ V4p1MiniGlobal(
+ theme: String,
+ theme_dark: String,
+ site_name: String,
+ site_description: String,
+ )
+}
+
+pub type V4p1MiniIntegrations {
+ V4p1MiniIntegrations(git: Bool, sitemap: String, crawlable_context: Bool)
+}
+
+pub type V4p1MiniPosts {
+ V4p1MiniPosts(comments: V4p1MiniPostsComments)
+}
+
+pub type V4p1MiniPostsComments {
+ CommentsMastodonStored
+ CommentsGithubStored(username: String, repositoryname: String)
+ CommentsDisabled
+}
+
+pub fn new() -> V4p1Mini {
+ V4p1Mini(
+ global: V4p1MiniGlobal(
+ theme: "autumn",
+ theme_dark: "night",
+ site_name: "My Site",
+ site_description: "A big site on a mini Cynthia!",
+ ),
+ integrations: V4p1MiniIntegrations(
+ git: True,
+ sitemap: "",
+ crawlable_context: False,
+ ),
+ posts: V4p1MiniPosts(comments: CommentsDisabled),
+ )
+}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1/decodes.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1/decodes.gleam
new file mode 100644
index 0000000..85aa898
--- /dev/null
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1/decodes.gleam
@@ -0,0 +1,194 @@
+import cynthia_websites_mini_shared/config/v4
+import cynthia_websites_mini_shared/config/v4/decodes
+import cynthia_websites_mini_shared/config/v4_1
+import gleam/dynamic/decode
+import gleam/float
+import gleam/int
+import gleam/option
+import gleam/result
+import gleam/string
+import plinth/javascript/console
+import tom
+
+pub fn v4p1_mini_dynamic() -> decode.Decoder(v4_1.V4p1Mini) {
+ use global <- decode.field("global", {
+ use theme <- decode.field("theme", decode.string)
+ use theme_dark <- decode.field("theme_dark", decode.string)
+ use site_name <- decode.field("site_name", decode.string)
+ use site_description <- decode.field("site_description", decode.string)
+ decode.success(v4_1.V4p1MiniGlobal(
+ theme:,
+ theme_dark:,
+ site_name:,
+ site_description:,
+ ))
+ })
+ use integrations <- decode.field("integrations", {
+ use git <- decode.field("git", decode.bool)
+ use sitemap <- decode.field("sitemap", decode.string)
+ use crawlable_context <- decode.field("crawlable_context", decode.bool)
+ decode.success(v4_1.V4p1MiniIntegrations(git:, sitemap:, crawlable_context:))
+ })
+ use posts <- decode.field("posts", {
+ use comments <- decode.field("comments", {
+ use comments <- field_or(
+ field: "comments",
+ decoder: {
+ use variant <- decode.field("store", decode.string)
+ case variant |> string.lowercase {
+ "mastodon" -> decode.success(v4_1.CommentsMastodonStored)
+ "github" -> {
+ use username <- decode.field("username", decode.string)
+ use repositoryname <- decode.field(
+ "repositoryname",
+ decode.string,
+ )
+ decode.success(v4_1.CommentsGithubStored(
+ username:,
+ repositoryname:,
+ ))
+ }
+ "disabled" | "" -> decode.success(v4_1.CommentsDisabled)
+ _ ->
+ decode.failure(
+ v4_1.CommentsDisabled,
+ "v4_1.V4p1MiniPostsComments",
+ )
+ }
+ },
+ otherwise: v4_1.CommentsDisabled,
+ )
+ decode.success(comments)
+ })
+ decode.success(v4_1.V4p1MiniPosts(comments:))
+ })
+ decode.success(v4_1.V4p1Mini(global:, integrations:, posts:))
+}
+
+fn field_or(
+ field field: String,
+ decoder field_decoder: decode.Decoder(t),
+ otherwise default: t,
+ next next: fn(t) -> decode.Decoder(final),
+) -> decode.Decoder(final) {
+ use val <- decode.optional_field(
+ field,
+ option.None,
+ decode.optional(field_decoder),
+ )
+ next(val |> option.unwrap(default))
+}
+
+pub fn vp4p1mini_toml(toml_source: String) {
+ case tom.parse(toml_source) {
+ Ok(toml) -> {
+ let edition =
+ tom.get_string(toml, ["edition"]) |> result.map(string.lowercase)
+ let version =
+ result.or(tom.get_float(toml, ["version"]), {
+ tom.get_int(toml, ["version"]) |> result.map(int.to_float)
+ })
+
+ case edition, version {
+ Ok("mini"), Ok(4.1) -> {
+ v4_1.V4p1Mini(
+ global: {
+ let theme =
+ tom.get_string(toml, ["global", "theme", "default"])
+ |> result.unwrap({
+ tom.get_string(toml, ["global", "theme"])
+ |> result.unwrap(v4_1.new().global.theme)
+ })
+ v4_1.V4p1MiniGlobal(
+ theme:,
+ theme_dark: result.unwrap(
+ tom.get_string(toml, ["global", "theme", "dark"]),
+ theme,
+ ),
+ site_name: tom.get_string(toml, ["global", "site_name"])
+ |> result.unwrap(v4_1.new().global.site_name),
+ site_description: tom.get_string(toml, [
+ "global",
+ "site_description",
+ ])
+ |> result.unwrap(v4_1.new().global.site_description),
+ )
+ },
+ integrations: v4_1.V4p1MiniIntegrations(
+ git: tom.get_bool(toml, [
+ "integrations",
+ "git",
+ ])
+ |> result.unwrap(v4_1.new().integrations.git),
+ sitemap: tom.get_string(toml, [
+ "integrations",
+ "sitemap",
+ ])
+ |> result.unwrap(v4_1.new().integrations.sitemap),
+ crawlable_context: tom.get_bool(toml, [
+ "integrations",
+ "crawlable_context",
+ ])
+ |> result.unwrap(v4_1.new().integrations.crawlable_context),
+ ),
+ posts: v4_1.V4p1MiniPosts(comments: {
+ case
+ tom.get_string(toml, ["posts", "comments", "store"])
+ |> result.unwrap("disabled")
+ {
+ "mastodon" -> v4_1.CommentsMastodonStored
+ "github" -> {
+ case
+ tom.get_string(toml, ["posts", "comments", "username"]),
+ tom.get_string(toml, ["posts", "comments", "repositoryname"])
+ {
+ Ok(username), Ok(repositoryname) ->
+ v4_1.CommentsGithubStored(username:, repositoryname:)
+ _, _ -> v4_1.new().posts.comments
+ }
+ }
+ _ -> v4_1.CommentsDisabled
+ }
+ }),
+ )
+ |> Ok
+ }
+ Ok("mini"), Ok(4.0) -> {
+ decodes.v4mini_toml(toml_source) |> result.map(v4.upgrade)
+ }
+ Ok(_), Error(_) | Error(_), Ok(_) -> {
+ console.error("Unknown combination of edition and version.")
+ Error(Nil)
+ }
+ Error(_), Error(_) -> {
+ console.log("Could not parse TOML!")
+ Error(Nil)
+ }
+ Ok(edition), Ok(version) -> {
+ console.error(
+ "Config version "
+ <> version |> float.to_string()
+ <> " with edition '"
+ <> edition
+ <> "' is NOT supported by this version of Cynthia."
+ <> "\n Usually this means one of these options:"
+ <> "\n - it was written for a different edition"
+ <> "\n - it is invalid"
+ <> "\n - or this version of cynthia is too old to understand this file."
+ <> case edition == "mini" {
+ True ->
+ "\n\n\n It seems to be that last option, since the edition it is written for, does match 'mini'."
+ False -> ""
+ },
+ )
+ Error(Nil)
+ }
+ }
+ }
+ // We don't propogate upwards, we give back a Error value but inform here and then exit upstream.
+ Error(_) -> {
+ console.log("Could not parse TOML!")
+ Error(Nil)
+ }
+ }
+}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1/encodes.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1/encodes.gleam
new file mode 100644
index 0000000..e4fc3e9
--- /dev/null
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/config/v4_1/encodes.gleam
@@ -0,0 +1,95 @@
+import cynthia_websites_mini_shared/config/v4_1
+import gleam/bool
+import gleam/json
+
+pub fn v4p1_mini_json(v4p1_mini: v4_1.V4p1Mini) -> json.Json {
+ json.object([
+ #(
+ "global",
+ json.object([
+ #("theme", json.string(v4p1_mini.global.theme)),
+ #("theme_dark", json.string(v4p1_mini.global.theme_dark)),
+ #("site_name", json.string(v4p1_mini.global.site_name)),
+ #("site_description", json.string(v4p1_mini.global.site_description)),
+ ]),
+ ),
+ #(
+ "integrations",
+ json.object([
+ #("git", json.bool(v4p1_mini.integrations.git)),
+ #("sitemap", json.string(v4p1_mini.integrations.sitemap)),
+ #(
+ "crawlable_context",
+ json.bool(v4p1_mini.integrations.crawlable_context),
+ ),
+ ]),
+ ),
+ #("posts", {
+ json.object([
+ #("comments", case v4p1_mini.posts.comments {
+ v4_1.CommentsMastodonStored ->
+ json.object([
+ #("store", json.string("mastodon")),
+ ])
+ v4_1.CommentsGithubStored(username:, repositoryname:) ->
+ json.object([
+ #("store", json.string("github")),
+ #("username", json.string(username)),
+ #("repositoryname", json.string(repositoryname)),
+ ])
+ v4_1.CommentsDisabled ->
+ json.object([
+ #("store", json.string("disabled")),
+ ])
+ }),
+ ])
+ }),
+ ])
+}
+
+pub fn v4p1_mini_toml(v4p1_mini: v4_1.V4p1Mini) -> String {
+ "# Do not edit these variables! It is set by Cynthia to tell it's config format apart.
+ config.edition=\"mini\"
+ config.version=4.1
+ [global]
+ # Theme to use for light mode - default themes: autumn, default
+ # Theme to use for dark mode - default themes: night, default-dark
+ theme = { default = \""
+ <> v4p1_mini.global.theme
+ <> "\", dark = \""
+ <> v4p1_mini.global.theme_dark
+ <> "\" }
+ # Your website's name, displayed in various places
+ site_name = \""
+ <> v4p1_mini.global.site_name
+ <> "\"
+ # A brief description of your website
+ site_description = \""
+ <> v4p1_mini.global.site_description
+ <> "\"
+
+ [integrations]
+ # Enable git integration for the website
+ # This will allow Cynthia Mini to detect the git repository
+ # For example linking to the commit hash in the footer
+ git = "
+ <> v4p1_mini.integrations.git |> bool.to_string
+ <> "
+
+ # Enable sitemap generation
+ # This will generate a sitemap.xml file in the root of the website
+ #
+ # You will need to enter the base URL of your website in the sitemap variable below.
+ # If your homepage is at \"https://example.com/#/\", then the sitemap variable should be set to \"https://example.com\".
+ # If you do not want to use a sitemap, set this to \"false\", or leave it empty (\"\"), you can also remove the sitemap variable altogether.
+ sitemap = \""
+ <> v4p1_mini.integrations.sitemap
+ <> "\"
+
+ # Enable crawlable context (JSON-LD injection)
+ # This will allow search engines to crawl the website, and makes it
+ # possible for the website to be indexed by search engine and LLMs.
+ crawlable_context = "
+ <> v4p1_mini.integrations.crawlable_context |> bool.to_string
+ <> ""
+}
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/dom.gleam b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/ffi.gleam
similarity index 56%
rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/dom.gleam
rename to cynthia_websites_mini_client/src/cynthia_websites_mini_shared/ffi.gleam
index b73f03b..bcb6238 100644
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/dom.gleam
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/ffi.gleam
@@ -3,7 +3,7 @@ import plinth/browser/document
import plinth/browser/element
pub fn push_title(title: String) -> Result(Nil, String) {
- use title_element <- result.then(
+ use title_element <- result.try(
document.query_selector("title")
|> result.replace_error("No title element found"),
)
@@ -22,23 +22,38 @@ pub fn push_title(title: String) -> Result(Nil, String) {
Ok(Nil)
}
+@external(javascript, "./ts_ffi.ts", "my_own_version")
+pub fn version() -> String
+
/// Get the color scheme of the user's system (media query)
-@external(javascript, "./dom.ts", "get_color_scheme")
-pub fn get_color_scheme() -> String
+@external(javascript, "./ts_ffi.ts", "get_color_scheme")
+pub fn get_color_scheme() -> Bool
/// Set the data attribute of an element
-@external(javascript, "./dom.ts", "set_data")
+@external(javascript, "./ts_ffi.ts", "set_data")
pub fn set_data(element: element.Element, key: String, value: String) -> Nil
/// Set the hash of the window
-@external(javascript, "./dom.ts", "set_hash")
+@external(javascript, "./ts_ffi.ts", "set_hash")
pub fn set_hash(hash: String) -> Nil
/// Get innerhtml of an element
-@external(javascript, "./dom.ts", "get_inner_html")
+@external(javascript, "./ts_ffi.ts", "get_inner_html")
pub fn get_inner_html(element: element.Element) -> String
/// jsonify_string
/// Convert a string to a JSON safe string
-@external(javascript, "./dom.ts", "jsonify_string")
+@external(javascript, "./ts_ffi.ts", "jsonify_string")
pub fn jsonify_string(str: String) -> Result(String, Nil)
+
+@external(javascript, "./ts_ffi.ts", "destroy_comment_box")
+pub fn destroy_comment_box() -> Nil
+
+@external(javascript, "./ts_ffi.ts", "apply_styles_to_comment_box")
+pub fn comment_box_forced_styles() -> Nil
+
+@external(javascript, "./ts_ffi.ts", "browse")
+pub fn browse(a: String) -> Nil
+
+@external(javascript, "./ts_ffi.ts", "browse_prompt")
+pub fn browse_prompt(s: String) -> Nil
diff --git a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/dom.ts b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/ts_ffi.ts
similarity index 86%
rename from cynthia_websites_mini_client/src/cynthia_websites_mini_client/dom.ts
rename to cynthia_websites_mini_client/src/cynthia_websites_mini_shared/ts_ffi.ts
index 289ad3d..7d6f9b1 100644
--- a/cynthia_websites_mini_client/src/cynthia_websites_mini_client/dom.ts
+++ b/cynthia_websites_mini_client/src/cynthia_websites_mini_shared/ts_ffi.ts
@@ -4,9 +4,9 @@ export function get_color_scheme() {
// Media queries the preferred color colorscheme
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
- return "dark";
+ return false;
}
- return "light";
+ return true;
}
export function set_data(el: HTMLElement, key: string, val: string) {
@@ -69,3 +69,17 @@ export function jsonify_string(str: string) {
return new Error(null);
}
}
+
+import { version } from "package.json";
+export function my_own_version(): string {
+ return version;
+}
+
+export function browse_prompt(l: string) {
+ if (window.confirm("Leave this page and go to '" + l + "'?")) {
+ browse(l);
+ }
+}
+export function browse(l: string) {
+ window.location.assign(l);
+}
diff --git a/cynthia_websites_mini_client/test/cynthia_websites_mini_client_test.gleam b/cynthia_websites_mini_client/test/cynthia_websites_mini_client_test.gleam
index 2fa852f..fba3c88 100644
--- a/cynthia_websites_mini_client/test/cynthia_websites_mini_client_test.gleam
+++ b/cynthia_websites_mini_client/test/cynthia_websites_mini_client_test.gleam
@@ -1,65 +1,13 @@
-import birdie
-import cynthia_websites_mini_client/configtype
-import cynthia_websites_mini_client/pottery/djotparse
import gleeunit
-import gleeunit/should
-import lustre/element
-import lustre/element/html
-pub fn main() {
+pub fn main() -> Nil {
gleeunit.main()
}
// gleeunit test functions end in `_test`
pub fn hello_world_test() {
- 1
- |> should.equal(1)
-}
-
-pub fn simple_djot_test() {
- "# Hello World\n\nThis is a test paragraph.\n\n- Item 1\n- Item 2"
- |> djotparse.entry_to_conversion()
- |> html.section([], _)
- |> element.to_readable_string
- |> birdie.snap(title: "simple_djot_test")
-}
-
-pub fn djot_with_preprocessing_test() {
- "# Hello World\n\nThis is a test paragraph.\n\n- [ ] Task item\n- [x] Completed task\n\n> This is a blockquote\n\nAnother paragraph."
- |> djotparse.entry_to_conversion()
- |> html.section([], _)
- |> element.to_readable_string
- |> birdie.snap(title: "djot_with_preprocessing_test")
-}
-
-pub fn autolinks_test() {
- "External page example, using the theme list, downloading from "
- |> djotparse.entry_to_conversion()
- |> html.section([], _)
- |> element.to_readable_string
- |> birdie.snap(title: "autolinks_test")
-}
-
-pub fn links_in_preprocessed_items_test() {
- "- [ ] Task with [link](https://example.com)\n- [x] Completed task with [another link](https://test.com)\n\n> Blockquote with [a link](https://blockquote.example)"
- |> djotparse.entry_to_conversion()
- |> html.section([], _)
- |> element.to_readable_string
- |> birdie.snap(title: "links_in_preprocessed_items_test")
-}
-
-pub fn ordered_list_with_links_test() {
- "1. First item with [link](https://first.com)\n2. Second item with [another link](https://second.com)\n3. Third item with **bold** and [link](https://third.com)"
- |> djotparse.entry_to_conversion()
- |> html.section([], _)
- |> element.to_readable_string
- |> birdie.snap(title: "ordered_list_with_links_test")
-}
+ let name = "Joe"
+ let greeting = "Hello, " <> name <> "!"
-pub fn ootb_index_rendering_test() {
- configtype.ootb_index
- |> djotparse.entry_to_conversion()
- |> html.body([], _)
- |> element.to_readable_string
- |> birdie.snap(title: "ootb_index_rendering_test")
+ assert greeting == "Hello, Joe!"
}
diff --git a/cynthia_websites_mini_server/gleam.toml b/cynthia_websites_mini_server/gleam.toml
index 573ce7c..6b522c0 100644
--- a/cynthia_websites_mini_server/gleam.toml
+++ b/cynthia_websites_mini_server/gleam.toml
@@ -18,38 +18,19 @@ typescript_declarations = true
[dependencies]
# Shared dependencies with the client
-gleam_stdlib = "0.59.0"
-plinth = ">= 0.5.9 and < 1.0.0"
-gleam_javascript = "1.0.0"
-gleam_community_colour = "1.4.1"
-
-gleam_json = "2.3.0"
-gleam_http = "3.7.2"
-gleam_fetch = "1.3.0"
+gleam_stdlib = ">= 0.44.0 and < 2.0.0"
+plinth = ">= 0.9.2 and < 1.0.0"
+tom = "1.1.1"
# Other dependencies
argv = "1.0.2"
birl = "1.8.0"
-conversation = "2.0.1"
bungibindies = "1.2.0-rc"
cynthia_websites_mini_client = { path = "../cynthia_websites_mini_client" }
-edit_distance = "2.0.1"
-envoy = "1.0.2"
-filepath = "1.1.2"
-glam = "2.0.2"
-glance = "3.0.0"
-gleam_community_ansi = "1.4.3"
-gleam_regexp = "1.1.1"
-gleam_yielder = "1.1.0"
-gleamy_lights = "2.3.0"
-javascript_mutable_reference = "1.0.0"
-justin = "1.0.1"
-ranger = "1.4.0"
simplifile = "2.2.1"
-tom = "1.1.1"
-
+gleamy_lights = ">= 2.3.1 and < 3.0.0"
+gleam_javascript = ">= 1.0.0 and < 2.0.0"
-webls = "1.5.1"
[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"
birdie = ">= 1.2.7 and < 2.0.0"
diff --git a/cynthia_websites_mini_server/manifest.toml b/cynthia_websites_mini_server/manifest.toml
index af2f9f2..40927bc 100644
--- a/cynthia_websites_mini_server/manifest.toml
+++ b/cynthia_websites_mini_server/manifest.toml
@@ -6,47 +6,32 @@ packages = [
{ name = "birdie", version = "1.3.0", build_tools = ["gleam"], requirements = ["argv", "edit_distance", "filepath", "glance", "gleam_community_ansi", "gleam_stdlib", "justin", "rank", "simplifile", "term_size", "trie_again"], otp_app = "birdie", source = "hex", outer_checksum = "425916385B6CD82A58F54CC39605262A524B169746FC9AD9C799BC76E88F7AF3" },
{ name = "birl", version = "1.8.0", build_tools = ["gleam"], requirements = ["gleam_regexp", "gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "2AC7BA26F998E3DFADDB657148BD5DDFE966958AD4D6D6957DD0D22E5B56C400" },
{ name = "bungibindies", version = "1.2.0-rc", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_stdlib"], otp_app = "bungibindies", source = "hex", outer_checksum = "C1A4DD5D0BE282E4A6F007ECE3FE1477E38CFDF1B6043A5ABAB63C53443AC473" },
- { name = "conversation", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "conversation", source = "hex", outer_checksum = "103DF47463B8432AB713D6643DC17244B9C82E2B172A343150805129FE584A2F" },
- { name = "cynthia_websites_mini_client", version = "1.2.0-rc2", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_fetch", "gleam_http", "gleam_javascript", "gleam_json", "gleam_stdlib", "gleam_time", "houdini", "jot", "lustre", "modem", "odysseus", "plinth", "rsvp"], source = "local", path = "../cynthia_websites_mini_client" },
+ { name = "cynthia_websites_mini_client", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib", "plinth", "tom"], source = "local", path = "../cynthia_websites_mini_client" },
{ name = "edit_distance", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "edit_distance", source = "hex", outer_checksum = "A1E485C69A70210223E46E63985FA1008B8B2DDA9848B7897469171B29020C05" },
- { name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" },
+ { name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" },
{ name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" },
{ name = "glam", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "4932A2D139AB0389E149396407F89654928D7B815E212BB02F13C66F53B1BBA1" },
{ name = "glance", version = "3.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "F3458292AFB4136CEE23142A8727C1270494E7A96978B9B9F9D2C1618583EF3D" },
{ name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" },
- { name = "gleam_community_colour", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "386CB9B01B33371538672EEA8A6375A0A0ADEF41F17C86DDCB81C92AD00DA610" },
- { name = "gleam_erlang", version = "0.34.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "0C38F2A128BAA0CEF17C3000BD2097EB80634E239CE31A86400C4416A5D0FDCC" },
- { name = "gleam_fetch", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "2CBF9F2E1C71AEBBFB13A9D5720CD8DB4263EB02FE60C5A7A1C6E17B0151C20C" },
- { name = "gleam_http", version = "3.7.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8A70D2F70BB7CFEB5DF048A2183FFBA91AF6D4CF5798504841744A16999E33D2" },
- { name = "gleam_httpc", version = "4.1.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C670EBD46FC1472AD5F1F74F1D3938D1D0AC1C7531895ED1D4DDCB6F07279F43" },
+ { name = "gleam_community_colour", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "E34DD2C896AC3792151EDA939DA435FF3B69922F33415ED3C4406C932FBE9634" },
{ name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" },
- { name = "gleam_json", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "C55C5C2B318533A8072D221C5E06E5A75711C129E420DD1CE463342106012E5D" },
- { name = "gleam_otp", version = "0.16.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "50DA1539FC8E8FA09924EB36A67A2BBB0AD6B27BCDED5A7EF627057CF69D035E" },
+ { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" },
{ name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" },
{ name = "gleam_stdlib", version = "0.59.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F8FEE9B35797301994B81AF75508CF87C328FE1585558B0FFD188DC2B32EAA95" },
- { name = "gleam_time", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "0DF3834D20193F0A38D0EB21F0A78D48F2EC276C285969131B86DF8D4EF9E762" },
{ name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" },
- { name = "gleamy_lights", version = "2.3.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_community_colour", "gleam_stdlib"], otp_app = "gleamy_lights", source = "hex", outer_checksum = "8A3D43BCA0D935F7CC787F4D0D1771F822B3366114C08B93CC8D00747618499A" },
+ { name = "gleamy_lights", version = "2.3.1", build_tools = ["gleam"], requirements = ["envoy", "gleam_community_colour", "gleam_stdlib"], otp_app = "gleamy_lights", source = "hex", outer_checksum = "CD89DD48BBCD8FBB6B8CB84101C70221CBFB901F711C3C7F81F47288EC8074FD" },
{ name = "gleeunit", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "A7DD6C07B7DA49A6E28796058AA89E651D233B357D5607006D70619CD89DAAAB" },
- { name = "glexer", version = "2.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "5C235CBDF4DA5203AD5EAB1D6D8B456ED8162C5424FE2309CFFB7EF438B7C269" },
- { name = "houdini", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "houdini", source = "hex", outer_checksum = "5DB1053F1AF828049C2B206D4403C18970ABEF5C18671CA3C2D2ED0DD64F6385" },
- { name = "javascript_mutable_reference", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "javascript_mutable_reference", source = "hex", outer_checksum = "3EE953EE7FE4FAFD17C16F24184F4C832FE260D761753F28F20D4AC1DA080F03" },
- { name = "jot", version = "5.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "houdini", "splitter"], otp_app = "jot", source = "hex", outer_checksum = "B1A0C91A3D273971D1CA1F644FF0A9CAC8256BDA249CADC927041BF14E7114A6" },
+ { name = "glexer", version = "2.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "splitter"], otp_app = "glexer", source = "hex", outer_checksum = "41D8D2E855AEA87ADC94B7AF26A5FEA3C90268D4CF2CCBBD64FD6863714EE085" },
{ name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" },
- { name = "lustre", version = "5.0.3", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "0BB69D69A9E75E675AA2C32A4A0E5086041D037829FC8AD385BA6A59E45A60A2" },
- { name = "modem", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib", "lustre"], otp_app = "modem", source = "hex", outer_checksum = "EF6B6B187E9D6425DFADA3A1AC212C01C4F34913A135DA2FF9B963EEF324C1F7" },
- { name = "odysseus", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "odysseus", source = "hex", outer_checksum = "6A97DA1075BDDEA8B60F47B1DFFAD49309FA27E73843F13A0AF32EA7087BA11C" },
- { name = "plinth", version = "0.7.1", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "63BB36AACCCCB82FBE46A862CF85CB88EBE4EF280ECDBAC4B6CB042340B9E1D8" },
+ { name = "plinth", version = "0.9.2", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "A5A14C590F9820F8447E989B66C73C9DE077FAAB75618D639DFA1F3BCA45F946" },
{ name = "pprint", version = "1.0.6", build_tools = ["gleam"], requirements = ["glam", "gleam_stdlib"], otp_app = "pprint", source = "hex", outer_checksum = "4E9B34AE03B2E81D60F230B9BAF1792BE1AC37AFB5564B8DEBEE56BAEC866B7D" },
{ name = "ranger", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_yielder"], otp_app = "ranger", source = "hex", outer_checksum = "C8988E8F8CDBD3E7F4D8F2E663EF76490390899C2B2885A6432E942495B3E854" },
{ name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" },
- { name = "rsvp", version = "1.0.4", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_fetch", "gleam_http", "gleam_httpc", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "rsvp", source = "hex", outer_checksum = "EFCA7CD53B0A8738C06E136422D1FF080DBB657C89E077F7B9DD20BFACE0A77A" },
{ name = "simplifile", version = "2.2.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "C88E0EE2D509F6D86EB55161D631657675AA7684DAB83822F7E59EB93D9A60E3" },
{ name = "splitter", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "3DFD6B6C49E61EDAF6F7B27A42054A17CFF6CA2135FF553D0CB61C234D281DD0" },
{ name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" },
{ name = "tom", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0910EE688A713994515ACAF1F486A4F05752E585B9E3209D8F35A85B234C2719" },
- { name = "trie_again", version = "1.1.3", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "365FE609649F3A098D1D7FC7EA5222EE422F0B3745587BF2AB03352357CA70BB" },
- { name = "webls", version = "1.5.1", build_tools = ["gleam"], requirements = ["birl", "gleam_stdlib"], otp_app = "webls", source = "hex", outer_checksum = "6C78FFCD3BB888725F83FBD0729BDEA1BFEDFBB06544FCA15BF98FBA04F863B0" },
+ { name = "trie_again", version = "1.1.4", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "E3BD66B4E126EF567EA8C4944EAB216413392ADF6C16C36047AF79EE5EF13466" },
]
[requirements]
@@ -54,29 +39,12 @@ argv = { version = "1.0.2" }
birdie = { version = ">= 1.2.7 and < 2.0.0" }
birl = { version = "1.8.0" }
bungibindies = { version = "1.2.0-rc" }
-conversation = { version = "2.0.1" }
cynthia_websites_mini_client = { path = "../cynthia_websites_mini_client" }
-edit_distance = { version = "2.0.1" }
-envoy = { version = "1.0.2" }
-filepath = { version = "1.1.2" }
-glam = { version = "2.0.2" }
-glance = { version = "3.0.0" }
-gleam_community_ansi = { version = "1.4.3" }
-gleam_community_colour = { version = "1.4.1" }
-gleam_fetch = { version = "1.3.0" }
-gleam_http = { version = "3.7.2" }
-gleam_javascript = { version = "1.0.0" }
-gleam_json = { version = "2.3.0" }
-gleam_regexp = { version = "1.1.1" }
-gleam_stdlib = { version = "0.59.0" }
-gleam_yielder = { version = "1.1.0" }
-gleamy_lights = { version = "2.3.0" }
+gleam_javascript = { version = ">= 1.0.0 and < 2.0.0" }
+gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
+gleamy_lights = { version = ">= 2.3.1 and < 3.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
-javascript_mutable_reference = { version = "1.0.0" }
-justin = { version = "1.0.1" }
-plinth = { version = ">= 0.5.9 and < 1.0.0" }
+plinth = { version = ">= 0.9.2 and < 1.0.0" }
pprint = { version = ">= 1.0.5 and < 2.0.0" }
-ranger = { version = "1.4.0" }
simplifile = { version = "2.2.1" }
tom = { version = "1.1.1" }
-webls = { version = "1.5.1" }
diff --git a/cynthia_websites_mini_server/src/cynthia_websites_mini_server.gleam b/cynthia_websites_mini_server/src/cynthia_websites_mini_server.gleam
index 34b1484..f8cffa0 100644
--- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server.gleam
+++ b/cynthia_websites_mini_server/src/cynthia_websites_mini_server.gleam
@@ -1,27 +1,15 @@
import bungibindies
import bungibindies/bun
-import bungibindies/bun/http/serve.{ServeOptions}
import cynthia_websites_mini_client
-import cynthia_websites_mini_client/configtype
-import cynthia_websites_mini_client/shared/jsonld
-import cynthia_websites_mini_client/shared/sitemap
-import cynthia_websites_mini_server/config
-import cynthia_websites_mini_server/mutable_model_type
-import cynthia_websites_mini_server/ssrs
-import cynthia_websites_mini_server/utils/files
-import cynthia_websites_mini_server/web
+import cynthia_websites_mini_shared/config/site_json
+import cynthia_websites_mini_shared/ffi
import gleam/bool
-import gleam/int
import gleam/javascript/array
-import gleam/javascript/promise
-import gleam/json
import gleam/list
-import gleam/option.{None, Some}
+import gleam/option.{type Option, None, Some}
import gleam/string
-import gleamy_lights/console
import gleamy_lights/premixed
-import javascript/mutable_reference
-import plinth/javascript/global
+import plinth/javascript/console
import plinth/node/process
import simplifile
@@ -55,17 +43,13 @@ pub fn main() {
<> premixed.text_bright_orange(process.cwd())
<> "!",
)
- use m <- promise.await(mutable_model_type.new())
- case process.argv() |> array.to_list() |> list.drop(2) {
- ["dynamic", ..] | ["host", ..] ->
- dynamic_site_server(m, 60_000) |> promise.resolve
- ["preview", ..] -> dynamic_site_server(m, 20) |> promise.resolve
- ["pregenerate", ..] | ["static"] -> static_site_server(m)
+ let args = process.argv() |> array.to_list() |> list.drop(2)
+ case args {
+ ["pregenerate", ..] | ["static"] -> start()
["init", ..] | ["initialise", ..] -> {
- config.initcfg()
- |> promise.resolve
+ init(args |> list.includes("--force"))
}
- ["man", ..] | ["help", ..] | ["--help", ..] | ["-h", ..] | [] -> {
+ _ -> {
case process.argv() |> array.to_list() |> list.drop(2) {
[] -> console.error("No subcommand given.\n")
_ -> Nil
@@ -85,25 +69,13 @@ pub fn main() {
premixed.text_pink("initialise\n"),
])
<> "\t\t\t\tInitialise the config file then exit\n\n"
- // Dynamic:
+ // Run:
<> string.concat([
- premixed.text_pink("\tdynamic"),
- " | ",
- premixed.text_pink("host\n"),
- ])
- <> "\t\t\t\tStart a dynamic website server\n\n"
- // Pregenerate:
- <> string.concat([
- premixed.text_pink("\tstatic"),
+ premixed.text_pink("\trun"),
" | ",
premixed.text_pink("pregenerate\n"),
])
<> "\t\t\t\tGenerate a static website\n\n"
- // Preview:
- <> premixed.text_pink("\tpreview\n")
- <> "\t\t\t\tStart a dynamic website server for previewing\n"
- <> "\t\t\t\tthis is the same as dynamic, but with a shorter\n"
- <> "\t\t\t\tinterval for the cache\n\n"
// Help:
<> premixed.text_lilac("\thelp")
<> "\n"
@@ -114,7 +86,6 @@ pub fn main() {
)
<> ".\n",
)
- |> promise.resolve
}
[a, ..] ->
console.error(
@@ -129,270 +100,136 @@ pub fn main() {
<> premixed.text_purple("help")
<> "´ to see a list of all subcommands.\n",
)
- |> promise.resolve
}
}
-fn dynamic_site_server(mutmodel: mutable_model_type.MutableModel, lease: Int) {
- console.info("Cynthia Mini is in dynamic site mode!")
- let model = mutmodel |> mutable_reference.get
- let conf = model.config
- {
- let folder = process.cwd() <> "/assets/cynthia-mini"
- case simplifile.create_directory_all(folder) {
- Ok(..) -> Nil
- Error(e) -> {
- console.error(
- "A problem occurred while creating the ´"
- <> folder
- <> "´ directory: "
- <> premixed.text_error_red(string.inspect(e)),
- )
- process.exit(1)
- panic as "We should not reach here"
- }
- }
+fn get_context() -> site_json.SiteJSON {
+ let config = case
+ {
+ let global_conf_filepath =
+ files.path_join([process.cwd(), "/cynthia.toml"])
+ let global_conf_filepath_exists = files.file_exist(global_conf_filepath)
- case files.file_exist(process.cwd() <> "/assets/cynthia-mini/README.md") {
- True -> Nil
- False -> {
- case
- simplifile.write(
- process.cwd() <> "/assets/cynthia-mini/README.md",
- "# What does this folder do?\n\r\n\rThis folder holds a few files Cynthia Mini serves to the browser to make sure everything works alright.\n\r\n\rThese are usually checked and downloaded if necessary only during start of the server,\n\rso try not to touch them! If you believe one of the files in here might be faulty, delete it, and restart the server.\n\r\n\rHave a nice day! :)",
- )
- {
- Ok(..) -> Nil
- Error(e) -> {
- console.error(
- "A problem occurred while creating the ´"
- <> process.cwd()
- <> "/assets/cynthia-mini/README.md"
- <> "´ file: "
- <> premixed.text_error_red(string.inspect(e)),
- )
- process.exit(1)
- panic as "We should not reach here"
+ case global_conf_filepath_exists {
+ True -> {
+ Nil
+ }
+ // No config was found. Let's look for legacy config or initialise.
+ False -> {
+ let global_conf_filepath_legacy =
+ files.path_join([process.cwd(), "/cynthia-mini.toml"])
+ let global_conf_filepath_legacy_exists =
+ files.file_exist(global_conf_filepath_legacy)
+ case
+ global_conf_filepath_legacy_exists,
+ simplifile.read(global_conf_filepath_legacy)
+ {
+ True, Ok(legacy_config) -> {
+ console.warn(
+ "A legacy config file was found! Cynthia Mini will attempt to auto-convert it on the go and continue.",
+ )
+ let upgraded_config =
+ "# This file was upgraded to the universal Cynthia Config format\n# Do not edit these two variables! They are set by Cynthia to tell it's config format apart.\nconfig.edition=\"mini\"\nconfig.version=4.0\n\n"
+ <> legacy_config
+ case
+ simplifile.write(
+ to: global_conf_filepath,
+ contents: upgraded_config,
+ )
+ {
+ Ok(_) -> {
+ let _ =
+ simplifile.rename(
+ at: global_conf_filepath_legacy,
+ to: global_conf_filepath_legacy <> ".old",
+ )
+ Nil
+ }
+ Error(_) -> {
+ console.error(
+ "Error: Could not write upgraded config to "
+ <> global_conf_filepath
+ <> ". Please check file permissions.",
+ )
+ process.exit(1)
+ }
+ }
+ }
+ True, Error(_) -> {
+ console.error(
+ "Some error happened while trying to read "
+ <> global_conf_filepath_legacy
+ <> ".",
+ )
+ process.exit(1)
+ }
+ // No config found, and no old config found.
+ False, _ -> {
+ init()
+ get_context()
+ }
}
}
- Nil
}
+ let e = "Could not read " <> global_conf_filepath
+ let assert Ok(toml) = simplifile.read(global_conf_filepath) as e
+ // Call the latest decoder for it and return. If it encounters an older config format it should be able to recognise and convert by itself.
+ decodes.vp4p1mini_toml(toml)
}
- }
- console.log("Starting server...")
- case
- bun.serve(ServeOptions(
- development: Some(True),
- hostname: conf.server_host,
- port: conf.server_port,
- static_served: ssrs.ssrs(mutmodel),
- handler: web.handle_request(_, mutmodel),
- id: None,
- reuse_port: None,
- ))
{
- Ok(..) -> {
- console.log(
- "Server started! Running on: "
- <> premixed.text_cyan(
- "http://"
- <> option.unwrap(conf.server_host, "localhost")
- <> ":"
- <> int.to_string(option.unwrap(conf.server_port, 8080))
- <> "/",
- ),
- )
- global.set_interval(lease, fn() {
- mutable_reference.update(mutmodel, fn(model) {
- case model.cached_response {
- None -> model
- Some(..) ->
- // Drops the cached response to keep it updated
- mutable_model_type.MutableModelContent(
- ..model,
- cached_response: None,
- )
- }
- })
- })
- }
- Error(e) -> {
- console.error(
- "A problem occurred while starting the server: "
- <> premixed.text_error_red(string.inspect(e)),
- )
+ Ok(conf) -> conf
+ Error(_) -> {
process.exit(1)
- panic as "We should not reach here"
+ panic as "Shouldn't be here."
}
}
+ let content = {
+ todo
+ }
- Nil
+ site_json.SiteJSON(config, content)
}
-fn static_site_server(mutmodel: mutable_model_type.MutableModel) {
- console.info("Cynthia Mini is in pregeneration mode!")
-
- {
- let folder = process.cwd() <> "/assets/cynthia-mini"
- case simplifile.create_directory_all(folder) {
- Ok(..) -> Nil
- Error(e) -> {
- console.error(
- "A problem occurred while creating the ´"
- <> folder
- <> "´ directory: "
- <> premixed.text_error_red(string.inspect(e)),
- )
- process.exit(1)
- panic as "We should not reach here"
- }
- }
-
- case files.file_exist(process.cwd() <> "/assets/cynthia-mini/README.md") {
- True -> Nil
- False -> {
- case
- simplifile.write(
- process.cwd() <> "/assets/cynthia-mini/README.md",
- "# What does this folder do?\n\r\n\rThis folder holds a few files Cynthia Mini serves to the browser to make sure everything works alright.\n\r\n\rThese are usually checked and downloaded if necessary only during start of the server,\n\rso try not to touch them! If you believe one of the files in here might be faulty, delete it, and restart the server.\n\r\n\rHave a nice day! :)",
- )
- {
- Ok(..) -> Nil
- Error(e) -> {
- console.error(
- "A problem occurred while creating the ´"
- <> process.cwd()
- <> "/assets/cynthia-mini/README.md"
- <> "´ file: "
- <> premixed.text_error_red(string.inspect(e)),
- )
- process.exit(1)
- panic as "We should not reach here"
- }
- }
- Nil
- }
- }
- }
-
- use complete_data <- promise.await(config.load())
+import cynthia_websites_mini_shared/config/v4_1/decodes
- // Generate JSON representations
- let complete_data_json =
- complete_data |> configtype.encode_complete_data_for_client
- let res_string = complete_data_json |> json.to_string
- let res_jsonld = jsonld.generate_jsonld(complete_data)
- let opt_sitemap = sitemap.generate_sitemap(complete_data)
+pub fn create_html(json: site_json.SiteJSON, path: String) {
+ "
+
+
- let outdir = process.cwd() <> "/out"
- case simplifile.create_directory_all(outdir) {
- Ok(..) -> Nil
- Error(e) -> {
- console.error(
- "A problem occurred while creating the ´out´ directory: "
- <> premixed.text_error_red(string.inspect(e)),
- )
- process.exit(1)
- panic as "We should not reach here"
- }
- }
- case simplifile.write(to: outdir <> "/site.json", contents: res_string) {
- Ok(..) -> Nil
- Error(e) -> {
- console.error(
- "A problem occurred while creating the ´site.json´ file: "
- <> premixed.text_error_red(string.inspect(e)),
- )
- process.exit(1)
- panic as "We should not reach here"
- }
- }
- case
- simplifile.write(
- to: outdir <> "/index.html",
- contents: ssrs.index_html(mutable_reference.get(mutmodel)),
- )
- {
- Ok(..) -> Nil
- Error(e) -> {
- console.error(
- "A problem occurred while creating the ´index.html´ file: "
- <> premixed.text_error_red(string.inspect(e)),
- )
- process.exit(1)
- panic as "We should not reach here"
- }
- }
- case
- simplifile.copy_directory(
- at: process.cwd() <> "/assets/",
- to: outdir <> "/assets/",
- )
- {
- Ok(..) -> Nil
- Error(e) -> {
- console.error(
- "A problem occurred while copying the assets directory: "
- <> premixed.text_error_red(string.inspect(e)),
- )
- process.exit(1)
- panic as "We should not reach here"
- }
- }
- case simplifile.is_file(outdir <> "/site.json") {
- Ok(True) -> Nil
- _ -> {
- console.error(
- "An unknown problem occurred while creating the ´site.json´ file.",
- )
- process.exit(1)
- panic as "We should not reach here"
- }
- }
- case simplifile.is_file(outdir <> "/index.html") {
- Ok(True) -> Nil
- _ -> {
- console.error(
- "An unknown problem occurred while creating the ´index.html´ file.",
- )
- process.exit(1)
- panic as "We should not reach here"
- }
- }
- case opt_sitemap {
- None -> Nil
- Some(res_sitemap) -> {
- case
- simplifile.write(to: outdir <> "/sitemap.xml", contents: res_sitemap)
- {
- Ok(..) -> Nil
- Error(e) -> {
- console.error(
- "A problem occurred while creating the ´sitemap.xml´ file: "
- <> premixed.text_error_red(string.inspect(e)),
- )
- process.exit(1)
- panic as "We should not reach here"
- }
- }
- }
- }
+
+<<site hosted by Cynthia mini>>
+ "/>
+ "/>
+" <> case model.cached_jsonld {
+ Some(jsonld) ->
+ ""
+ None -> " "
+ } <> "
+
+
+
+
+
+
+
+
+ " <> first_view <> "
+
+ " <> footer(True, json.config.integrations.git) <> "
+
+
+"
+}
- console.info(
- premixed.text_ok_green("Site pregeneration complete!")
- <> " Serve files from "
- <> premixed.text_orange(outdir <> "/")
- <> " and you should have a site running!",
- )
- promise.resolve(Nil)
+fn init(forced: Bool) {
+ todo
}
diff --git a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config.gleam b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config.gleam
deleted file mode 100644
index e2afaa7..0000000
--- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config.gleam
+++ /dev/null
@@ -1,711 +0,0 @@
-import bungibindies/bun
-import bungibindies/bun/spawn
-import cynthia_websites_mini_client/configtype
-import cynthia_websites_mini_client/contenttypes
-import cynthia_websites_mini_server/config/v4
-import cynthia_websites_mini_server/utils/files
-import cynthia_websites_mini_server/utils/prompts
-import gleam/bool
-import gleam/dynamic/decode
-import gleam/fetch
-import gleam/float
-import gleam/http/request
-import gleam/int
-import gleam/javascript/promise.{type Promise}
-import gleam/json
-import gleam/list
-import gleam/option.{None, Some}
-import gleam/result
-import gleam/string
-import gleamy_lights/premixed
-import plinth/javascript/console
-import plinth/node/fs
-import plinth/node/process
-import simplifile
-import tom
-
-/// # Config.load()
-/// Loads the configuration from the `cynthia.toml` file and the content from the `content` directory.
-/// Then saves the configuration to the database.
-pub fn load() -> Promise(configtype.CompleteData) {
- use global_config <- promise.await(capture_config())
- use content_list <- promise.await(content_getter())
- let content = case content_list {
- Ok(lis) -> lis
- Error(msg) -> {
- console.error("Error: There was an error getting content:\n" <> msg)
- process.exit(1)
- panic as "We should not reach here"
- }
- }
-
- let complete_data = configtype.merge(global_config, content)
- complete_data
- |> promise.resolve
-}
-
-pub fn capture_config() {
- let global_conf_filepath = files.path_join([process.cwd(), "/cynthia.toml"])
- let global_conf_filepath_exists = files.file_exist(global_conf_filepath)
-
- case global_conf_filepath_exists {
- True -> Nil
- False -> {
- let global_conf_filepath_legacy =
- files.path_join([process.cwd(), "/cynthia-mini.toml"])
- let global_conf_filepath_legacy_exists =
- files.file_exist(global_conf_filepath_legacy)
- case
- global_conf_filepath_legacy_exists,
- simplifile.read(global_conf_filepath_legacy)
- {
- True, Ok(legacy_config) -> {
- console.warn(
- "A legacy config file was found! Cynthia Mini will attempt to auto-convert it on the go and continue.",
- )
- let upgraded_config =
- "# This file was upgraded to the universal Cynthia Config format\n# Do not edit these two variables! They are set by Cynthia to tell it's config format apart.\nconfig.edition=\"mini\"\nconfig.version=4\n\n"
- <> legacy_config
- case
- simplifile.write(
- to: global_conf_filepath,
- contents: upgraded_config,
- )
- {
- Ok(_) -> {
- let _ =
- simplifile.rename(
- at: global_conf_filepath_legacy,
- to: global_conf_filepath_legacy <> ".old",
- )
- Nil
- }
- Error(_) -> {
- console.error(
- "Error: Could not write upgraded config to "
- <> global_conf_filepath
- <> ". Please check file permissions.",
- )
- process.exit(1)
- }
- }
- }
- True, Error(_) -> {
- console.error(
- "Some error happened while trying to read "
- <> global_conf_filepath_legacy
- <> ".",
- )
- process.exit(1)
- }
- False, _ -> {
- dialog_initcfg()
- process.exit(0)
- }
- }
- }
- }
- let global_conf_content_sync =
- simplifile.read(global_conf_filepath) |> result.unwrap("")
- let m = case parse_config_format(global_conf_content_sync) {
- // Correct config format: mini-4
- Ok(#("mini", 4)) -> v4.parse_mini()
-
- // Erronous config format outcomes
- Ok(#(c, d)) -> promise_error_unknown_config_format(c, d)
- Error(_) -> promise_error_cannot_read_config_format()
- }
- use parse_configtoml_result <- promise.await(m)
-
- let global_config = case parse_configtoml_result {
- Ok(config) -> config
- Error(why) -> {
- premixed.text_error_red("Error: Could not load cynthia.toml: " <> why)
- |> console.error
- process.exit(1)
- panic as "We should not reach here"
- }
- }
- global_config
- |> promise.resolve()
-}
-
-fn promise_error_unknown_config_format(
- edition: String,
- version: Int,
-) -> Promise(Result(a, String)) {
- let err =
- "Config version "
- <> version |> int.to_string()
- <> " with edition '"
- <> edition
- <> "' is NOT supported by this version of Cynthia."
- <> "\n Usually this means one of these options:"
- <> "\n - it was written for a different edition"
- <> "\n - it is invalid"
- <> "\n - or this version of cynthia is too old to understand this file."
- <> case edition == "mini" {
- True ->
- "\n\n\n It seems to be that last option, since the edition it is written for, does match 'mini'."
- False -> ""
- }
- promise.resolve(Error(err))
-}
-
-fn promise_error_cannot_read_config_format() {
- promise.resolve(Error(
- "Cannot properly read config.edition and/or config.version, Cynthia doesn't know how to parse this file anymore!",
- ))
-}
-
-fn parse_config_format(toml_str: String) -> Result(#(String, Int), Nil) {
- use d <- result.try(tom.parse(toml_str) |> result.replace_error(Nil))
- use edition <- result.try(
- tom.get_string(d, ["config", "edition"]) |> result.replace_error(Nil),
- )
- use version <- result.try(
- tom.get_int(d, ["config", "version"]) |> result.replace_error(Nil),
- )
- Ok(#(edition, version))
-}
-
-fn content_getter() -> promise.Promise(
- Result(List(contenttypes.Content), String),
-) {
- let promises: List(Promise(Result(contenttypes.Content, String))) = {
- fn(file) {
- file
- |> string.replace(".meta.json", "")
- |> files.path_normalize()
- }
- |> fn(value) {
- list.map(
- list.filter(
- result.unwrap(
- simplifile.get_files(files.path_join([process.cwd() <> "/content"])),
- [],
- ),
- fn(file) { file |> string.ends_with(".meta.json") },
- ),
- value,
- )
- }
- |> list.map(get_inner_and_meta)
- }
- promise.map(promise.await_list(promises), result.all)
-}
-
-fn get_inner_and_meta(
- file: String,
-) -> Promise(Result(contenttypes.Content, String)) {
- use meta_json <- promise.try_await(
- simplifile.read(file <> ".meta.json")
- |> result.replace_error(
- "FS error while reading ´" <> file <> ".meta.json´.",
- )
- |> promise.resolve,
- )
- // Sometimes stuff is saved somewhere else, like in a different file path or maybe somewhere on the web, of course Cynthia Mini can still find those files!
- // ...However, we first need to know there is an "external" file somewhere, we do that by checking the 'path' field.
- // The extension before .meta.json is still used to parse the content.
- let possibly_extern =
- json.parse(meta_json, {
- use path <- decode.optional_field("path", "", decode.string)
- decode.success(path)
- })
- |> result.unwrap("")
- |> string.to_option
- use permalink <- promise.try_await(
- json.parse(meta_json, {
- use path <- decode.optional_field("permalink", "", decode.string)
- decode.success(path)
- })
- |> result.replace_error("Could not decode permalink for ´" <> file <> "´")
- |> promise.resolve,
- )
-
- use inner_plain <- promise.try_await({
- // This case also check if the permalink starts with "!", in which case it is a content list.
- // Content lists will be generated on the client side, and their pre-given content
- // will be discarded, so loading it in from anywhere would be a waste of resources.
- case string.starts_with(permalink, "!"), possibly_extern {
- True, _ -> promise.resolve(Ok(""))
- False, None -> {
- promise.resolve(
- simplifile.read(file)
- |> result.replace_error("FS error while reading ´" <> file <> "´."),
- )
- }
- False, Some(p) -> get_ext(p)
- }
- })
-
- // Now, conversion to Djot for markdown files done in-place:
- let converted: Result(#(String, String), String) = case
- string.ends_with(file, "markdown")
- |> bool.or(
- string.ends_with(file, "md") |> bool.or(string.ends_with(file, "mdown")),
- )
- {
- True -> {
- // If the file is external, we need to write it to a temporary file first.
- let wri = case possibly_extern {
- Some(..) -> {
- simplifile.write(file, inner_plain)
- |> result.replace_error(
- "There was an error while writing the external content to '"
- <> file |> premixed.text_bright_yellow()
- <> "'.",
- )
- }
- None -> Ok(Nil)
- }
- use _ <- result.try(wri)
-
- use pandoc_path <- result.try(result.replace_error(
- bun.which("pandoc"),
- "There is a markdown file in Cynthia's content folder, but to convert that to Djot and display it, you need to have Pandoc installed on the PATH, which it is not!",
- ))
- let pandoc_child =
- spawn.sync(spawn.OptionsToSubprocess(
- [pandoc_path, file, "-f", "gfm", "-t", "djot"],
- cwd: Some(process.cwd()),
- env: None,
- stderr: Some(spawn.Pipe),
- stdout: Some(spawn.Pipe),
- ))
- let pandoc_child = case
- {
- let assert spawn.SyncSubprocess(asserted_sync_child) = pandoc_child
- spawn.success(asserted_sync_child)
- }
- {
- True -> Ok(pandoc_child)
- False -> {
- Error(
- "There was an error while trying to convert '"
- <> file |> premixed.text_bright_yellow()
- <> "' to Djot: \n"
- <> result.unwrap(spawn.stderr(pandoc_child), "")
- <> "\n\nMake sure you have at least Pandoc 3.7.0 installed on your system, earlier versions may not work correctly.",
- )
- }
- }
- use pandoc_child <- result.try(pandoc_child)
- let new_inner_plain: Result(String, String) =
- spawn.stdout(pandoc_child)
- |> result.replace_error("")
- use new_inner_plain <- result.try(new_inner_plain)
-
- // If the file was external, we need delete the temporary file.
- let re = case possibly_extern {
- Some(..) -> {
- simplifile.delete(file)
- |> result.replace_error(
- "There was an error while deleting the temporary file '"
- <> file |> premixed.text_bright_yellow()
- <> "'.",
- )
- }
- None -> Ok(Nil)
- }
-
- use _ <- result.try(re)
-
- Ok(#(new_inner_plain, file <> ".dj"))
- }
-
- False -> {
- Ok(#(inner_plain, file))
- }
- }
-
- let metadata = case converted {
- Ok(#(inner_plain, file)) -> {
- let decoder = contenttypes.content_decoder_and_merger(inner_plain, file)
- json.parse(meta_json, decoder)
- |> result.map_error(fn(e) {
- "Some error decoding metadata for ´"
- <> file |> premixed.text_magenta()
- <> "´: "
- <> string.inspect(e)
- })
- }
- Error(l) -> Error(l)
- }
-
- promise.resolve(metadata)
-}
-
-/// Gets external content, beit by file path or by http(s) url.
-fn get_ext(path: String) -> promise.Promise(Result(String, String)) {
- case string.starts_with(string.lowercase(path), "http") {
- True -> {
- let start = bun.nanoseconds()
- console.log(
- "Downloading external content ´" <> premixed.text_blue(path) <> "´...",
- )
-
- let assert Ok(req) = request.to(path)
- use resp <- promise.try_await(
- promise.map(fetch.send(req), fn(e) {
- result.replace_error(
- e,
- "Error while downloading external content ´"
- <> path
- <> "´: "
- <> string.inspect(e),
- )
- }),
- )
- use resp <- promise.try_await(
- promise.map(fetch.read_text_body(resp), fn(e) {
- result.replace_error(
- e,
- "Error while reading external content ´"
- <> path
- <> "´: "
- <> string.inspect(e),
- )
- }),
- )
- let end = bun.nanoseconds()
- let duration_ms = { end -. start } /. 1_000_000.0
- case resp.status {
- 200 -> {
- console.log(
- "Downloaded external content ´"
- <> premixed.text_blue(path)
- <> "´ in "
- <> int.to_string(duration_ms |> float.truncate)
- <> "ms!",
- )
- Ok(resp.body)
- }
- _ -> {
- Error(
- "Error while downloading external content ´"
- <> path
- <> "´: "
- <> string.inspect(resp.status),
- )
- }
- }
- |> promise.resolve
- }
- False -> {
- // Is a file path
- promise.resolve(
- simplifile.read(path)
- |> result.replace_error(
- "FS error while reading external content file ´" <> path <> "´.",
- ),
- )
- }
- }
-}
-
-fn dialog_initcfg() {
- console.log("No Cynthia Mini configuration found...")
- case
- prompts.for_confirmation(
- "CynthiaMini can create \n"
- <> premixed.text_orange(process.cwd() <> "/cynthia.toml")
- <> "\n ...and some sample content.\n"
- <> premixed.text_magenta(
- "Do you want to initialise new config at this location?",
- ),
- True,
- )
- {
- False -> {
- console.error("No Cynthia Mini configuration found... Exiting.")
- process.exit(1)
- panic as "We should not reach here"
- }
- True -> initcfg()
- }
-}
-
-const brand_new_config = "# Do not edit these variables! It is set by Cynthia to tell it's config format apart.
-config.edition=\"mini\"
-config.version=4
-[global]
-# Theme to use for light mode - default themes: autumn, default
-theme = \"autumn\"
-# Theme to use for dark mode - default themes: night, default-dark
-theme_dark = \"night\"
-# For some browsers, this will change the colour of UI elements such as the address bar
-# and the status bar on mobile devices.
-# This is a hex colour, e.g. #FFFFFF
-colour = \"#FFFFFF\"
-# Your website's name, displayed in various places
-site_name = \"My Site\"
-# A brief description of your website
-site_description = \"A big site on a mini Cynthia!\"
-
-[server]
-# Port number for the web server
-port = 8080
-# Host address for the web server
-host = \"localhost\"
-
-[integrations]
-# Enable git integration for the website
-# This will allow Cynthia Mini to detect the git repository
-# For example linking to the commit hash in the footer
-git = true
-
-# Enable sitemap generation
-# This will generate a sitemap.xml file in the root of the website
-#
-# You will need to enter the base URL of your website in the sitemap variable below.
-# If your homepage is at \"https://example.com/#/\", then the sitemap variable should be set to \"https://example.com\".
-# If you do not want to use a sitemap, set this to \"false\", or leave it empty (\"\"), you can also remove the sitemap variable altogether.
-sitemap = \"\"
-
-# Enable crawlable context (JSON-LD injection)
-# This will allow search engines to crawl the website, and makes it
-# possible for the website to be indexed by search engine and LLMs.
-crawlable_context = false
-
-[variables]
-# You can define your own variables here, which can be used in templates.
-
-## ownit_template
-##
-## Use this to define your own template for the 'ownit' layout.
-##
-## The template will be used for the 'ownit' layout, which is used for pages and posts.
-## You can use the following variables in the template:
-## - body: string (The main HTML content)
-## - is_post: boolean (True if the current item is a post, false if it's a page)
-## - title: string (The title of the page or post)
-## - description: string (The description of the page or post)
-## - site_name: string (The global site name)
-## - category: string (The category of the post, empty for pages)
-## - date_modified: string (The last modification date of the post, empty for pages)
-## - date_published: string (The publication date of the post, empty for pages)
-## - tags: string[] (An array of tags for the post, empty for pages)
-## - menu_1_items: [string, string][] (Array of menu items for menu 1, e.g., [[\"Home\", \"/\"], [\"About\", \"/about\"]])
-## - menu_2_items: [string, string][] (Array of menu items for menu 2)
-## - menu_3_items: [string, string][] (Array of menu items for menu 3)
-ownit_template = \"\"\"
-
- {{ title }}
-
- {{#if is_post}}
-
- Published: {{ date_published }}
- {{#if category }} | Category: {{ category }}{{/if}}
-
- {{/if}}
-
-
- {{{ body }}}
-
- {{#if is_post}}
- {{#if tags}}
-
- Tags:
- {{#each tags}}
-{{this}}
- {{/each}}
-
- {{/if}}
- {{/if}}
-
-\"\"\"
-
-[posts]
-# Enable comments on posts using utteranc.es
-# Format: \"username/repositoryname\"
-#
-# You will need to give the utterances bot access to your repo.
-# See https://github.com/apps/utterances to add the utterances bot to your repo
-comment_repo = \"\""
-
-pub fn initcfg() {
- console.log("Creating Cynthia Mini configuration...")
- // Check if cynthia.toml exists
- case files.file_exist(process.cwd() <> "/cynthia.toml") {
- True -> {
- console.error(
- "Error: A config already exists in this directory. Please remove it and try again.",
- )
- process.exit(1)
- panic as "We should not reach here"
- }
- False -> Nil
- }
- let assert Ok(_) =
- simplifile.create_directory_all(process.cwd() <> "/content")
- let assert Ok(_) = simplifile.create_directory_all(process.cwd() <> "/assets")
- let _ =
- { process.cwd() <> "/cynthia.toml" }
- |> fs.write_file_sync(brand_new_config)
- |> result.map_error(fn(e) {
- console.error(premixed.text_error_red(
- "Error: Could not write cynthia.toml: " <> e,
- ))
- process.exit(1)
- })
- {
- console.log("Downloading default site icon...")
- // Download https://raw.githubusercontent.com/strawmelonjuice/CynthiaWebsiteEngine-mini/refs/heads/main/asset/153916590.png to assets/site_icon.png
- // Ignore any errors, if it fails, it fails.
- let assert Ok(req) =
- request.to(
- "https://raw.githubusercontent.com/strawmelonjuice/CynthiaWebsiteEngine-mini/refs/heads/main/asset/153916590.png",
- )
- use resp <- promise.try_await(fetch.send(req))
- use resp <- promise.try_await(fetch.read_bytes_body(resp))
- case
- simplifile.write_bits(process.cwd() <> "/assets/site_icon.png", resp.body)
- {
- Ok(_) -> Nil
- Error(_) -> {
- console.error("Error: Could not write assets/site_icon.png")
- Nil
- }
- }
- promise.resolve(Ok(Nil))
- }
- {
- console.log("Creating example content...")
- [
- item(
- to: "hangers.dj",
- with: contenttypes.Content(
- filename: "hangers.dj",
- title: "Hangers",
- description: "An example page about hangers",
- layout: "theme",
- permalink: "/hangers",
- data: contenttypes.PageData([2], False),
- inner_plain: "I have no clue. What are hangers again?
-
-This page will only show up if you have a layout with two or more menus available! :)",
- ),
- ),
- ext_item(
- to: "themes.dj",
- // We are downloading markdown content as Djot content without conversion... Hopefully it'll parse correctly.
- // Until the documentation is updated to reflect the new default file type :)
- from: "https://raw.githubusercontent.com/CynthiaWebsiteEngine/Mini-docs/refs/heads/main/content/3.%20Customisation/3.2-themes.dj",
- with: contenttypes.Content(
- filename: "themes.dj",
- title: "Themes",
- description: "External page example, using the theme list, downloading from ",
- layout: "theme",
- permalink: "/themes",
- data: contenttypes.PageData([1], False),
- inner_plain: "",
- ),
- ),
- item(
- "index.dj",
- contenttypes.Content(
- filename: "index.dj",
- title: "Example landing",
- description: "This is an example index page",
- layout: "cindy-landing",
- permalink: "/",
- data: contenttypes.PageData([1], True),
- inner_plain: configtype.ootb_index,
- ),
- ),
- item(
- to: "example-post.dj",
- with: contenttypes.Content(
- filename: "",
- title: "An example post!",
- description: "This is an example post",
- layout: "theme",
- permalink: "/example-post",
- data: contenttypes.PostData(
- category: "example",
- date_published: "2021-01-01",
- date_updated: "2021-01-01",
- tags: ["example"],
- ),
- inner_plain: "# Hello, World!\n\nHello! This is an example post, you'll find me at `content/example-post.dj`.",
- ),
- ),
- item(
- to: "posts",
- with: contenttypes.Content(
- filename: "posts",
- title: "Posts",
- description: "this page is not actually shown, due to the ! prefix in the permalink",
- layout: "default",
- permalink: "!/",
- data: contenttypes.PageData(in_menus: [1], hide_meta_block: True),
- inner_plain: "",
- ),
- ),
- ]
- |> list.flatten
- |> write_posts_and_pages_to_fs
- }
-}
-
-fn item(
- to path: String,
- with content: contenttypes.Content,
-) -> List(#(String, String)) {
- let path = files.path_join([process.cwd(), "/content/", path])
- let meta_json =
- content
- |> contenttypes.encode_content_for_fs
- |> json.to_string()
- let meta_path = path <> ".meta.json"
- case string.starts_with(content.permalink, "!") {
- True -> {
- // No content file for post lists.
- [#(meta_path, meta_json)]
- }
- False -> [#(meta_path, meta_json), #(path, content.inner_plain)]
- }
-}
-
-fn ext_item(
- to fpath: String,
- from path: String,
- with content: contenttypes.Content,
-) -> List(#(String, String)) {
- let meta_json =
- json.object([
- #("path", json.string(path)),
- #("title", json.string(content.title)),
- #("description", json.string(content.description)),
- #("layout", json.string(content.layout)),
- #("permalink", json.string(content.permalink)),
- #("data", contenttypes.encode_content_data(content.data)),
- ])
- |> json.to_string()
-
- [
- #(
- files.path_join([process.cwd(), "/content/", fpath]) <> ".meta.json",
- meta_json,
- ),
- ]
-}
-
-// What? The function name is descriptive!
-fn write_posts_and_pages_to_fs(items: List(#(String, String))) -> Nil {
- items
- |> list.each(fn(set) {
- let #(path, content) = set
- path
- |> fs.write_file_sync(content)
- })
-}
diff --git a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config/v4.gleam b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config/v4.gleam
deleted file mode 100644
index 4306bcf..0000000
--- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/config/v4.gleam
+++ /dev/null
@@ -1,468 +0,0 @@
-//// Cynthia v4 Config format
-
-import bungibindies/bun
-import cynthia_websites_mini_client/configtype
-import cynthia_websites_mini_client/configurable_variables
-import cynthia_websites_mini_server/utils/files
-import gleam/bit_array
-import gleam/bool
-import gleam/dict
-import gleam/fetch
-import gleam/float
-import gleam/http/request
-import gleam/int
-import gleam/javascript/promise.{type Promise}
-import gleam/list
-import gleam/option.{None, Some}
-import gleam/result
-import gleam/string
-import gleamy_lights/premixed
-import plinth/javascript/console
-import plinth/node/fs
-import plinth/node/process
-import simplifile
-import tom
-
-/// Parses the mini edition format for v4
-pub fn parse_mini() -> Promise(
- Result(configtype.SharedCynthiaConfigGlobalOnly, String),
-) {
- use str <- promise.try_await(
- fs.read_file_sync(files.path_normalize(process.cwd() <> "/cynthia.toml"))
- |> result.map_error(fn(e) {
- premixed.text_error_red("Error: Could not read cynthia.toml: " <> e)
- process.exit(1)
- })
- |> result.map_error(string.inspect)
- |> promise.resolve(),
- )
- use res <- promise.try_await(
- tom.parse(str) |> result.map_error(string.inspect) |> promise.resolve(),
- )
-
- use config <- promise.try_await(
- cynthia_config_global_only_exploiter(res)
- |> promise.map(result.map_error(_, string.inspect)),
- )
- promise.resolve(Ok(config))
-}
-
-type ConfigTomlDecodeError {
- TomlGetStringError(tom.GetError)
- TomlGetIntError(tom.GetError)
- FieldError(String)
-}
-
-fn cynthia_config_global_only_exploiter(
- o: dict.Dict(String, tom.Toml),
-) -> Promise(
- Result(configtype.SharedCynthiaConfigGlobalOnly, ConfigTomlDecodeError),
-) {
- use global_theme <- promise.try_await(
- {
- use field <- result.try(
- tom.get(o, ["global", "theme"])
- |> result.replace_error(FieldError(
- "config->global.theme does not exist",
- )),
- )
- tom.as_string(field)
- |> result.map_error(TomlGetStringError)
- }
- |> promise.resolve(),
- )
- use global_theme_dark <- promise.try_await(
- {
- use field <- result.try(
- tom.get(o, ["global", "theme_dark"])
- |> result.replace_error(FieldError(
- "config->global.theme_dark does not exist",
- )),
- )
- tom.as_string(field)
- |> result.map_error(TomlGetStringError)
- }
- |> promise.resolve(),
- )
- use global_colour <- promise.try_await(
- {
- use field <- result.try(
- tom.get(o, ["global", "colour"])
- |> result.replace_error(FieldError(
- "config->global.colour does not exist",
- )),
- )
- tom.as_string(field)
- |> result.map_error(TomlGetStringError)
- }
- |> promise.resolve(),
- )
- use global_site_name <- promise.try_await(
- {
- use field <- result.try(
- tom.get(o, ["global", "site_name"])
- |> result.replace_error(FieldError(
- "config->global.site_name does not exist",
- )),
- )
- tom.as_string(field)
- |> result.map_error(TomlGetStringError)
- }
- |> promise.resolve(),
- )
- use global_site_description <- promise.try_await(
- {
- use field <- result.try(
- tom.get(o, ["global", "site_description"])
- |> result.replace_error(FieldError(
- "config->global.site_description does not exist",
- )),
- )
- tom.as_string(field)
- |> result.map_error(TomlGetStringError)
- }
- |> promise.resolve(),
- )
- let server_port =
- option.from_result({
- use field <- result.try(
- tom.get(o, ["server", "port"])
- |> result.replace_error(FieldError("config->server.port does not exist")),
- )
- tom.as_int(field)
- |> result.map_error(TomlGetIntError)
- })
- let server_host =
- option.from_result({
- use field <- result.try(
- tom.get(o, ["server", "host"])
- |> result.replace_error(FieldError("config->server.host does not exist")),
- )
- tom.as_string(field)
- |> result.map_error(TomlGetStringError)
- })
- let comment_repo = case
- tom.get(o, ["posts", "comment_repo"]) |> result.map(tom.as_string)
- {
- Ok(Ok(field)) -> {
- Some(field)
- }
- _ -> None
- }
- let git_integration = case
- tom.get(o, ["integrations", "git"]) |> result.map(tom.as_bool)
- {
- Ok(Ok(field)) -> {
- field
- }
- _ -> True
- }
- let sitemap = case
- tom.get(o, ["integrations", "sitemap"]) |> result.map(tom.as_string)
- {
- Ok(Ok(field)) -> {
- case string.lowercase(field) {
- "" -> None
- "false" -> None
- _ -> Some(field)
- }
- }
- _ -> None
- }
- let crawlable_context = case
- tom.get(o, ["integrations", "crawlable_context"]) |> result.map(tom.as_bool)
- {
- Ok(Ok(field)) -> {
- field
- }
- _ -> False
- }
- let other_vars = case result.map(tom.get(o, ["variables"]), tom.as_table) {
- Ok(Ok(d)) ->
- {
- dict.map_values(d, fn(key, unasserted_value) {
- let promise_of_a_somewhat_asserted_value = case unasserted_value {
- tom.InlineTable(inline) -> {
- case inline |> dict.to_list() {
- [#("url", tom.String(url))] -> {
- let start = bun.nanoseconds()
- console.log(
- "Downloading external data ´"
- <> premixed.text_blue(url)
- <> "´...",
- )
-
- let req = case request.to(url) {
- Ok(r) -> r
- Error(_) -> {
- console.error(
- "Invalid URL for variable: '"
- <> url |> premixed.text_bright_yellow()
- <> "'.",
- )
- process.exit(1)
- panic as "We should not reach here."
- }
- }
- use resp <- promise.await(
- promise.map(fetch.send(req), fn(e) {
- case e {
- Ok(v) -> v
- Error(_) -> {
- console.error(
- "There was an error while trying to download '"
- <> url |> premixed.text_bright_yellow()
- <> "' to a variable.",
- )
- process.exit(1)
- panic as "We should not reach here."
- }
- }
- }),
- )
- use resp <- promise.await(
- promise.map(fetch.read_bytes_body(resp), fn(e) {
- case e {
- Ok(v) -> v
- Error(_) -> {
- console.error(
- "There was an error while trying to download '"
- <> url |> premixed.text_bright_yellow()
- <> "' to a variable.",
- )
- process.exit(1)
- panic as "We should not reach here."
- }
- }
- }),
- )
- let end = bun.nanoseconds()
- let duration_ms = { end -. start } /. 1_000_000.0
- case resp.status {
- 200 -> {
- console.log(
- "Downloaded external content ´"
- <> premixed.text_blue(url)
- <> "´ in "
- <> int.to_string(duration_ms |> float.truncate)
- <> "ms!",
- )
- [
- bit_array.base64_encode(resp.body, True),
- configurable_variables.var_bitstring,
- ]
- }
- _ -> {
- console.error(
- "There was an error while trying to download '"
- <> url |> premixed.text_bright_yellow()
- <> "' to a variable.",
- )
- process.exit(1)
- panic as "We should not reach here."
- }
- }
- |> promise.resolve()
- }
- [#("path", tom.String(path))] -> {
- // let file = bun.file(path)
- // use content <- promise.await(bunfile.text())
- // `bunfile.text()` pretends it's infallible but is not. It should return a promised result.
- //
- // Also see: https://github.com/strawmelonjuice/bungibindies/issues/2
- // Also missing: bunfile.bits(), but that is also because the bitarray and byte array transform is scary to me.
- //
- // For now, this means we continue using the sync simplifile.read_bits() function,
- case simplifile.read_bits(path) {
- Ok(bits) -> [
- bit_array.base64_encode(bits, True),
- configurable_variables.var_bitstring,
- ]
- Error(_) -> {
- console.error(
- "Unable to read file '"
- <> path |> premixed.text_bright_yellow()
- <> "' to variable.",
- )
- process.exit(1)
- panic as "Should not reach here."
- }
- }
- |> promise.resolve()
- }
- _ ->
- [configurable_variables.var_unsupported]
- |> promise.resolve()
- }
- }
- _ -> {
- case unasserted_value {
- tom.Bool(z) -> [
- bool.to_string(z),
- configurable_variables.var_boolean,
- ]
- tom.Date(date) -> [
- date.year |> int.to_string,
- date.month |> int.to_string,
- date.day |> int.to_string,
- configurable_variables.var_date,
- ]
- tom.DateTime(tom.DateTimeValue(date, time, offset)) -> {
- case offset {
- tom.Local -> [
- int.to_string(date.year),
- int.to_string(date.month),
- int.to_string(date.day),
- int.to_string(time.hour),
- int.to_string(time.minute),
- int.to_string(time.second),
- int.to_string(time.millisecond),
- configurable_variables.var_datetime,
- ]
- _ -> [configurable_variables.var_unsupported]
- }
- }
- tom.Float(a) -> [
- float.to_string(a),
- configurable_variables.var_float,
- ]
- tom.Int(b) -> [int.to_string(b), configurable_variables.var_int]
- tom.String(guitar) -> [
- guitar,
- configurable_variables.var_string,
- ]
- tom.Time(time) -> [
- int.to_string(time.hour),
- int.to_string(time.minute),
- int.to_string(time.second),
- int.to_string(time.millisecond),
- configurable_variables.var_time,
- ]
- _ -> [configurable_variables.var_unsupported]
- }
- |> promise.resolve()
- }
- }
- use reality <- promise.await(
- promise_of_a_somewhat_asserted_value
- |> promise.map(fn(somewhat_asserted_value) {
- let assert Ok(conclusion) = somewhat_asserted_value |> list.last()
- as "This must be a value, since we just actively set it above."
- conclusion
- }),
- )
- use somewhat_asserted_value <- promise.await(
- promise_of_a_somewhat_asserted_value,
- )
- let expectation =
- configurable_variables.typecontrolled
- |> list.key_find(key)
- |> result.unwrap(reality)
- // Sometimes, reality can be transitioned into expectation
- // --that's a horrible joke.
- let #(reality, expectation, somewhat_asserted_value) = {
- case reality, expectation {
- "bits", "string" -> {
- let assert Ok(b64) = somewhat_asserted_value |> list.first()
- let hopefully_bits = b64 |> bit_array.base64_decode
- case hopefully_bits {
- Ok(bits) -> {
- case bits |> bit_array.to_string() {
- Ok(str) -> #(
- configurable_variables.var_unsupported,
- expectation,
- [str, configurable_variables.var_string],
- )
- Error(..) -> #(
- configurable_variables.var_unsupported,
- expectation,
- [configurable_variables.var_unsupported],
- )
- }
- }
- Error(..) -> {
- #(configurable_variables.var_unsupported, expectation, [
- configurable_variables.var_unsupported,
- ])
- }
- }
- }
- _, _ -> #(reality, expectation, somewhat_asserted_value)
- }
- }
- let z: Result(List(String), ConfigTomlDecodeError) = case
- reality == configurable_variables.var_unsupported
- {
- True ->
- Error(FieldError(
- "variables->" <> key <> " does not contain a supported value.",
- ))
- False -> {
- { expectation == reality }
- |> bool.guard(Ok(somewhat_asserted_value), fn() {
- Error(FieldError(
- "variables->"
- <> key
- <> " does not contain the expected value. --> Expected: "
- <> expectation
- <> ", got: "
- <> reality,
- ))
- })
- }
- }
- promise.resolve(z)
- })
- }
- |> dict.to_list()
- |> list.map(fn(x) {
- let #(key, promise_of_a_value) = x
- use value <- promise.await(promise_of_a_value)
- promise.resolve(#(key, value))
- })
- |> promise.await_list()
- _ -> promise.resolve([])
- }
- use other_vars <- promise.await(other_vars)
- // A kind of manual result.all()
- let other_vars = case
- list.find_map(other_vars, fn(le) {
- let #(_key, result_of_value): #(
- String,
- Result(List(String), ConfigTomlDecodeError),
- ) = le
- case result_of_value {
- Error(err) -> Ok(err)
- _ -> Error(Nil)
- }
- })
- {
- Ok(pq) -> Error(pq)
- Error(Nil) -> {
- other_vars
- |> list.map(fn(it) {
- let assert Ok(b) = it.1
- #(it.0, b)
- })
- |> Ok
- }
- }
-
- use other_vars <- promise.try_await(other_vars |> promise.resolve)
-
- Ok(configtype.SharedCynthiaConfigGlobalOnly(
- global_theme:,
- global_theme_dark:,
- global_colour:,
- global_site_name:,
- global_site_description:,
- server_port:,
- server_host:,
- git_integration:,
- crawlable_context:,
- sitemap:,
- comment_repo:,
- other_vars:,
- ))
- |> promise.resolve()
-}
diff --git a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/mutable_model_type.gleam b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/mutable_model_type.gleam
deleted file mode 100644
index 2c7ed6b..0000000
--- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/mutable_model_type.gleam
+++ /dev/null
@@ -1,28 +0,0 @@
-import cynthia_websites_mini_client/configtype
-import cynthia_websites_mini_server/config as config_module
-import gleam/javascript/promise
-import gleam/option.{type Option, None}
-import javascript/mutable_reference
-
-pub type MutableModel =
- mutable_reference.MutableReference(MutableModelContent)
-
-pub fn new() -> promise.Promise(MutableModel) {
- use cfg <- promise.await(config_module.capture_config())
- mutable_reference.new(MutableModelContent(
- cached_response: None,
- cached_jsonld: None,
- cached_sitemap: None,
- config: cfg,
- ))
- |> promise.resolve()
-}
-
-pub type MutableModelContent {
- MutableModelContent(
- cached_response: Option(String),
- cached_jsonld: Option(String),
- cached_sitemap: Option(String),
- config: configtype.SharedCynthiaConfigGlobalOnly,
- )
-}
diff --git a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/request_ffi.ts b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/request_ffi.ts
deleted file mode 100644
index 128cfde..0000000
--- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/request_ffi.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import type { BunFile } from "bun";
-import { Ok as GleamOk, Error as GleamError } from "../../prelude";
-
-export async function get_request_body(req: Request) {
- let a = req.body!;
- const chunks: Uint8Array[] = [];
- const reader = a.getReader();
- while (true) {
- const { done, value } = await reader.read();
- if (done) {
- break;
- } else {
- chunks.push(value);
- }
- }
- return concatArrayBuffers(chunks);
-}
-
-export async function get_request_body_as_text(req: Request): Promise {
- const bits = await get_request_body(req);
- const decoder = new TextDecoder("utf-8");
- return decoder.decode(bits);
-}
-
-function concatArrayBuffers(chunks: Uint8Array[]): Uint8Array {
- const result = new Uint8Array(chunks.reduce((a, c) => a + c.length, 0));
- let offset = 0;
- for (const chunk of chunks) {
- result.set(chunk, offset);
- offset += chunk.length;
- }
- return result;
-}
-
-export async function answer_bunrequest_with_file(file: BunFile) {
- return new Response(await file.bytes());
-}
diff --git a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/ssrs.gleam b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/ssrs.gleam
index 6aa4b6f..190fa33 100644
--- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/ssrs.gleam
+++ b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/ssrs.gleam
@@ -3,9 +3,8 @@ import bungibindies/bun/http/serve/response
import bungibindies/bun/spawn.{OptionsToSubprocess}
import cynthia_websites_mini_client
import cynthia_websites_mini_client/configtype
-import cynthia_websites_mini_client/dom
import cynthia_websites_mini_client/ui
-import cynthia_websites_mini_server/mutable_model_type
+import cynthia_websites_mini_server/mutable_model_messages
import cynthia_websites_mini_server/utils/files.{client_css, client_js}
import gleam/bool
import gleam/dict
@@ -15,12 +14,11 @@ import gleam/option.{type Option, None, Some}
import gleam/result
import gleam/string
import gleamy_lights/console
-import javascript/mutable_reference
import plinth/node/process
import simplifile
-pub fn ssrs(mutable_model: mutable_model_type.MutableModel) {
+pub fn ssrs(mutable_model: mutable_model_messages.MutableModel) {
let model = mutable_model |> mutable_reference.get()
dict.new()
|> dict.insert("/index.html", main(model))
@@ -28,49 +26,7 @@ pub fn ssrs(mutable_model: mutable_model_type.MutableModel) {
|> Some
}
-pub fn index_html(model: mutable_model_type.MutableModelContent) {
- let gc: configtype.SharedCynthiaConfigGlobalOnly = model.config
- "
-
-
-
-
-<<site hosted by Cynthia mini>>
- "/>
- "/>
-
-" <> case model.cached_jsonld {
- Some(jsonld) ->
- ""
- None -> " "
- } <> "
-
-
-
-
-
-
-
-
-
- " <> footer(True, gc.git_integration) <> "
-
-
-"
-}
-
-fn main(model: mutable_model_type.MutableModelContent) {
+fn main(model: mutable_model_messages.MutableModelContent) {
response.new()
|> response.set_body(index_html(model))
|> response.set_headers(
@@ -110,8 +66,11 @@ pub fn footer(can_hide: Bool, git_integration: Bool) {
True ->
[ui.footer]
|> list.append(
- case { simplifile.is_directory(process.cwd() <> "/.git/") } {
- Ok(True) -> {
+ case
+ { simplifile.is_directory(process.cwd() <> "/.git/") },
+ which("git")
+ {
+ Ok(True), Some(git) -> {
console.log("[Git integration] git repository detected")
[
", created from "
@@ -119,7 +78,7 @@ pub fn footer(can_hide: Bool, git_integration: Bool) {
helper_get_git_remote_commit(),
{
spawn.sync(OptionsToSubprocess(
- cmd: ["git", "rev-parse", "--short", "HEAD"],
+ cmd: [git, "rev-parse", "--short", "HEAD"],
cwd: Some(process.cwd()),
env: None,
stderr: Some(spawn.Ignore),
@@ -163,13 +122,13 @@ pub fn footer(can_hide: Bool, git_integration: Bool) {
window.setTimeout(function () {
let lastScrollTop = 0;
let ticking = false;
-
+
function handleScroll(event) {
if (!ticking) {
requestAnimationFrame(function() {
const footer = document.querySelector('#cynthiafooter');
let scrollingDown;
-
+
if (event.type === 'wheel') {
// For wheel events, use deltaY
scrollingDown = event.deltaY > 0;
@@ -180,7 +139,7 @@ pub fn footer(can_hide: Bool, git_integration: Bool) {
scrollingDown = currentScroll > (target.lastScrollTop || 0);
target.lastScrollTop = currentScroll;
}
-
+
if (scrollingDown) {
// Scrolling down
footer.style.transform = 'translate3d(0, 40px, 0)';
@@ -190,7 +149,7 @@ pub fn footer(can_hide: Bool, git_integration: Bool) {
footer.style.transform = 'translate3d(0, 0, 0)';
footer.style.opacity = '1';
}
-
+
ticking = false;
});
ticking = true;
@@ -200,7 +159,7 @@ pub fn footer(can_hide: Bool, git_integration: Bool) {
// Listen at the document level for all scroll events
document.addEventListener('scroll', handleScroll, { capture: true, passive: true });
document.addEventListener('wheel', handleScroll, { capture: true, passive: true });
-
+
document.querySelector('#cynthiafooter').addEventListener('click', function () {
this.style.transform = 'translate3d(0, 0, 0)';
this.style.opacity = '1';
@@ -213,31 +172,32 @@ pub fn footer(can_hide: Bool, git_integration: Bool) {
/// If succeeds, returns a html link to the current commit on the remote, by just removing the last part of the URL and adding "/commit/".
fn helper_get_git_remote_commit() -> Option(String) {
- let remote_cmd =
- spawn.sync(OptionsToSubprocess(
- cmd: ["git", "config", "--get", "remote.origin.url"],
- cwd: Some(process.cwd()),
- env: None,
- stderr: Some(spawn.Ignore),
- stdout: Some(spawn.Pipe),
- ))
- |> spawn.stdout()
- |> result.map(string.trim)
- |> option.from_result()
- |> option.map(fn(str) {
- case string.ends_with(str, ".git") {
- True -> string.drop_end(str, 4)
- False -> str
- }
- })
- use remote <- option.then(remote_cmd)
- // If remote does not start with http(s), we can't use it.
- use <- bool.guard(
- when: bool.negate(string.starts_with(remote, "http")),
- return: None,
- )
case which("git") {
Ok(git) -> {
+ let remote_cmd =
+ spawn.sync(OptionsToSubprocess(
+ cmd: [git, "config", "--get", "remote.origin.url"],
+ cwd: Some(process.cwd()),
+ env: None,
+ stderr: Some(spawn.Ignore),
+ stdout: Some(spawn.Pipe),
+ ))
+ |> spawn.stdout()
+ |> result.map(string.trim)
+ |> option.from_result()
+ |> option.map(fn(str) {
+ case string.ends_with(str, ".git") {
+ True -> string.drop_end(str, 4)
+ False -> str
+ }
+ })
+ use remote <- option.then(remote_cmd)
+ // If remote does not start with http(s), we can't use it.
+ use <- bool.guard(
+ when: bool.negate(string.starts_with(remote, "http")),
+ return: None,
+ )
+
let commit_cmd =
spawn.sync(OptionsToSubprocess(
cmd: [git, "rev-parse", "--verify", "HEAD"],
diff --git a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/web.gleam b/cynthia_websites_mini_server/src/cynthia_websites_mini_server/web.gleam
deleted file mode 100644
index cdb525c..0000000
--- a/cynthia_websites_mini_server/src/cynthia_websites_mini_server/web.gleam
+++ /dev/null
@@ -1,217 +0,0 @@
-import bungibindies/bun
-import bungibindies/bun/bunfile.{type BunFile}
-import bungibindies/bun/http/serve/request.{type Request}
-import bungibindies/bun/http/serve/response
-import cynthia_websites_mini_client/configtype
-import cynthia_websites_mini_client/shared/jsonld
-import cynthia_websites_mini_client/shared/sitemap
-import cynthia_websites_mini_server/config
-import cynthia_websites_mini_server/mutable_model_type
-import cynthia_websites_mini_server/ssrs
-import gleam/dict
-import gleam/javascript/array
-import gleam/javascript/promise.{type Promise}
-import gleam/json
-import gleam/option.{None, Some}
-import gleam/result
-import gleam/uri
-import gleamy_lights/console
-import gleamy_lights/premixed
-import javascript/mutable_reference
-import plinth/node/process
-import simplifile
-
-pub fn handle_request(
- req: Request,
- mutable_model: mutable_model_type.MutableModel,
-) {
- let assert Ok(req_uri) = req |> request.url() |> uri.parse()
- as "Request URI should be valid"
- let path = req_uri.path
-
- // Ensure JSONs are generated if needed
- use _ <- promise.await(
- case mutable_reference.get(mutable_model).cached_jsonld {
- Some(_) -> promise.resolve(Nil)
- // Cache hit, no need to generate
- None -> generate_jsons(mutable_model) |> promise.map(fn(_) { Nil })
- },
- )
-
- let assert Some(dynastatic) = ssrs.ssrs(mutable_model)
- as "These routes should always be valid."
- case path {
- "/" -> {
- console.log(
- premixed.text_ok_green("[ 200 ]\t")
- <> "(GET)\t"
- <> premixed.text_lightblue("/")
- <> " "
- <> premixed.text_cyan(
- "\t(this means client-side will now start loading a web page)",
- ),
- )
- dynastatic
- |> dict.get("/index.html")
- |> result.unwrap(response.new())
- |> promise.resolve()
- }
- "/site.json" -> {
- console.log(
- premixed.text_ok_green("[ 200 ]\t")
- <> "(GET)\t"
- <> premixed.text_lightblue("/site.json")
- <> " "
- <> premixed.text_cyan(
- "\t(this means client-side will now start loading content!)",
- ),
- )
- let model = mutable_reference.get(mutable_model)
- let re = case model.cached_response {
- Some(res_string) -> {
- // Cache hit! Return the cached response string so that it can be used in the response body
- res_string |> promise.resolve
- }
- None -> {
- // If there is no cached response, load the complete data from the config file
- // and encode it as JSON
- use res <- promise.map(generate_jsons(mutable_model))
- let res_string: String = res.0
-
- // Now return the response string promise so that it can be used in the response body
- res_string
- }
- }
- use body <- promise.await(re)
- response.set_body(response.new(), body)
- |> response.set_headers(
- [#("Content-Type", "application/json; charset=utf-8")]
- |> array.from_list(),
- )
- |> response.set_status(200)
- |> promise.resolve
- }
- "/sitemap.xml" -> {
- let model = mutable_reference.get(mutable_model)
- case model.cached_sitemap {
- Some(sitemap_xml) -> {
- console.log(
- premixed.text_ok_green("[ 200 ]\t")
- <> "(GET)\t"
- <> premixed.text_lightblue("/sitemap.xml"),
- )
- response.set_body(response.new(), sitemap_xml)
- |> response.set_headers(
- [#("Content-Type", "application/xml; charset=utf-8")]
- |> array.from_list(),
- )
- |> response.set_status(200)
- |> promise.resolve()
- }
- None -> {
- use _ <- promise.await(generate_jsons(mutable_model))
- let model = mutable_reference.get(mutable_model)
-
- case model.cached_sitemap {
- Some(sitemap_xml) -> {
- console.log(
- premixed.text_ok_green("[ 200 ]\t")
- <> "(GET)\t"
- <> premixed.text_lightblue("/sitemap.xml"),
- )
- response.set_body(response.new(), sitemap_xml)
- |> response.set_headers(
- [#("Content-Type", "application/xml; charset=utf-8")]
- |> array.from_list(),
- )
- |> response.set_status(200)
- |> promise.resolve()
- }
- None -> {
- console.error(
- premixed.text_error_red("[ 404 ] ")
- <> "(GET)\t"
- <> premixed.text_lightblue("/sitemap.xml"),
- )
- dynastatic
- |> dict.get("/404")
- |> result.unwrap(response.new())
- |> promise.resolve()
- }
- }
- }
- }
- }
- "/assets/" <> f -> {
- let filepath = process.cwd() <> "/assets/" <> f
- case simplifile.is_file(filepath) {
- Ok(True) -> {
- console.log(
- premixed.text_ok_green("[ 200 ]\t")
- <> "(GET)\t"
- <> premixed.text_lightblue("/assets/")
- <> premixed.text_cyan(f),
- )
- filepath
- |> bun.file()
- |> answer_bunrequest_with_file()
- }
- _ -> {
- console.error(
- premixed.text_error_red("[ 404 ] ")
- <> "(GET)\t"
- <> premixed.text_lightblue("/assets/")
- <> premixed.text_cyan(f),
- )
- dynastatic
- |> dict.get("/404")
- |> result.unwrap(response.new())
- |> promise.resolve()
- }
- }
- }
- f -> {
- console.error(
- premixed.text_error_red("[ 404 ] ")
- <> "("
- <> req |> request.method
- <> ")\t"
- <> premixed.text_lightblue(f),
- )
- dynastatic
- |> dict.get("/404")
- |> result.unwrap(response.new())
- |> promise.resolve()
- }
- }
-}
-
-@external(javascript, "./request_ffi.ts", "get_request_body")
-pub fn get_request_body(req: Request) -> Promise(BitArray)
-
-@external(javascript, "./request_ffi.ts", "get_request_body_as_text")
-pub fn get_request_body_as_text(req: Request) -> Promise(String)
-
-@external(javascript, "./request_ffi.ts", "answer_bunrequest_with_file")
-pub fn answer_bunrequest_with_file(file: BunFile) -> Promise(response.Response)
-
-fn generate_jsons(
- mutable_model: mutable_model_type.MutableModel,
-) -> Promise(#(String, String, String)) {
- use complete_data <- promise.await(config.load())
- let complete_data_json =
- complete_data |> configtype.encode_complete_data_for_client
- let res_string = complete_data_json |> json.to_string
- let res_jsonld = jsonld.generate_jsonld(complete_data)
- let opt_sitemap = sitemap.generate_sitemap(complete_data)
- // Add all representations to the model cache
- mutable_reference.update(mutable_model, fn(model) {
- mutable_model_type.MutableModelContent(
- ..model,
- cached_response: Some({ res_string }),
- cached_jsonld: Some({ res_jsonld }),
- cached_sitemap: opt_sitemap,
- )
- })
- #(res_string, res_jsonld, option.unwrap(opt_sitemap, "")) |> promise.resolve
-}
diff --git a/generate-ffi b/generate-ffi
index eb9a9d3..b6bdeb6 100755
--- a/generate-ffi
+++ b/generate-ffi
@@ -12,6 +12,8 @@ const config_root = process.env.MISE_CONFIG_ROOT || ".";
// 1. Generate Gleam theme file
const daisyui_themes = [];
+const layouts_doubles = [];
+
let themes_gleam_content = `
// This file is generated in Cynthia Mini's build process.
// Do not edit it manually.
@@ -36,6 +38,10 @@ pub type ThemePrevalence {
pub const themes = [
`;
for (const theme of themeconf) {
+ let layout = theme.layout.trim()
+ if (layout == "cindy") {layout="cindy-simple"}
+ layout = layout.replaceAll("-","_")
+ if (existsSync("./cynthia_websites_mini_client/src/cynthia_websites_mini_client/layouts/" + layout + ".gleam")) {
let prevalence = "ThemeLight";
if (theme.prevalence === "dark") prevalence = "ThemeDark";
const daisy_ui_theme_name = (typeof theme.daisyUI === "string") ? theme.daisyUI : Object.keys(theme.daisyUI)[0];
@@ -43,8 +49,23 @@ for (const theme of themeconf) {
const font_size_mono = theme["font-size-mono"].toString().includes(".") ? theme["font-size-mono"] : `${theme["font-size-mono"]}.0`;
themes_gleam_content += ` SharedCynthiaTheme(\n name: \"${theme.name}\",\n fonts: ${JSON.stringify(theme.fonts)},\n fonts_mono: ${JSON.stringify(theme["fonts-mono"])},\n font_size: ${font_size},\n font_size_mono: ${font_size_mono},\n prevalence: ${prevalence},\n daisy_ui_theme_name: \"${daisy_ui_theme_name}\",\n layout: \"${theme.layout}\"\n ),\n`;
daisyui_themes.push(theme.daisyUI);
+ layouts_doubles.push(layout);
+ }
}
+
+
themes_gleam_content += `]\n`;
+let s = new Set(layouts_doubles);
+const layouts = [...s]
+let layout_imports = "";
+let layout_registrars = `
+/// Registers all layouts as Lustre components
+pub fn register_all() {`
+for (let layout of layouts) {
+ layout_registrars+="\n\tlet assert Ok(_) = "+layout+".register()\n"
+ layout_imports+="\nimport cynthia_websites_mini_client/layouts/"+layout+"\n"
+}
+layout_registrars+="}"
// Write the generated Gleam file, creating the directory if it doesn't exist
const shared_ui_dir = path.join(config_root, "/cynthia_websites_mini_client/src/cynthia_websites_mini_client/ui");
if (!existsSync(shared_ui_dir)) {
@@ -52,7 +73,7 @@ if (!existsSync(shared_ui_dir)) {
}
writeFileSync(
path.join(shared_ui_dir, "themes_generated.gleam"),
- themes_gleam_content
+ layout_imports+themes_gleam_content+layout_registrars
);
// If this is part of the client's build process, we stop here. The themes file is still generated, but the CSS and client bundle will not be created.
diff --git a/mise.toml b/mise.toml
index 5d6f701..825dde4 100644
--- a/mise.toml
+++ b/mise.toml
@@ -1,6 +1,6 @@
[tools]
bun = "latest"
-gleam = "latest"
+gleam = "1.14.0"
# /================================== Tasks ================================\
# | These tasks are in comment-split sections |
@@ -294,4 +294,4 @@ if (found) {
} else {
console.log("No prohibited keywords found.");
}
-"""
\ No newline at end of file
+"""
diff --git a/package.json b/package.json
index 7e214d2..8a68d54 100644
--- a/package.json
+++ b/package.json
@@ -38,5 +38,8 @@
"prepack": "bun run bundle",
"run-in-test": "mise run run-dev",
"test": "mise run test-all"
+ },
+ "dependencies": {
+ "@djot/djot": "^0.3.2"
}
}
diff --git a/themes.json b/themes.json
index b3f642a..24a5210 100644
--- a/themes.json
+++ b/themes.json
@@ -570,15 +570,5 @@
}
},
"layout": "sepia"
- },
- {
- "name": "ownit",
- "prevalence": "light",
- "fonts": [],
- "font-size": 16,
- "fonts-mono": ["Fira Code", "Monaco", "monospace"],
- "font-size-mono": 14,
- "daisyUI": "autumn",
- "layout": "ownit"
}
]