Add support for pages, refactor rendering code
This commit is contained in:
parent
9e967b1eea
commit
fa2f463b75
26 changed files with 443 additions and 246 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
68
src/gloss/compiler.gleam
Normal 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)
|
||||
})
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
2
src/gloss/models/header.gleam
Normal file
2
src/gloss/models/header.gleam
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub type Header =
|
||||
#(String, String)
|
5
src/gloss/models/page.gleam
Normal file
5
src/gloss/models/page.gleam
Normal file
|
@ -0,0 +1,5 @@
|
|||
import gloss/models/header.{type Header}
|
||||
|
||||
pub type Page {
|
||||
Page(title: String, slug: String, headers: List(Header), content: String)
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
44
src/gloss/parser/common.gleam
Normal file
44
src/gloss/parser/common.gleam
Normal 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))
|
||||
}
|
35
src/gloss/parser/page.gleam
Normal file
35
src/gloss/parser/page.gleam
Normal 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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
27
src/gloss/rendering/views.gleam
Normal file
27
src/gloss/rendering/views.gleam
Normal 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)
|
|
@ -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)
|
||||
|
|
@ -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()
|
|
@ -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) {
|
17
src/gloss/rendering/views/page.gleam
Normal file
17
src/gloss/rendering/views/page.gleam
Normal 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)], []),
|
||||
])
|
||||
}
|
|
@ -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 {
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue