From fa2f463b75eb8b39e027fe272af19dafecd83026 Mon Sep 17 00:00:00 2001 From: Mikko Ahlroth Date: Sun, 7 Apr 2024 16:50:14 +0300 Subject: [PATCH] Add support for pages, refactor rendering code --- assets/css/custom.css | 6 +- src/gloss/builder.gleam | 12 +- src/gloss/compiler.gleam | 68 ++++++++ src/gloss/config.gleam | 35 ++-- src/gloss/defaults.gleam | 30 ++-- src/gloss/models/database.gleam | 12 ++ src/gloss/models/header.gleam | 2 + src/gloss/models/page.gleam | 5 + src/gloss/models/post.gleam | 4 +- src/gloss/parser.gleam | 60 +++++-- src/gloss/parser/common.gleam | 44 ++++++ src/gloss/parser/page.gleam | 35 ++++ src/gloss/parser/post.gleam | 48 +----- src/gloss/paths.gleam | 37 +++-- src/gloss/renderer.gleam | 149 +++++++++--------- src/gloss/rendering/database.gleam | 32 ++-- src/gloss/rendering/templates.gleam | 28 ---- src/gloss/rendering/views.gleam | 27 ++++ .../rendering/{templates => views}/base.gleam | 0 .../rendering/{templates => views}/feed.gleam | 4 +- .../{templates => views}/list_page.gleam | 7 +- .../rendering/{templates => views}/nav.gleam | 2 +- src/gloss/rendering/views/page.gleam | 17 ++ .../{templates => views}/single_post.gleam | 11 +- src/gloss/writer.gleam | 11 +- src/gloss2.gleam | 3 +- 26 files changed, 443 insertions(+), 246 deletions(-) create mode 100644 src/gloss/compiler.gleam create mode 100644 src/gloss/models/header.gleam create mode 100644 src/gloss/models/page.gleam create mode 100644 src/gloss/parser/common.gleam create mode 100644 src/gloss/parser/page.gleam delete mode 100644 src/gloss/rendering/templates.gleam create mode 100644 src/gloss/rendering/views.gleam rename src/gloss/rendering/{templates => views}/base.gleam (100%) rename src/gloss/rendering/{templates => views}/feed.gleam (95%) rename src/gloss/rendering/{templates => views}/list_page.gleam (82%) rename src/gloss/rendering/{templates => views}/nav.gleam (97%) create mode 100644 src/gloss/rendering/views/page.gleam rename src/gloss/rendering/{templates => views}/single_post.gleam (85%) diff --git a/assets/css/custom.css b/assets/css/custom.css index 2324af5..6899700 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -89,12 +89,14 @@ body > footer { gap: 4rem; } -.post h2 { +.post h2, +.page h2 { font-size: 4rem; line-height: 4rem; } -.post header { +.post header, +.page header { margin: 0; padding: 0; } diff --git a/src/gloss/builder.gleam b/src/gloss/builder.gleam index 4bd951b..0de9468 100644 --- a/src/gloss/builder.gleam +++ b/src/gloss/builder.gleam @@ -1,10 +1,10 @@ import gleam/result import gloss/parser import gloss/rendering/database as render_database -import gloss/renderer import gloss/writer import gloss/config.{type Configuration} import gloss/models/database.{type Database} +import gloss/compiler.{type CompileDatabase} pub type BuildError { ParseError(err: parser.ParseError) @@ -16,11 +16,15 @@ pub fn parse(config: Configuration) { |> result.map_error(ParseError) } -pub fn render(db: Database, config: Configuration) { - renderer.render(db, config) +pub fn compile(db: Database, config: Configuration) { + config.compiling.database_compiler(db, config.compiling.item_compiler) } -pub fn write(posts: render_database.Database, config: Configuration) { +pub fn render(db: Database, compiled: CompileDatabase, config: Configuration) { + config.rendering.renderer(db, compiled, config) +} + +pub fn write(posts: render_database.RenderDatabase, config: Configuration) { config.writer(posts, config.paths) |> result.map_error(WriteError) } diff --git a/src/gloss/compiler.gleam b/src/gloss/compiler.gleam new file mode 100644 index 0000000..8722fe1 --- /dev/null +++ b/src/gloss/compiler.gleam @@ -0,0 +1,68 @@ +import gleam/option +import gleam/dict.{type Dict} +import gleam/list +import gloss/utils/ordered_tree +import gloss/models/post.{type Post} +import gloss/models/page.{type Page} +import gloss/models/database.{type Database, type PostID} +import gloss/utils/marked + +pub type PostContent { + PostContent(full: String, short: option.Option(String)) +} + +pub type CompiledPost { + CompiledPost(orig: Post, content: PostContent) +} + +pub type CompiledPage { + CompiledPage(orig: Page, content: String) +} + +pub type Compiler = + fn(String, Database) -> String + +pub type CompileDatabase { + CompileDatabase(posts: Dict(PostID, CompiledPost), pages: List(CompiledPage)) +} + +pub fn default_compiler(content: String, _db: Database) { + marked.default_parse(content) +} + +pub fn compile(db: Database, compiler: Compiler) { + CompileDatabase( + posts: compile_posts(db, compiler), + pages: compile_pages(db, compiler), + ) +} + +pub fn compile_posts( + db: Database, + compiler: Compiler, +) -> Dict(PostID, CompiledPost) { + let posts = + database.get_posts_with_ids(db, ordered_tree.Asc) + |> list.map(fn(post_with_id) { + let content = compiler(post_with_id.post.content, db) + let short_content = + option.map(post_with_id.post.short_content, compiler(_, db)) + #( + post_with_id.id, + CompiledPost( + post_with_id.post, + content: PostContent(full: content, short: short_content), + ), + ) + }) + + dict.from_list(posts) +} + +pub fn compile_pages(db: Database, compiler: Compiler) { + database.pages(db) + |> list.map(fn(page) { + let content = compiler(page.content, db) + CompiledPage(orig: page, content: content) + }) +} diff --git a/src/gloss/config.gleam b/src/gloss/config.gleam index 1a47112..6e33fc7 100644 --- a/src/gloss/config.gleam +++ b/src/gloss/config.gleam @@ -1,28 +1,38 @@ import gleam/option import gloss/paths.{type PathConfiguration} -import gloss/rendering/templates.{ - type BaseRenderer, type FeedRenderer, type ListPageRenderer, - type PostContentRenderer, type SinglePostRenderer, +import gloss/rendering/views.{ + type BaseView, type FeedView, type ListPageView, type PageView, + type SinglePostView, } +import gloss/compiler.{type CompileDatabase, type Compiler} import gloss/parser.{type Parser} import gloss/writer.{type Writer} import gloss/models/database.{type Database} +import gloss/rendering/database.{type RenderDatabase} as _ -pub type Templates { - Templates( - base: fn(Database, Configuration) -> BaseRenderer, - single_post_full: fn(Database, Configuration) -> SinglePostRenderer, - single_post_list: fn(Database, Configuration) -> SinglePostRenderer, - list_page: fn(Database, Configuration) -> ListPageRenderer, - feed: fn(Database, Configuration) -> FeedRenderer, +pub type Views { + Views( + base: fn(Database, Configuration) -> BaseView, + single_post_full: fn(Database, Configuration) -> SinglePostView, + single_post_list: fn(Database, Configuration) -> SinglePostView, + page: fn(Database, Configuration) -> PageView, + list_page: fn(Database, Configuration) -> ListPageView, + feed: fn(Database, Configuration) -> FeedView, + ) +} + +pub type Compiling { + Compiling( + database_compiler: fn(Database, Compiler) -> CompileDatabase, + item_compiler: Compiler, ) } pub type Rendering { Rendering( - templates: Templates, + renderer: fn(Database, CompileDatabase, Configuration) -> RenderDatabase, + views: Views, copyright: String, - content_renderer: PostContentRenderer, posts_per_page: Int, posts_in_feed: Int, ) @@ -37,6 +47,7 @@ pub type Configuration { blog_name: String, blog_url: String, author: Author, + compiling: Compiling, rendering: Rendering, paths: PathConfiguration, parser: Parser, diff --git a/src/gloss/defaults.gleam b/src/gloss/defaults.gleam index 82f837e..c457bbc 100644 --- a/src/gloss/defaults.gleam +++ b/src/gloss/defaults.gleam @@ -1,16 +1,20 @@ -import gloss/config.{type Configuration, Configuration, Rendering} -import gloss/rendering/templates/single_post -import gloss/rendering/templates/base -import gloss/rendering/templates/list_page -import gloss/rendering/templates/feed +import gloss/config.{type Configuration, Compiling, Configuration, Rendering} +import gloss/rendering/views/single_post +import gloss/rendering/views/base +import gloss/rendering/views/list_page +import gloss/rendering/views/page +import gloss/rendering/views/feed +import gloss/renderer import gloss/paths import gloss/parser import gloss/writer +import gloss/compiler -const default_templates = config.Templates( +const default_views = config.Views( base: base.generate, single_post_full: single_post.full_view, single_post_list: single_post.list_view, + page: page.generate, list_page: list_page.generate, feed: feed.generate, ) @@ -25,18 +29,18 @@ pub fn default_config( blog_name: blog_name, blog_url: blog_url, author: author, + compiling: Compiling( + database_compiler: compiler.compile, + item_compiler: compiler.default_compiler, + ), rendering: Rendering( - templates: default_templates, + renderer: renderer.render, + views: default_views, copyright: copyright, - content_renderer: single_post.content_renderer, posts_per_page: 10, posts_in_feed: 20, ), - paths: paths.conf( - paths.default_index, - paths.default_single_post, - paths.default_tag, - ), + paths: paths.defaults, parser: parser.default_parse, writer: writer.write, ) diff --git a/src/gloss/models/database.gleam b/src/gloss/models/database.gleam index 474d897..f51a9b1 100644 --- a/src/gloss/models/database.gleam +++ b/src/gloss/models/database.gleam @@ -3,6 +3,7 @@ import gleam/list import gleam/option.{None, Some} import gleam/order.{type Order} import gloss/models/post.{type Post, type Tag} +import gloss/models/page.{type Page} import gloss/utils/date.{type Month} import gloss/utils/ordered_tree.{type OrderedTree} import gloss/utils/uniqid.{type Generator, type UniqID} @@ -26,6 +27,7 @@ pub type YearPosts = pub opaque type Database { Database( posts: OrderedTree(PostWithID), + pages: List(Page), tags: TagPosts, years: YearPosts, posts_by_id: Dict(PostID, Post), @@ -36,6 +38,7 @@ pub opaque type Database { pub fn new() -> Database { Database( posts: new_tree(), + pages: [], tags: dict.new(), years: dict.new(), posts_by_id: dict.new(), @@ -85,6 +88,7 @@ pub fn add_post(db: Database, post: Post) -> Database { }) Database( + ..db, id_generator: id_generator, posts: posts, tags: tags, @@ -93,6 +97,10 @@ pub fn add_post(db: Database, post: Post) -> Database { ) } +pub fn add_page(db: Database, page: Page) -> Database { + Database(..db, pages: [page, ..db.pages]) +} + pub fn tags(db: Database) { db.tags } @@ -108,6 +116,10 @@ pub fn get_posts_with_ids( ordered_tree.to_list(db.posts, order) } +pub fn pages(db: Database) { + db.pages +} + fn new_tree() -> OrderedTree(PostWithID) { ordered_tree.new(comparator) } diff --git a/src/gloss/models/header.gleam b/src/gloss/models/header.gleam new file mode 100644 index 0000000..c42eca2 --- /dev/null +++ b/src/gloss/models/header.gleam @@ -0,0 +1,2 @@ +pub type Header = + #(String, String) diff --git a/src/gloss/models/page.gleam b/src/gloss/models/page.gleam new file mode 100644 index 0000000..39d03f3 --- /dev/null +++ b/src/gloss/models/page.gleam @@ -0,0 +1,5 @@ +import gloss/models/header.{type Header} + +pub type Page { + Page(title: String, slug: String, headers: List(Header), content: String) +} diff --git a/src/gloss/models/post.gleam b/src/gloss/models/post.gleam index d0938ea..200f217 100644 --- a/src/gloss/models/post.gleam +++ b/src/gloss/models/post.gleam @@ -3,15 +3,13 @@ import gleam/order.{type Order, Eq, Gt, Lt} import gloss/utils/date.{type Date} import gloss/utils/time.{type Time, Time} import gloss/utils/luxon.{type DateTime} +import gloss/models/header.{type Header} pub type PostedAt { JustDate(Date) DateTime(date: Date, time: Time, tz: String, luxon: DateTime) } -pub type Header = - #(String, String) - pub type Tag = String diff --git a/src/gloss/parser.gleam b/src/gloss/parser.gleam index 184a7a8..3611e72 100644 --- a/src/gloss/parser.gleam +++ b/src/gloss/parser.gleam @@ -3,7 +3,9 @@ import gleam/list import gleam/regex import gloss/utils/fs import gloss/models/database.{type Database} +import gloss/parser/common import gloss/parser/post +import gloss/parser/page const default_data_path = "./data" @@ -12,15 +14,15 @@ pub type Parser = pub type ParseError { FileError(path: String, err: fs.FSError) - PostParseError(filename: String, err: post.ParseError) + PostParseError(filename: String, err: common.ParseError) } pub fn default_parse() -> Result(Database, ParseError) { - let db = database.new() - parse_posts(post_path(), db) + parse_posts(database.new(), post_path()) + |> result.try(parse_pages(_, page_path())) } -pub fn parse_posts(path: String, db: Database) -> Result(Database, ParseError) { +pub fn parse_posts(db: Database, path: String) -> Result(Database, ParseError) { use filenames <- result.try( fs.readdir(path) |> result.map_error(fn(err) { FileError(path, err) }), @@ -35,22 +37,50 @@ pub fn parse_posts(path: String, db: Database) -> Result(Database, ParseError) { let filenames = list.filter(filenames, fn(file) { regex.check(filename_regex, file) }) - use posts <- result.try(result.all(list.map( - filenames, - fn(file) { - use contents <- result.try( - fs.read_file(path <> "/" <> file) - |> result.map_error(fn(err) { FileError(file, err) }), - ) + use posts <- result.try( + result.all( + list.map(filenames, fn(file) { + use contents <- result.try( + fs.read_file(path <> "/" <> file) + |> result.map_error(fn(err) { FileError(file, err) }), + ) - post.parse(file, contents) - |> result.map_error(fn(err) { PostParseError(file, err) }) - }, - ))) + post.parse(file, contents) + |> result.map_error(fn(err) { PostParseError(file, err) }) + }), + ), + ) Ok(list.fold(posts, db, database.add_post)) } +pub fn parse_pages(db: Database, path: String) -> Result(Database, ParseError) { + use filenames <- result.try( + fs.readdir(path) + |> result.map_error(fn(err) { FileError(path, err) }), + ) + + use pages <- result.try( + result.all( + list.map(filenames, fn(file) { + use contents <- result.try( + fs.read_file(path <> "/" <> file) + |> result.map_error(fn(err) { FileError(file, err) }), + ) + + page.parse(file, contents) + |> result.map_error(fn(err) { PostParseError(file, err) }) + }), + ), + ) + + Ok(list.fold(pages, db, database.add_page)) +} + fn post_path() { default_data_path <> "/posts" } + +fn page_path() { + default_data_path <> "/pages" +} diff --git a/src/gloss/parser/common.gleam b/src/gloss/parser/common.gleam new file mode 100644 index 0000000..7196761 --- /dev/null +++ b/src/gloss/parser/common.gleam @@ -0,0 +1,44 @@ +import gleam/result +import gleam/string +import gleam/list +import gleam/bool +import gloss/models/header.{type Header} + +const header_separator = ":" + +pub type ParseError { + EmptyFile + HeaderMissing + MalformedFilename + YearNotInt + MonthNotInt + DayNotInt + InvalidDate + MalformedHeader(header: String) +} + +pub fn try(value: Result(a, b), error: c, if_ok: fn(a) -> Result(d, c)) { + result.try(result.replace_error(value, error), if_ok) +} + +pub fn parse_headers(headers: List(String)) -> Result(List(Header), ParseError) { + headers + |> list.map(parse_header) + |> result.all() +} + +fn parse_header(header: String) -> Result(Header, ParseError) { + let header_parts = string.split(header, header_separator) + let parts_amount = list.length(header_parts) + + use <- bool.guard(parts_amount < 2, Error(MalformedHeader(header))) + + let assert Ok(name) = list.first(header_parts) + let assert Ok(rest) = list.rest(header_parts) + let value = string.join(rest, header_separator) + + let name = string.trim(name) + let value = string.trim(value) + + Ok(#(name, value)) +} diff --git a/src/gloss/parser/page.gleam b/src/gloss/parser/page.gleam new file mode 100644 index 0000000..6517106 --- /dev/null +++ b/src/gloss/parser/page.gleam @@ -0,0 +1,35 @@ +import gleam/string +import gleam/result +import gleam/list +import gloss/models/page.{type Page, Page} +import gloss/parser/common.{type ParseError, EmptyFile, HeaderMissing, try} + +const filename_postfix = ".md" + +pub fn parse(filename: String, contents: String) -> Result(Page, ParseError) { + let lines = string.split(contents, "\n") + + use title <- try(list.first(lines), EmptyFile) + use rest <- try(list.rest(lines), HeaderMissing) + let slug = parse_slug(filename) + + let #(headers, body) = + list.split_while(rest, fn(line) { !string.is_empty(line) }) + + use headers <- result.try(common.parse_headers(headers)) + let body = string.join(body, "\n") + + Ok(Page(title: title, slug: slug, headers: headers, content: body)) +} + +fn parse_slug(filename: String) -> String { + case string.ends_with(filename, filename_postfix) { + True -> + string.slice( + filename, + 0, + string.length(filename) - string.length(filename_postfix), + ) + False -> filename + } +} diff --git a/src/gloss/parser/post.gleam b/src/gloss/parser/post.gleam index 40c6378..a84728d 100644 --- a/src/gloss/parser/post.gleam +++ b/src/gloss/parser/post.gleam @@ -5,11 +5,16 @@ import gleam/option import gleam/int import gleam/bool import gleam/regex -import gloss/models/post.{type Header, type Post, type PostedAt, type Tag, Post} +import gloss/models/header.{type Header} +import gloss/models/post.{type Post, type PostedAt, type Tag, Post} import gloss/utils/date.{Date} import gloss/utils/time.{type Time, Time} import gloss/utils/ints/day import gloss/utils/luxon +import gloss/parser/common.{ + type ParseError, DayNotInt, EmptyFile, HeaderMissing, InvalidDate, + MalformedFilename, MalformedHeader, MonthNotInt, YearNotInt, try, +} pub const filename_regex = "^\\d{4}-\\d\\d-\\d\\d-.*\\.md$" @@ -19,25 +24,12 @@ const filename_postfix = ".md" const tag_separator = "," -const header_separator = ":" - const split_re = "" type FilenameMeta { FilenameMeta(date: PostedAt, order: Int, slug: String) } -pub type ParseError { - EmptyFile - HeaderMissing - MalformedFilename - YearNotInt - MonthNotInt - DayNotInt - InvalidDate - MalformedHeader(header: String) -} - pub fn parse(filename: String, contents: String) -> Result(Post, ParseError) { let lines = string.split(contents, "\n") @@ -62,7 +54,7 @@ pub fn parse(filename: String, contents: String) -> Result(Post, ParseError) { list.split_while(rest, fn(line) { !string.is_empty(line) }) let tags = parse_tags(tags) - use headers <- result.try(parse_headers(headers)) + use headers <- result.try(common.parse_headers(headers)) let body = string.join(body, "\n") let short_content = parse_short_content(body) use time <- result.try(parse_time(headers)) @@ -147,28 +139,6 @@ fn parse_tags(tags: String) -> List(Tag) { |> list.map(string.trim) } -fn parse_headers(headers: List(String)) -> Result(List(Header), ParseError) { - headers - |> list.map(parse_header) - |> result.all() -} - -fn parse_header(header: String) -> Result(Header, ParseError) { - let header_parts = string.split(header, header_separator) - let parts_amount = list.length(header_parts) - - use <- bool.guard(parts_amount < 2, Error(MalformedHeader(header))) - - let assert Ok(name) = list.first(header_parts) - let assert Ok(rest) = list.rest(header_parts) - let value = string.join(rest, header_separator) - - let name = string.trim(name) - let value = string.trim(value) - - Ok(#(name, value)) -} - fn parse_short_content(body: String) -> option.Option(String) { let assert Ok(re) = regex.compile( @@ -199,7 +169,3 @@ fn parse_time( } } } - -fn try(value: Result(a, b), error: c, if_ok: fn(a) -> Result(d, c)) { - result.try(result.replace_error(value, error), if_ok) -} diff --git a/src/gloss/paths.gleam b/src/gloss/paths.gleam index da6a983..dc9548e 100644 --- a/src/gloss/paths.gleam +++ b/src/gloss/paths.gleam @@ -1,6 +1,7 @@ import gleam/int import gleam/string import gloss/models/post.{type Post} +import gloss/models/page.{type Page} import gloss/paths/post as post_paths import gloss/utils/date.{type Month} @@ -10,6 +11,7 @@ pub type PathConfiguration { PathConfiguration( index: String, single_post: fn(Post) -> String, + page: fn(Page) -> String, tag: fn(String) -> String, year: fn(Int) -> String, month: fn(Int, Month) -> String, @@ -18,17 +20,16 @@ pub type PathConfiguration { ) } -pub fn conf(index, single_post, tag) -> PathConfiguration { - PathConfiguration( - index, - single_post, - tag, - year_archive, - month_archive, - list_page, - html, - ) -} +pub const defaults = PathConfiguration( + index: default_index, + single_post: default_single_post, + page: default_page, + tag: default_tag, + year: default_year_archive, + month: default_month_archive, + list_page: default_list_page, + html: default_html, +) pub fn default_single_post(post: Post) { let post_path = post_paths.post_to_path(post) @@ -36,16 +37,20 @@ pub fn default_single_post(post: Post) { "/" <> post_path.date_path <> "/" <> post_path.slug } +pub fn default_page(page: Page) { + "/" <> page.slug +} + pub fn default_tag(tag: String) { "/tag/" <> tag } -pub fn year_archive(year: Int) { +pub fn default_year_archive(year: Int) { "/archive" <> "/" <> int.to_string(year) } -pub fn month_archive(year: Int, month: Month) { - year_archive(year) +pub fn default_month_archive(year: Int, month: Month) { + default_year_archive(year) <> "/" <> string.pad_left(int.to_string(date.month_to_int(month)), 2, "0") } @@ -53,7 +58,7 @@ pub fn month_archive(year: Int, month: Month) { /// Get the given list path with a page number. /// /// The first page does not get any appended page number. -pub fn list_page(path: String, page: Int) { +pub fn default_list_page(path: String, page: Int) { case page { 1 -> path other -> path <> "/" <> int.to_string(other) @@ -61,6 +66,6 @@ pub fn list_page(path: String, page: Int) { } /// Get path with the .html extension -pub fn html(path: String) { +pub fn default_html(path: String) { path <> ".html" } diff --git a/src/gloss/renderer.gleam b/src/gloss/renderer.gleam index 583729d..f3d4a04 100644 --- a/src/gloss/renderer.gleam +++ b/src/gloss/renderer.gleam @@ -3,53 +3,60 @@ import gleam/dict.{type Dict} import gleam/result import gleam/int import lustre/element.{type Element} -import gloss/rendering/templates.{ - type BaseRenderer, type FeedRenderer, type ListPageRenderer, - type PostContentRenderer, type SinglePostRenderer, ListInfo, +import gloss/rendering/views.{ + type BaseView, type FeedView, type ListPageView, type PageView, + type SinglePostView, ListInfo, } import gloss/rendering/database.{ - type Database as RenderDatabase, type RenderedPost, type RenderedSinglePost, - ListPage, RenderedPost, RenderedSinglePost, -} as render_database + type RenderDatabase, type RenderedSinglePost, ListPage, RenderDatabase, + RenderedPage, RenderedSinglePost, +} as _ +import gloss/compiler.{ + type CompileDatabase, type CompiledPage, type CompiledPost, +} import gloss/models/database.{type Database, type PostID, type PostWithID} import gloss/utils/ordered_tree import gloss/config.{type Configuration} import gloss/utils/date -pub type Renderers { - Renderers( - base: BaseRenderer, - single_post_full: SinglePostRenderer, - list_page: ListPageRenderer, - feed: FeedRenderer, +pub type Views { + Views( + base: BaseView, + single_post_full: SinglePostView, + page: PageView, + list_page: ListPageView, + feed: FeedView, ) } -pub fn render(db: Database, config: Configuration) -> RenderDatabase { - let renderers = - Renderers( - base: config.rendering.templates.base(db, config), - single_post_full: config.rendering.templates.single_post_full(db, config), - list_page: config.rendering.templates.list_page(db, config), - feed: config.rendering.templates.feed(db, config), +pub fn render( + db: Database, + compiled: CompileDatabase, + config: Configuration, +) -> RenderDatabase { + let views = + Views( + base: config.rendering.views.base(db, config), + single_post_full: config.rendering.views.single_post_full(db, config), + page: config.rendering.views.page(db, config), + list_page: config.rendering.views.list_page(db, config), + feed: config.rendering.views.feed(db, config), ) let all_posts = database.get_posts_with_ids(db, ordered_tree.Desc) - let post_contents = - render_post_contents(all_posts, config.rendering.content_renderer) - let posts = render_posts(db, post_contents, renderers) - let index_pages = - render_index_pages(config, all_posts, post_contents, renderers) - let tag_pages = render_tag_pages(config, db, post_contents, renderers) - let year_pages = render_year_pages(config, db, post_contents, renderers) - let month_pages = render_month_pages(config, db, post_contents, renderers) - let feed = render_feed(all_posts, post_contents, renderers) + let posts = render_posts(db, compiled.posts, views) + let pages = render_pages(db, compiled.pages, views) + let index_pages = render_index_pages(config, all_posts, compiled.posts, views) + let tag_pages = render_tag_pages(config, db, compiled.posts, views) + let year_pages = render_year_pages(config, db, compiled.posts, views) + let month_pages = render_month_pages(config, db, compiled.posts, views) + let feed = render_feed(all_posts, compiled.posts, views) - render_database.Database( + RenderDatabase( orig: db, - posts: post_contents, single_posts: posts, index: [], + pages: pages, index_pages: index_pages, tag_pages: tag_pages, year_pages: year_pages, @@ -58,24 +65,10 @@ pub fn render(db: Database, config: Configuration) -> RenderDatabase { ) } -pub fn render_post_contents( - all_posts: List(database.PostWithID), - renderer: PostContentRenderer, -) -> Dict(PostID, RenderedPost) { - let posts = - all_posts - |> list.map(fn(post_with_id) { - let content = renderer(post_with_id.post) - #(post_with_id.id, RenderedPost(post_with_id.post, content: content)) - }) - - dict.from_list(posts) -} - pub fn render_posts( db: Database, - post_contents: Dict(PostID, RenderedPost), - renderers: Renderers, + post_contents: Dict(PostID, CompiledPost), + views: Views, ) -> List(RenderedSinglePost) { let all_posts = database.get_posts_with_ids(db, ordered_tree.Desc) @@ -83,25 +76,33 @@ pub fn render_posts( |> list.map(fn(post_with_id) { let assert Ok(content) = dict.get(post_contents, post_with_id.id) let rendered = - renderers.base( - renderers.single_post_full(content), - post_with_id.post.title, - ) + views.base(views.single_post_full(content), post_with_id.post.title) RenderedSinglePost(post_with_id.post, rendered) }) } +pub fn render_pages( + _db: Database, + compiled_pages: List(CompiledPage), + views: Views, +) { + list.map(compiled_pages, fn(page) { + let rendered = views.base(views.page(page), page.orig.title) + RenderedPage(page.orig, rendered) + }) +} + fn render_index_pages( config: Configuration, posts: List(database.PostWithID), - posts_with_contents: Dict(PostID, RenderedPost), - renderers: Renderers, + compiled_posts: Dict(PostID, CompiledPost), + views: Views, ) { pageify_posts( posts, config, - posts_with_contents, - renderers, + compiled_posts, + views, "", config.paths.index, element.none(), @@ -111,8 +112,8 @@ fn render_index_pages( fn render_tag_pages( config: Configuration, db: Database, - posts_with_contents: Dict(PostID, RenderedPost), - renderers: Renderers, + compiled_posts: Dict(PostID, CompiledPost), + views: Views, ) { let tags = database.tags(db) @@ -121,8 +122,8 @@ fn render_tag_pages( pageify_posts( posts, config, - posts_with_contents, - renderers, + compiled_posts, + views, tag, config.paths.tag(tag), element.none(), @@ -133,8 +134,8 @@ fn render_tag_pages( fn render_year_pages( config: Configuration, db: Database, - posts_with_contents: Dict(PostID, RenderedPost), - renderers: Renderers, + compiled_posts: Dict(PostID, CompiledPost), + views: Views, ) { let years = database.years(db) @@ -153,8 +154,8 @@ fn render_year_pages( pageify_posts( posts, config, - posts_with_contents, - renderers, + compiled_posts, + views, "Archives for " <> int.to_string(year), config.paths.year(year), element.none(), @@ -165,8 +166,8 @@ fn render_year_pages( fn render_month_pages( config: Configuration, db: Database, - posts_with_contents: Dict(PostID, RenderedPost), - renderers: Renderers, + compiled_posts: Dict(PostID, CompiledPost), + views: Views, ) { let years = database.years(db) @@ -179,8 +180,8 @@ fn render_month_pages( pageify_posts( posts, config, - posts_with_contents, - renderers, + compiled_posts, + views, "Archives for " <> date.month_to_string(month) <> " " @@ -195,22 +196,22 @@ fn render_month_pages( fn render_feed( posts: List(database.PostWithID), - posts_with_contents: Dict(PostID, RenderedPost), - renderers: Renderers, + compiled_posts: Dict(PostID, CompiledPost), + views: Views, ) { let posts = list.map(posts, fn(post) { - let assert Ok(rendered) = dict.get(posts_with_contents, post.id) + let assert Ok(rendered) = dict.get(compiled_posts, post.id) rendered }) - renderers.feed(posts) + views.feed(posts) } fn pageify_posts( posts: List(PostWithID), config: Configuration, - posts_with_contents: Dict(PostID, RenderedPost), - renderers: Renderers, + compiled_posts: Dict(PostID, CompiledPost), + views: Views, title_prefix: String, root_path: String, extra_header: Element(Nil), @@ -226,13 +227,13 @@ fn pageify_posts( current_page: page, total_pages: total_pages, posts: list.map(page_posts, fn(post_with_id) { - let assert Ok(post) = dict.get(posts_with_contents, post_with_id.id) + let assert Ok(post) = dict.get(compiled_posts, post_with_id.id) post }), extra_header: extra_header, ) - let page_content = renderers.base(renderers.list_page(info), title_prefix) + let page_content = views.base(views.list_page(info), title_prefix) ListPage(page: page, content: page_content) }) } diff --git a/src/gloss/rendering/database.gleam b/src/gloss/rendering/database.gleam index ff8fdec..e3d8c70 100644 --- a/src/gloss/rendering/database.gleam +++ b/src/gloss/rendering/database.gleam @@ -1,39 +1,35 @@ -import gleam/option.{type Option} import gleam/dict.{type Dict} import lustre/element.{type Element} -import gloss/models/database.{type Database as OrigDatabase, type PostID} as _ +import gloss/models/database.{type Database, type PostID} as _ import gloss/models/post.{type Post} +import gloss/models/page.{type Page} import gloss/utils/date.{type Month} pub type PostList = List(PostID) -pub type RenderedContent { - RenderedContent(full: String, short: Option(String)) -} - -pub type RenderedPost { - RenderedPost(orig: Post, content: RenderedContent) -} - pub type RenderedSinglePost { RenderedSinglePost(orig: Post, content: Element(Nil)) } pub type RenderedPage { + RenderedPage(page: Page, content: Element(Nil)) +} + +pub type RenderedListPage { ListPage(page: Int, content: Element(Nil)) } -pub type Database { - Database( - orig: OrigDatabase, - posts: Dict(PostID, RenderedPost), +pub type RenderDatabase { + RenderDatabase( + orig: Database, single_posts: List(RenderedSinglePost), index: PostList, - index_pages: List(RenderedPage), - tag_pages: Dict(String, List(RenderedPage)), - year_pages: Dict(Int, List(RenderedPage)), - month_pages: Dict(#(Int, Month), List(RenderedPage)), + pages: List(RenderedPage), + index_pages: List(RenderedListPage), + tag_pages: Dict(String, List(RenderedListPage)), + year_pages: Dict(Int, List(RenderedListPage)), + month_pages: Dict(#(Int, Month), List(RenderedListPage)), feed: Element(Nil), ) } diff --git a/src/gloss/rendering/templates.gleam b/src/gloss/rendering/templates.gleam deleted file mode 100644 index 6995018..0000000 --- a/src/gloss/rendering/templates.gleam +++ /dev/null @@ -1,28 +0,0 @@ -import lustre/element.{type Element} -import gloss/models/post.{type Post} -import gloss/rendering/database.{type RenderedContent, type RenderedPost} as _ - -pub type PostContentRenderer = - fn(Post) -> RenderedContent - -pub type BaseRenderer = - fn(Element(Nil), String) -> Element(Nil) - -pub type SinglePostRenderer = - fn(RenderedPost) -> Element(Nil) - -pub type FeedRenderer = - fn(List(RenderedPost)) -> Element(Nil) - -pub type ListInfo { - ListInfo( - root_path: String, - current_page: Int, - total_pages: Int, - posts: List(RenderedPost), - extra_header: Element(Nil), - ) -} - -pub type ListPageRenderer = - fn(ListInfo) -> Element(Nil) diff --git a/src/gloss/rendering/views.gleam b/src/gloss/rendering/views.gleam new file mode 100644 index 0000000..fe38409 --- /dev/null +++ b/src/gloss/rendering/views.gleam @@ -0,0 +1,27 @@ +import lustre/element.{type Element} +import gloss/compiler.{type CompiledPage, type CompiledPost} + +pub type BaseView = + fn(Element(Nil), String) -> Element(Nil) + +pub type SinglePostView = + fn(CompiledPost) -> Element(Nil) + +pub type PageView = + fn(CompiledPage) -> Element(Nil) + +pub type FeedView = + fn(List(CompiledPost)) -> Element(Nil) + +pub type ListInfo { + ListInfo( + root_path: String, + current_page: Int, + total_pages: Int, + posts: List(CompiledPost), + extra_header: Element(Nil), + ) +} + +pub type ListPageView = + fn(ListInfo) -> Element(Nil) diff --git a/src/gloss/rendering/templates/base.gleam b/src/gloss/rendering/views/base.gleam similarity index 100% rename from src/gloss/rendering/templates/base.gleam rename to src/gloss/rendering/views/base.gleam diff --git a/src/gloss/rendering/templates/feed.gleam b/src/gloss/rendering/views/feed.gleam similarity index 95% rename from src/gloss/rendering/templates/feed.gleam rename to src/gloss/rendering/views/feed.gleam index df46464..a54f4d0 100644 --- a/src/gloss/rendering/templates/feed.gleam +++ b/src/gloss/rendering/views/feed.gleam @@ -9,12 +9,12 @@ import lustre/ssg/atom.{ import gloss/models/database.{type Database} import gloss/models/post import gloss/config.{type Configuration} -import gloss/rendering/database.{type RenderedPost} as _ +import gloss/compiler.{type CompiledPost} import gloss/utils/luxon import gloss/utils/date pub fn generate(_db: Database, config: Configuration) { - fn(posts: List(RenderedPost)) { + fn(posts: List(CompiledPost)) { let now = luxon.utc_now() let #(posts, _) = list.split(posts, config.rendering.posts_in_feed) diff --git a/src/gloss/rendering/templates/list_page.gleam b/src/gloss/rendering/views/list_page.gleam similarity index 82% rename from src/gloss/rendering/templates/list_page.gleam rename to src/gloss/rendering/views/list_page.gleam index a026e08..80fd879 100644 --- a/src/gloss/rendering/templates/list_page.gleam +++ b/src/gloss/rendering/views/list_page.gleam @@ -3,13 +3,12 @@ import lustre/element import lustre/element/html.{footer, nav} import lustre/attribute.{attribute, class} import gloss/models/database.{type Database} -import gloss/rendering/templates.{type ListInfo} -import gloss/rendering/templates/nav +import gloss/rendering/views.{type ListInfo} +import gloss/rendering/views/nav import gloss/config.{type Configuration} pub fn generate(db: Database, config: Configuration) { - let single_post_renderer = - config.rendering.templates.single_post_list(db, config) + let single_post_renderer = config.rendering.views.single_post_list(db, config) fn(info: ListInfo) { let none = element.none() diff --git a/src/gloss/rendering/templates/nav.gleam b/src/gloss/rendering/views/nav.gleam similarity index 97% rename from src/gloss/rendering/templates/nav.gleam rename to src/gloss/rendering/views/nav.gleam index 2ef7a9d..3ddd654 100644 --- a/src/gloss/rendering/templates/nav.gleam +++ b/src/gloss/rendering/views/nav.gleam @@ -3,7 +3,7 @@ import gleam/int import lustre/element/html.{a, li, span, ul} import lustre/element.{text} import lustre/attribute.{attribute, href} -import gloss/rendering/templates.{type ListInfo} +import gloss/rendering/views.{type ListInfo} import gloss/config.{type Configuration} pub fn view(list_info: ListInfo, root_path: String, config: Configuration) { diff --git a/src/gloss/rendering/views/page.gleam b/src/gloss/rendering/views/page.gleam new file mode 100644 index 0000000..8749f22 --- /dev/null +++ b/src/gloss/rendering/views/page.gleam @@ -0,0 +1,17 @@ +import lustre/element/html.{article, div, h2, header} +import lustre/element.{text} +import lustre/attribute.{attribute, class} +import gloss/models/database.{type Database} +import gloss/compiler.{type CompiledPage} +import gloss/config.{type Configuration} + +pub fn generate(db: Database, config: Configuration) { + view(_, db, config) +} + +fn view(page: CompiledPage, _db: Database, _config: Configuration) { + article([class("page")], [ + header([], [h2([], [text(page.orig.title)])]), + div([attribute("dangerous-unescaped-html", page.content)], []), + ]) +} diff --git a/src/gloss/rendering/templates/single_post.gleam b/src/gloss/rendering/views/single_post.gleam similarity index 85% rename from src/gloss/rendering/templates/single_post.gleam rename to src/gloss/rendering/views/single_post.gleam index 33bdbcd..dd44162 100644 --- a/src/gloss/rendering/templates/single_post.gleam +++ b/src/gloss/rendering/views/single_post.gleam @@ -7,9 +7,8 @@ import lustre/element.{type Element, text} import lustre/attribute.{attribute, class, href} import gloss/models/database.{type Database} import gloss/models/post.{type Post} -import gloss/rendering/database.{type RenderedPost, RenderedContent} as _ +import gloss/compiler.{type CompiledPost} import gloss/config.{type Configuration} -import gloss/utils/marked import gloss/utils/date import gloss/utils/time import gloss/utils/luxon @@ -22,13 +21,7 @@ pub fn list_view(db: Database, config: Configuration) { view(_, False, db, config) } -pub fn content_renderer(post: Post) { - let full = marked.default_parse(post.content) - let short = option.map(post.short_content, marked.default_parse) - RenderedContent(full: full, short: short) -} - -fn view(post: RenderedPost, is_full: Bool, _db: Database, config: Configuration) { +fn view(post: CompiledPost, is_full: Bool, _db: Database, config: Configuration) { let post_url = config.paths.html(config.paths.single_post(post.orig)) let content = case post.content.short, is_full { diff --git a/src/gloss/writer.gleam b/src/gloss/writer.gleam index 23adad4..edc60be 100644 --- a/src/gloss/writer.gleam +++ b/src/gloss/writer.gleam @@ -4,7 +4,7 @@ import gleam/result import lustre/ssg import lustre/ssg/xml import lustre/element -import gloss/rendering/database.{type Database} as _ +import gloss/rendering/database.{type RenderDatabase} as _ import gloss/models/post.{type Post} import gloss/paths/post.{type PostPath} as _ import gloss/paths.{type PathConfiguration} @@ -15,13 +15,13 @@ pub type PostPathGenerator = fn(Post) -> PostPath pub type Writer = - fn(Database, PathConfiguration) -> Result(Nil, WriteError) + fn(RenderDatabase, PathConfiguration) -> Result(Nil, WriteError) pub type WriteError { WriteError(err: ssg.BuildError) } -pub fn write(db: Database, path_conf: PathConfiguration) { +pub fn write(db: RenderDatabase, path_conf: PathConfiguration) { let site = ssg.new(default_output) |> ssg.add_static_dir("./assets") @@ -81,6 +81,11 @@ pub fn write(db: Database, path_conf: PathConfiguration) { ssg.add_static_route(acc, path, post) }) + let site = + list.fold(db.pages, site, fn(acc, page) { + ssg.add_static_route(acc, path_conf.page(page.page), page.content) + }) + site |> ssg.build() |> result.map_error(WriteError) diff --git a/src/gloss2.gleam b/src/gloss2.gleam index eb9f950..21bbe51 100644 --- a/src/gloss2.gleam +++ b/src/gloss2.gleam @@ -22,6 +22,7 @@ pub fn main() { pub fn build(config: Configuration) { use db <- result.try(builder.parse(config)) - let posts = builder.render(db, config) + let compiled = builder.compile(db, config) + let posts = builder.render(db, compiled, config) builder.write(posts, config) }