Add support for pages, refactor rendering code

This commit is contained in:
Mikko Ahlroth 2024-04-07 16:50:14 +03:00
parent 9e967b1eea
commit fa2f463b75
26 changed files with 443 additions and 246 deletions

View file

@ -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;
}

View file

@ -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)
}

68
src/gloss/compiler.gleam Normal file
View file

@ -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)
})
}

View file

@ -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,

View file

@ -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,
)

View file

@ -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)
}

View file

@ -0,0 +1,2 @@
pub type Header =
#(String, String)

View file

@ -0,0 +1,5 @@
import gloss/models/header.{type Header}
pub type Page {
Page(title: String, slug: String, headers: List(Header), content: String)
}

View file

@ -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

View file

@ -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"
}

View file

@ -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))
}

View file

@ -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
}
}

View file

@ -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 = "<!--\\s*SPLIT\\s*-->"
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)
}

View file

@ -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"
}

View file

@ -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)
})
}

View file

@ -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),
)
}

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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()

View file

@ -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) {

View file

@ -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)], []),
])
}

View file

@ -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 {

View file

@ -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)

View file

@ -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)
}