diff --git a/src/gloss/models/database.gleam b/src/gloss/models/database.gleam index 0fa1a1a..93e5ede 100644 --- a/src/gloss/models/database.gleam +++ b/src/gloss/models/database.gleam @@ -1,3 +1,6 @@ +//// The database contains all the parsed data from the input files, but not +//// processed content. + import gleam/dict.{type Dict} import gleam/list import gleam/option.{None, Some} @@ -9,6 +12,7 @@ import gloss/utils/date.{type Month} import gloss/utils/ordered_tree.{type OrderedTree} import gloss/utils/uniqid.{type Generator, type UniqID} +/// Internal post ID, generated automatically. pub type PostID = UniqID @@ -16,12 +20,15 @@ pub type PostWithID { PostWithID(id: PostID, post: Post) } +/// All tags and their posts. pub type TagPosts = Dict(Tag, OrderedTree(PostWithID)) +/// Posts organised by month. This is used inside `YearPosts`. pub type MonthPosts = Dict(Month, OrderedTree(PostWithID)) +/// Posts organised by year, containing posts organised by month. pub type YearPosts = Dict(Int, MonthPosts) @@ -37,6 +44,7 @@ pub opaque type Database { ) } +/// Create a new empty database. pub fn new() -> Database { Database( posts: new_tree(), @@ -49,6 +57,7 @@ pub fn new() -> Database { ) } +/// Add a post into the database. pub fn add_post(db: Database, post: Post) -> Database { let post_date = post.get_date(post) let #(id, id_generator) = uniqid.get(db.id_generator) @@ -100,22 +109,27 @@ pub fn add_post(db: Database, post: Post) -> Database { ) } +/// Add a page into the database. pub fn add_page(db: Database, page: Page) -> Database { Database(..db, pages: [page, ..db.pages]) } +/// Set the menu items of the database, replacing any old ones. pub fn set_menu(db: Database, menu: List(MenuItem)) -> Database { Database(..db, menu: menu) } +/// Get posts organised by tags. pub fn tags(db: Database) { db.tags } +/// Get posts organised by years and months. pub fn years(db: Database) { db.years } +/// Get all posts in the given order. pub fn get_posts_with_ids( db: Database, order: ordered_tree.WalkOrder, @@ -123,10 +137,12 @@ pub fn get_posts_with_ids( ordered_tree.to_list(db.posts, order) } +/// Get pages. pub fn pages(db: Database) { db.pages } +/// Get menu items. pub fn menu(db: Database) { db.menu } diff --git a/src/gloss/models/header.gleam b/src/gloss/models/header.gleam index c42eca2..61d0c0f 100644 --- a/src/gloss/models/header.gleam +++ b/src/gloss/models/header.gleam @@ -1,2 +1,3 @@ +/// A post or page header: a string key and a string value. pub type Header = #(String, String) diff --git a/src/gloss/models/menu.gleam b/src/gloss/models/menu.gleam index 727586d..bc1a812 100644 --- a/src/gloss/models/menu.gleam +++ b/src/gloss/models/menu.gleam @@ -1,3 +1,4 @@ +/// A menu item that points to some URL (relative or absolute) and has a name. pub type MenuItem { MenuItem(url: String, name: String) } diff --git a/src/gloss/models/page.gleam b/src/gloss/models/page.gleam index 39d03f3..a4b4e12 100644 --- a/src/gloss/models/page.gleam +++ b/src/gloss/models/page.gleam @@ -1,3 +1,5 @@ +//// A static page in the blog that is not part of any post lists or archives. + import gloss/models/header.{type Header} pub type Page { diff --git a/src/gloss/models/post.gleam b/src/gloss/models/post.gleam index aa3da6e..ae86c99 100644 --- a/src/gloss/models/post.gleam +++ b/src/gloss/models/post.gleam @@ -1,15 +1,22 @@ +//// A post in the blog. + +import gleam/int import gleam/option.{type Option} -import gleam/order.{type Order, Eq, Gt, Lt} +import gleam/order.{type Order, Eq} 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 { + /// The post only had date information. JustDate(Date) + + /// The post had date, time, and timezone information. DateTime(date: Date, time: Time, tz: String, luxon: DateTime) } +/// A tag is any string of lowercase characters, numbers, dashes, or underscores. pub type Tag = String @@ -20,12 +27,15 @@ pub type Post { tags: List(Tag), headers: List(Header), content: String, + /// The content before the split, if any short_content: Option(String), date: PostedAt, + /// The post's order during that day, if there are multiple posts on the same day. order: Int, ) } +/// Get the date of the post. pub fn get_date(post: Post) -> Date { case post.date { JustDate(date) -> date @@ -33,6 +43,7 @@ pub fn get_date(post: Post) -> Date { } } +/// Get the time of the post, if it exists. pub fn get_time(post: Post) -> Option(Time) { case post.date { JustDate(..) -> option.None @@ -40,6 +51,7 @@ pub fn get_time(post: Post) -> Option(Time) { } } +/// Get the Luxon datetime of the post, if it exists. pub fn get_luxon(post: Post) -> Option(luxon.DateTime) { case post.date { JustDate(..) -> option.None @@ -47,21 +59,25 @@ pub fn get_luxon(post: Post) -> Option(luxon.DateTime) { } } +/// Compare two posts and get an order between them. pub fn comparator(a: Post, b: Post) -> Order { let a_date = get_date(a) let b_date = get_date(b) case date.compare(a_date, b_date) { - Lt -> Lt - Gt -> Gt Eq -> { let a_time = option.lazy_unwrap(get_time(a), time.nil_time) let b_time = option.lazy_unwrap(get_time(b), time.nil_time) - time.compare(a_time, b_time) + case time.compare(a_time, b_time) { + Eq -> int.compare(a.order, b.order) + other -> other + } } + other -> other } } +/// Get the post's date or datetime formatted as an ISO 8601 formatted string. pub fn to_iso8601(post: Post) -> String { case post.date { JustDate(d) -> date.format_iso(d) diff --git a/src/gloss/parser/common.gleam b/src/gloss/parser/common.gleam index 1e2acd6..9d6964a 100644 --- a/src/gloss/parser/common.gleam +++ b/src/gloss/parser/common.gleam @@ -4,10 +4,12 @@ import gleam/list import gleam/bool import gloss/models/header.{type Header} +/// The default file extension for source files. pub const filename_postfix = ".md" const header_separator = ":" +/// An error that occurred when parsing content. pub type ParseError { EmptyFile HeaderMissing @@ -19,16 +21,19 @@ pub type ParseError { MalformedHeader(header: String) } +/// If the `value` is `Ok`, run `if_ok`, otherwise return `error`. 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) } +/// Parse given lines as headers. pub fn parse_headers(headers: List(String)) -> Result(List(Header), ParseError) { headers |> list.map(parse_header) |> result.all() } +/// Parse given line as a header. pub fn parse_header(header: String) -> Result(Header, ParseError) { let header_parts = string.split(header, header_separator) let parts_amount = list.length(header_parts) diff --git a/src/gloss/parser/menu.gleam b/src/gloss/parser/menu.gleam index 4fa727e..17b265b 100644 --- a/src/gloss/parser/menu.gleam +++ b/src/gloss/parser/menu.gleam @@ -3,6 +3,7 @@ import gleam/string import gleam/result import gloss/models/menu.{MenuItem} +/// Parse the content and return menu items. pub fn parse(content: String) { string.split(content, "\n") |> list.filter(fn(line) { string.length(line) != 0 }) diff --git a/src/gloss/parser/page.gleam b/src/gloss/parser/page.gleam index 2b9ad99..7220f3c 100644 --- a/src/gloss/parser/page.gleam +++ b/src/gloss/parser/page.gleam @@ -4,6 +4,7 @@ import gleam/list import gloss/models/page.{type Page, Page} import gloss/parser/common.{type ParseError, EmptyFile, HeaderMissing, try} +/// Parse page from file data. pub fn parse(filename: String, contents: String) -> Result(Page, ParseError) { let lines = string.split(contents, "\n") diff --git a/src/gloss/parser/post.gleam b/src/gloss/parser/post.gleam index 4a50b63..56826b1 100644 --- a/src/gloss/parser/post.gleam +++ b/src/gloss/parser/post.gleam @@ -16,6 +16,7 @@ import gloss/parser/common.{ MalformedFilename, MalformedHeader, MonthNotInt, YearNotInt, try, } +/// Post filenames must match this regex. pub const filename_regex = "^\\d{4}-\\d\\d-\\d\\d-.*\\.md$" const filename_separator = "-" @@ -28,6 +29,7 @@ type FilenameMeta { FilenameMeta(date: PostedAt, order: Int, slug: String) } +/// Parse post from file data. pub fn parse(filename: String, contents: String) -> Result(Post, ParseError) { let filename = string.slice( diff --git a/src/gloss/paths.gleam b/src/gloss/paths.gleam index 2cbc6f5..25c491e 100644 --- a/src/gloss/paths.gleam +++ b/src/gloss/paths.gleam @@ -1,9 +1,10 @@ import gleam/int import gleam/string +import gleam/list 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} +import gloss/utils/ints/day pub const default_root = "" @@ -44,9 +45,18 @@ pub const defaults = PathConfiguration( ) pub fn default_single_post(post: Post) { - let post_path = post_paths.post_to_path(post) + let post_date = post.get_date(post) + let date_parts = + list.map( + [ + post_date.year, + date.month_to_int(post_date.month), + day.to_int(post_date.day), + ], + pad_int, + ) - "/" <> post_path.date_path <> "/" <> post_path.slug + "/" <> string.join(date_parts, "/") <> "/" <> post.slug } pub fn default_page(page: Page) { @@ -81,3 +91,9 @@ pub fn default_list_page(path: String, page: Int) { pub fn default_html(path: String) { path <> ".html" } + +fn pad_int(number: Int) -> String { + number + |> int.to_string() + |> string.pad_left(to: 2, with: "0") +} diff --git a/src/gloss/paths/post.gleam b/src/gloss/paths/post.gleam deleted file mode 100644 index abb8a89..0000000 --- a/src/gloss/paths/post.gleam +++ /dev/null @@ -1,31 +0,0 @@ -import gleam/string -import gleam/int -import gleam/list -import gloss/models/post.{type Post} -import gloss/utils/date -import gloss/utils/ints/day - -pub type PostPath { - PostPath(date_path: String, slug: String) -} - -pub fn post_to_path(post: Post) -> PostPath { - let post_date = post.get_date(post) - let date_parts = - list.map( - [ - post_date.year, - date.month_to_int(post_date.month), - day.to_int(post_date.day), - ], - pad_int, - ) - - PostPath(date_path: string.join(date_parts, "/"), slug: post.slug) -} - -fn pad_int(number: Int) -> String { - number - |> int.to_string() - |> string.pad_left(to: 2, with: "0") -} diff --git a/src/gloss/renderer.gleam b/src/gloss/renderer.gleam index 24a324b..34784f8 100644 --- a/src/gloss/renderer.gleam +++ b/src/gloss/renderer.gleam @@ -55,9 +55,7 @@ pub fn render( let feed = render_feed(all_posts, compiled.posts, views) RenderDatabase( - orig: db, single_posts: posts, - index: [], pages: pages, index_pages: index_pages, tag_pages: tag_pages, diff --git a/src/gloss/rendering/database.gleam b/src/gloss/rendering/database.gleam index e3d8c70..dde6704 100644 --- a/src/gloss/rendering/database.gleam +++ b/src/gloss/rendering/database.gleam @@ -1,35 +1,41 @@ +//// The render database stores the rendered posts and pages. + import gleam/dict.{type Dict} import lustre/element.{type Element} -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) - +/// A post and its rendered content. pub type RenderedSinglePost { RenderedSinglePost(orig: Post, content: Element(Nil)) } +/// A page and its rendered content. pub type RenderedPage { RenderedPage(page: Page, content: Element(Nil)) } +/// A list page's page number and the page's content. pub type RenderedListPage { ListPage(page: Int, content: Element(Nil)) } pub type RenderDatabase { RenderDatabase( - orig: Database, + /// Individual posts. single_posts: List(RenderedSinglePost), - index: PostList, + /// Individual pages. pages: List(RenderedPage), + /// "Index" list pages, meaning the main post flow. index_pages: List(RenderedListPage), + /// Tag list pages. tag_pages: Dict(String, List(RenderedListPage)), + /// Year list pages. year_pages: Dict(Int, List(RenderedListPage)), + /// Month list pages. month_pages: Dict(#(Int, Month), List(RenderedListPage)), + /// The feed (corresponding to the main post flow). feed: Element(Nil), ) } diff --git a/src/gloss/rendering/views.gleam b/src/gloss/rendering/views.gleam index eb9122f..1783e9e 100644 --- a/src/gloss/rendering/views.gleam +++ b/src/gloss/rendering/views.gleam @@ -3,36 +3,57 @@ import gloss/compiler.{type CompiledPage, type CompiledPost} import gloss/models/post import gloss/models/page +/// The base view renders the base page layout. +/// +/// The three arguments are: +/// - The inner content to render in the layout. +/// - Extra elements to put inside ``. +/// - Text to add as a prefix to the `` element. pub type BaseView = fn(Element(Nil), List(Element(Nil)), String) -> Element(Nil) +/// Types of pages in the blog. pub type PageType { + /// Individual post. Post(post.Post) + /// Individual page. Page(page.Page) + /// Any other page. Other(title: String, description: String) } +/// The meta view renders meta tags such as OpenGraph based on the page information. pub type MetaView = fn(PageType) -> List(Element(Nil)) +/// View to render an individual post. pub type SinglePostView = fn(CompiledPost) -> Element(Nil) +/// View to render an individual page. pub type PageView = fn(CompiledPage) -> Element(Nil) +/// View to render the RSS or Atom feed. pub type FeedView = fn(List(CompiledPost)) -> Element(Nil) +/// Information passed for list pages. pub type ListInfo { ListInfo( + /// The path to prefix before the page number. E.g. `/index` and the resulting path would then be `/index/2`. root_path: String, + /// The page number of the current page being rendered. current_page: Int, + /// The amount of pages in the current list. total_pages: Int, + /// Posts on this page. posts: List(CompiledPost), + /// Any extra content to put on top of the page before the posts. extra_header: Element(Nil), ) } +/// View to render a list page. A list page is a page with many posts in a list. pub type ListPageView = fn(ListInfo) -> Element(Nil) diff --git a/src/gloss/rendering/views/base.gleam b/src/gloss/rendering/views/base.gleam index 8e36212..aac6afb 100644 --- a/src/gloss/rendering/views/base.gleam +++ b/src/gloss/rendering/views/base.gleam @@ -18,6 +18,7 @@ import gloss/config.{type Configuration} const tag_min_size = 0.5 +/// The base view pre-renders some content once, so that it can be reused for every render. pub type PreRendered { PreRendered(pages: Element(Nil), tags: Element(Nil), archives: Element(Nil)) } diff --git a/src/gloss/rendering/views/single_post.gleam b/src/gloss/rendering/views/single_post.gleam index 4ad63da..8dd2823 100644 --- a/src/gloss/rendering/views/single_post.gleam +++ b/src/gloss/rendering/views/single_post.gleam @@ -13,10 +13,12 @@ import gloss/utils/date import gloss/utils/time import gloss/utils/luxon +/// Generate a view that renders a full post. pub fn full_view(db: Database, config: Configuration) { view(_, True, db, config) } +/// Generate a view that renders a post that's inside of a list. pub fn list_view(db: Database, config: Configuration) { view(_, False, db, config) } diff --git a/src/gloss/writer.gleam b/src/gloss/writer.gleam index a1ffc23..bf9a775 100644 --- a/src/gloss/writer.gleam +++ b/src/gloss/writer.gleam @@ -6,13 +6,8 @@ import lustre/ssg import lustre/ssg/xml import lustre/element import gloss/rendering/database.{type RenderDatabase} as _ -import gloss/models/post.{type Post} -import gloss/paths/post.{type PostPath} as _ import gloss/config.{type Configuration, WriteError} -pub type PostPathGenerator = - fn(Post) -> PostPath - pub fn write(db: RenderDatabase, config: Configuration) { let site = ssg.new(config.output_path)