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;
|
gap: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post h2 {
|
.post h2,
|
||||||
|
.page h2 {
|
||||||
font-size: 4rem;
|
font-size: 4rem;
|
||||||
line-height: 4rem;
|
line-height: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post header {
|
.post header,
|
||||||
|
.page header {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import gleam/result
|
import gleam/result
|
||||||
import gloss/parser
|
import gloss/parser
|
||||||
import gloss/rendering/database as render_database
|
import gloss/rendering/database as render_database
|
||||||
import gloss/renderer
|
|
||||||
import gloss/writer
|
import gloss/writer
|
||||||
import gloss/config.{type Configuration}
|
import gloss/config.{type Configuration}
|
||||||
import gloss/models/database.{type Database}
|
import gloss/models/database.{type Database}
|
||||||
|
import gloss/compiler.{type CompileDatabase}
|
||||||
|
|
||||||
pub type BuildError {
|
pub type BuildError {
|
||||||
ParseError(err: parser.ParseError)
|
ParseError(err: parser.ParseError)
|
||||||
|
@ -16,11 +16,15 @@ pub fn parse(config: Configuration) {
|
||||||
|> result.map_error(ParseError)
|
|> result.map_error(ParseError)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(db: Database, config: Configuration) {
|
pub fn compile(db: Database, config: Configuration) {
|
||||||
renderer.render(db, config)
|
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)
|
config.writer(posts, config.paths)
|
||||||
|> result.map_error(WriteError)
|
|> 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 gleam/option
|
||||||
import gloss/paths.{type PathConfiguration}
|
import gloss/paths.{type PathConfiguration}
|
||||||
import gloss/rendering/templates.{
|
import gloss/rendering/views.{
|
||||||
type BaseRenderer, type FeedRenderer, type ListPageRenderer,
|
type BaseView, type FeedView, type ListPageView, type PageView,
|
||||||
type PostContentRenderer, type SinglePostRenderer,
|
type SinglePostView,
|
||||||
}
|
}
|
||||||
|
import gloss/compiler.{type CompileDatabase, type Compiler}
|
||||||
import gloss/parser.{type Parser}
|
import gloss/parser.{type Parser}
|
||||||
import gloss/writer.{type Writer}
|
import gloss/writer.{type Writer}
|
||||||
import gloss/models/database.{type Database}
|
import gloss/models/database.{type Database}
|
||||||
|
import gloss/rendering/database.{type RenderDatabase} as _
|
||||||
|
|
||||||
pub type Templates {
|
pub type Views {
|
||||||
Templates(
|
Views(
|
||||||
base: fn(Database, Configuration) -> BaseRenderer,
|
base: fn(Database, Configuration) -> BaseView,
|
||||||
single_post_full: fn(Database, Configuration) -> SinglePostRenderer,
|
single_post_full: fn(Database, Configuration) -> SinglePostView,
|
||||||
single_post_list: fn(Database, Configuration) -> SinglePostRenderer,
|
single_post_list: fn(Database, Configuration) -> SinglePostView,
|
||||||
list_page: fn(Database, Configuration) -> ListPageRenderer,
|
page: fn(Database, Configuration) -> PageView,
|
||||||
feed: fn(Database, Configuration) -> FeedRenderer,
|
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 {
|
pub type Rendering {
|
||||||
Rendering(
|
Rendering(
|
||||||
templates: Templates,
|
renderer: fn(Database, CompileDatabase, Configuration) -> RenderDatabase,
|
||||||
|
views: Views,
|
||||||
copyright: String,
|
copyright: String,
|
||||||
content_renderer: PostContentRenderer,
|
|
||||||
posts_per_page: Int,
|
posts_per_page: Int,
|
||||||
posts_in_feed: Int,
|
posts_in_feed: Int,
|
||||||
)
|
)
|
||||||
|
@ -37,6 +47,7 @@ pub type Configuration {
|
||||||
blog_name: String,
|
blog_name: String,
|
||||||
blog_url: String,
|
blog_url: String,
|
||||||
author: Author,
|
author: Author,
|
||||||
|
compiling: Compiling,
|
||||||
rendering: Rendering,
|
rendering: Rendering,
|
||||||
paths: PathConfiguration,
|
paths: PathConfiguration,
|
||||||
parser: Parser,
|
parser: Parser,
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
import gloss/config.{type Configuration, Configuration, Rendering}
|
import gloss/config.{type Configuration, Compiling, Configuration, Rendering}
|
||||||
import gloss/rendering/templates/single_post
|
import gloss/rendering/views/single_post
|
||||||
import gloss/rendering/templates/base
|
import gloss/rendering/views/base
|
||||||
import gloss/rendering/templates/list_page
|
import gloss/rendering/views/list_page
|
||||||
import gloss/rendering/templates/feed
|
import gloss/rendering/views/page
|
||||||
|
import gloss/rendering/views/feed
|
||||||
|
import gloss/renderer
|
||||||
import gloss/paths
|
import gloss/paths
|
||||||
import gloss/parser
|
import gloss/parser
|
||||||
import gloss/writer
|
import gloss/writer
|
||||||
|
import gloss/compiler
|
||||||
|
|
||||||
const default_templates = config.Templates(
|
const default_views = config.Views(
|
||||||
base: base.generate,
|
base: base.generate,
|
||||||
single_post_full: single_post.full_view,
|
single_post_full: single_post.full_view,
|
||||||
single_post_list: single_post.list_view,
|
single_post_list: single_post.list_view,
|
||||||
|
page: page.generate,
|
||||||
list_page: list_page.generate,
|
list_page: list_page.generate,
|
||||||
feed: feed.generate,
|
feed: feed.generate,
|
||||||
)
|
)
|
||||||
|
@ -25,18 +29,18 @@ pub fn default_config(
|
||||||
blog_name: blog_name,
|
blog_name: blog_name,
|
||||||
blog_url: blog_url,
|
blog_url: blog_url,
|
||||||
author: author,
|
author: author,
|
||||||
|
compiling: Compiling(
|
||||||
|
database_compiler: compiler.compile,
|
||||||
|
item_compiler: compiler.default_compiler,
|
||||||
|
),
|
||||||
rendering: Rendering(
|
rendering: Rendering(
|
||||||
templates: default_templates,
|
renderer: renderer.render,
|
||||||
|
views: default_views,
|
||||||
copyright: copyright,
|
copyright: copyright,
|
||||||
content_renderer: single_post.content_renderer,
|
|
||||||
posts_per_page: 10,
|
posts_per_page: 10,
|
||||||
posts_in_feed: 20,
|
posts_in_feed: 20,
|
||||||
),
|
),
|
||||||
paths: paths.conf(
|
paths: paths.defaults,
|
||||||
paths.default_index,
|
|
||||||
paths.default_single_post,
|
|
||||||
paths.default_tag,
|
|
||||||
),
|
|
||||||
parser: parser.default_parse,
|
parser: parser.default_parse,
|
||||||
writer: writer.write,
|
writer: writer.write,
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,6 +3,7 @@ import gleam/list
|
||||||
import gleam/option.{None, Some}
|
import gleam/option.{None, Some}
|
||||||
import gleam/order.{type Order}
|
import gleam/order.{type Order}
|
||||||
import gloss/models/post.{type Post, type Tag}
|
import gloss/models/post.{type Post, type Tag}
|
||||||
|
import gloss/models/page.{type Page}
|
||||||
import gloss/utils/date.{type Month}
|
import gloss/utils/date.{type Month}
|
||||||
import gloss/utils/ordered_tree.{type OrderedTree}
|
import gloss/utils/ordered_tree.{type OrderedTree}
|
||||||
import gloss/utils/uniqid.{type Generator, type UniqID}
|
import gloss/utils/uniqid.{type Generator, type UniqID}
|
||||||
|
@ -26,6 +27,7 @@ pub type YearPosts =
|
||||||
pub opaque type Database {
|
pub opaque type Database {
|
||||||
Database(
|
Database(
|
||||||
posts: OrderedTree(PostWithID),
|
posts: OrderedTree(PostWithID),
|
||||||
|
pages: List(Page),
|
||||||
tags: TagPosts,
|
tags: TagPosts,
|
||||||
years: YearPosts,
|
years: YearPosts,
|
||||||
posts_by_id: Dict(PostID, Post),
|
posts_by_id: Dict(PostID, Post),
|
||||||
|
@ -36,6 +38,7 @@ pub opaque type Database {
|
||||||
pub fn new() -> Database {
|
pub fn new() -> Database {
|
||||||
Database(
|
Database(
|
||||||
posts: new_tree(),
|
posts: new_tree(),
|
||||||
|
pages: [],
|
||||||
tags: dict.new(),
|
tags: dict.new(),
|
||||||
years: dict.new(),
|
years: dict.new(),
|
||||||
posts_by_id: dict.new(),
|
posts_by_id: dict.new(),
|
||||||
|
@ -85,6 +88,7 @@ pub fn add_post(db: Database, post: Post) -> Database {
|
||||||
})
|
})
|
||||||
|
|
||||||
Database(
|
Database(
|
||||||
|
..db,
|
||||||
id_generator: id_generator,
|
id_generator: id_generator,
|
||||||
posts: posts,
|
posts: posts,
|
||||||
tags: tags,
|
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) {
|
pub fn tags(db: Database) {
|
||||||
db.tags
|
db.tags
|
||||||
}
|
}
|
||||||
|
@ -108,6 +116,10 @@ pub fn get_posts_with_ids(
|
||||||
ordered_tree.to_list(db.posts, order)
|
ordered_tree.to_list(db.posts, order)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn pages(db: Database) {
|
||||||
|
db.pages
|
||||||
|
}
|
||||||
|
|
||||||
fn new_tree() -> OrderedTree(PostWithID) {
|
fn new_tree() -> OrderedTree(PostWithID) {
|
||||||
ordered_tree.new(comparator)
|
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/date.{type Date}
|
||||||
import gloss/utils/time.{type Time, Time}
|
import gloss/utils/time.{type Time, Time}
|
||||||
import gloss/utils/luxon.{type DateTime}
|
import gloss/utils/luxon.{type DateTime}
|
||||||
|
import gloss/models/header.{type Header}
|
||||||
|
|
||||||
pub type PostedAt {
|
pub type PostedAt {
|
||||||
JustDate(Date)
|
JustDate(Date)
|
||||||
DateTime(date: Date, time: Time, tz: String, luxon: DateTime)
|
DateTime(date: Date, time: Time, tz: String, luxon: DateTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Header =
|
|
||||||
#(String, String)
|
|
||||||
|
|
||||||
pub type Tag =
|
pub type Tag =
|
||||||
String
|
String
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,9 @@ import gleam/list
|
||||||
import gleam/regex
|
import gleam/regex
|
||||||
import gloss/utils/fs
|
import gloss/utils/fs
|
||||||
import gloss/models/database.{type Database}
|
import gloss/models/database.{type Database}
|
||||||
|
import gloss/parser/common
|
||||||
import gloss/parser/post
|
import gloss/parser/post
|
||||||
|
import gloss/parser/page
|
||||||
|
|
||||||
const default_data_path = "./data"
|
const default_data_path = "./data"
|
||||||
|
|
||||||
|
@ -12,15 +14,15 @@ pub type Parser =
|
||||||
|
|
||||||
pub type ParseError {
|
pub type ParseError {
|
||||||
FileError(path: String, err: fs.FSError)
|
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) {
|
pub fn default_parse() -> Result(Database, ParseError) {
|
||||||
let db = database.new()
|
parse_posts(database.new(), post_path())
|
||||||
parse_posts(post_path(), db)
|
|> 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(
|
use filenames <- result.try(
|
||||||
fs.readdir(path)
|
fs.readdir(path)
|
||||||
|> result.map_error(fn(err) { FileError(path, err) }),
|
|> 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 =
|
let filenames =
|
||||||
list.filter(filenames, fn(file) { regex.check(filename_regex, file) })
|
list.filter(filenames, fn(file) { regex.check(filename_regex, file) })
|
||||||
|
|
||||||
use posts <- result.try(result.all(list.map(
|
use posts <- result.try(
|
||||||
filenames,
|
result.all(
|
||||||
fn(file) {
|
list.map(filenames, fn(file) {
|
||||||
use contents <- result.try(
|
use contents <- result.try(
|
||||||
fs.read_file(path <> "/" <> file)
|
fs.read_file(path <> "/" <> file)
|
||||||
|> result.map_error(fn(err) { FileError(file, err) }),
|
|> result.map_error(fn(err) { FileError(file, err) }),
|
||||||
)
|
)
|
||||||
|
|
||||||
post.parse(file, contents)
|
post.parse(file, contents)
|
||||||
|> result.map_error(fn(err) { PostParseError(file, err) })
|
|> result.map_error(fn(err) { PostParseError(file, err) })
|
||||||
},
|
}),
|
||||||
)))
|
),
|
||||||
|
)
|
||||||
|
|
||||||
Ok(list.fold(posts, db, database.add_post))
|
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() {
|
fn post_path() {
|
||||||
default_data_path <> "/posts"
|
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/int
|
||||||
import gleam/bool
|
import gleam/bool
|
||||||
import gleam/regex
|
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/date.{Date}
|
||||||
import gloss/utils/time.{type Time, Time}
|
import gloss/utils/time.{type Time, Time}
|
||||||
import gloss/utils/ints/day
|
import gloss/utils/ints/day
|
||||||
import gloss/utils/luxon
|
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$"
|
pub const filename_regex = "^\\d{4}-\\d\\d-\\d\\d-.*\\.md$"
|
||||||
|
|
||||||
|
@ -19,25 +24,12 @@ const filename_postfix = ".md"
|
||||||
|
|
||||||
const tag_separator = ","
|
const tag_separator = ","
|
||||||
|
|
||||||
const header_separator = ":"
|
|
||||||
|
|
||||||
const split_re = "<!--\\s*SPLIT\\s*-->"
|
const split_re = "<!--\\s*SPLIT\\s*-->"
|
||||||
|
|
||||||
type FilenameMeta {
|
type FilenameMeta {
|
||||||
FilenameMeta(date: PostedAt, order: Int, slug: String)
|
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) {
|
pub fn parse(filename: String, contents: String) -> Result(Post, ParseError) {
|
||||||
let lines = string.split(contents, "\n")
|
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) })
|
list.split_while(rest, fn(line) { !string.is_empty(line) })
|
||||||
|
|
||||||
let tags = parse_tags(tags)
|
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 body = string.join(body, "\n")
|
||||||
let short_content = parse_short_content(body)
|
let short_content = parse_short_content(body)
|
||||||
use time <- result.try(parse_time(headers))
|
use time <- result.try(parse_time(headers))
|
||||||
|
@ -147,28 +139,6 @@ fn parse_tags(tags: String) -> List(Tag) {
|
||||||
|> list.map(string.trim)
|
|> 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) {
|
fn parse_short_content(body: String) -> option.Option(String) {
|
||||||
let assert Ok(re) =
|
let assert Ok(re) =
|
||||||
regex.compile(
|
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/int
|
||||||
import gleam/string
|
import gleam/string
|
||||||
import gloss/models/post.{type Post}
|
import gloss/models/post.{type Post}
|
||||||
|
import gloss/models/page.{type Page}
|
||||||
import gloss/paths/post as post_paths
|
import gloss/paths/post as post_paths
|
||||||
import gloss/utils/date.{type Month}
|
import gloss/utils/date.{type Month}
|
||||||
|
|
||||||
|
@ -10,6 +11,7 @@ pub type PathConfiguration {
|
||||||
PathConfiguration(
|
PathConfiguration(
|
||||||
index: String,
|
index: String,
|
||||||
single_post: fn(Post) -> String,
|
single_post: fn(Post) -> String,
|
||||||
|
page: fn(Page) -> String,
|
||||||
tag: fn(String) -> String,
|
tag: fn(String) -> String,
|
||||||
year: fn(Int) -> String,
|
year: fn(Int) -> String,
|
||||||
month: fn(Int, Month) -> String,
|
month: fn(Int, Month) -> String,
|
||||||
|
@ -18,17 +20,16 @@ pub type PathConfiguration {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn conf(index, single_post, tag) -> PathConfiguration {
|
pub const defaults = PathConfiguration(
|
||||||
PathConfiguration(
|
index: default_index,
|
||||||
index,
|
single_post: default_single_post,
|
||||||
single_post,
|
page: default_page,
|
||||||
tag,
|
tag: default_tag,
|
||||||
year_archive,
|
year: default_year_archive,
|
||||||
month_archive,
|
month: default_month_archive,
|
||||||
list_page,
|
list_page: default_list_page,
|
||||||
html,
|
html: default_html,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default_single_post(post: Post) {
|
pub fn default_single_post(post: Post) {
|
||||||
let post_path = post_paths.post_to_path(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
|
"/" <> post_path.date_path <> "/" <> post_path.slug
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn default_page(page: Page) {
|
||||||
|
"/" <> page.slug
|
||||||
|
}
|
||||||
|
|
||||||
pub fn default_tag(tag: String) {
|
pub fn default_tag(tag: String) {
|
||||||
"/tag/" <> tag
|
"/tag/" <> tag
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn year_archive(year: Int) {
|
pub fn default_year_archive(year: Int) {
|
||||||
"/archive" <> "/" <> int.to_string(year)
|
"/archive" <> "/" <> int.to_string(year)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn month_archive(year: Int, month: Month) {
|
pub fn default_month_archive(year: Int, month: Month) {
|
||||||
year_archive(year)
|
default_year_archive(year)
|
||||||
<> "/"
|
<> "/"
|
||||||
<> string.pad_left(int.to_string(date.month_to_int(month)), 2, "0")
|
<> 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.
|
/// Get the given list path with a page number.
|
||||||
///
|
///
|
||||||
/// The first page does not get any appended 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 {
|
case page {
|
||||||
1 -> path
|
1 -> path
|
||||||
other -> path <> "/" <> int.to_string(other)
|
other -> path <> "/" <> int.to_string(other)
|
||||||
|
@ -61,6 +66,6 @@ pub fn list_page(path: String, page: Int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get path with the .html extension
|
/// Get path with the .html extension
|
||||||
pub fn html(path: String) {
|
pub fn default_html(path: String) {
|
||||||
path <> ".html"
|
path <> ".html"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,53 +3,60 @@ import gleam/dict.{type Dict}
|
||||||
import gleam/result
|
import gleam/result
|
||||||
import gleam/int
|
import gleam/int
|
||||||
import lustre/element.{type Element}
|
import lustre/element.{type Element}
|
||||||
import gloss/rendering/templates.{
|
import gloss/rendering/views.{
|
||||||
type BaseRenderer, type FeedRenderer, type ListPageRenderer,
|
type BaseView, type FeedView, type ListPageView, type PageView,
|
||||||
type PostContentRenderer, type SinglePostRenderer, ListInfo,
|
type SinglePostView, ListInfo,
|
||||||
}
|
}
|
||||||
import gloss/rendering/database.{
|
import gloss/rendering/database.{
|
||||||
type Database as RenderDatabase, type RenderedPost, type RenderedSinglePost,
|
type RenderDatabase, type RenderedSinglePost, ListPage, RenderDatabase,
|
||||||
ListPage, RenderedPost, RenderedSinglePost,
|
RenderedPage, RenderedSinglePost,
|
||||||
} as render_database
|
} as _
|
||||||
|
import gloss/compiler.{
|
||||||
|
type CompileDatabase, type CompiledPage, type CompiledPost,
|
||||||
|
}
|
||||||
import gloss/models/database.{type Database, type PostID, type PostWithID}
|
import gloss/models/database.{type Database, type PostID, type PostWithID}
|
||||||
import gloss/utils/ordered_tree
|
import gloss/utils/ordered_tree
|
||||||
import gloss/config.{type Configuration}
|
import gloss/config.{type Configuration}
|
||||||
import gloss/utils/date
|
import gloss/utils/date
|
||||||
|
|
||||||
pub type Renderers {
|
pub type Views {
|
||||||
Renderers(
|
Views(
|
||||||
base: BaseRenderer,
|
base: BaseView,
|
||||||
single_post_full: SinglePostRenderer,
|
single_post_full: SinglePostView,
|
||||||
list_page: ListPageRenderer,
|
page: PageView,
|
||||||
feed: FeedRenderer,
|
list_page: ListPageView,
|
||||||
|
feed: FeedView,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(db: Database, config: Configuration) -> RenderDatabase {
|
pub fn render(
|
||||||
let renderers =
|
db: Database,
|
||||||
Renderers(
|
compiled: CompileDatabase,
|
||||||
base: config.rendering.templates.base(db, config),
|
config: Configuration,
|
||||||
single_post_full: config.rendering.templates.single_post_full(db, config),
|
) -> RenderDatabase {
|
||||||
list_page: config.rendering.templates.list_page(db, config),
|
let views =
|
||||||
feed: config.rendering.templates.feed(db, config),
|
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 all_posts = database.get_posts_with_ids(db, ordered_tree.Desc)
|
||||||
let post_contents =
|
let posts = render_posts(db, compiled.posts, views)
|
||||||
render_post_contents(all_posts, config.rendering.content_renderer)
|
let pages = render_pages(db, compiled.pages, views)
|
||||||
let posts = render_posts(db, post_contents, renderers)
|
let index_pages = render_index_pages(config, all_posts, compiled.posts, views)
|
||||||
let index_pages =
|
let tag_pages = render_tag_pages(config, db, compiled.posts, views)
|
||||||
render_index_pages(config, all_posts, post_contents, renderers)
|
let year_pages = render_year_pages(config, db, compiled.posts, views)
|
||||||
let tag_pages = render_tag_pages(config, db, post_contents, renderers)
|
let month_pages = render_month_pages(config, db, compiled.posts, views)
|
||||||
let year_pages = render_year_pages(config, db, post_contents, renderers)
|
let feed = render_feed(all_posts, compiled.posts, views)
|
||||||
let month_pages = render_month_pages(config, db, post_contents, renderers)
|
|
||||||
let feed = render_feed(all_posts, post_contents, renderers)
|
|
||||||
|
|
||||||
render_database.Database(
|
RenderDatabase(
|
||||||
orig: db,
|
orig: db,
|
||||||
posts: post_contents,
|
|
||||||
single_posts: posts,
|
single_posts: posts,
|
||||||
index: [],
|
index: [],
|
||||||
|
pages: pages,
|
||||||
index_pages: index_pages,
|
index_pages: index_pages,
|
||||||
tag_pages: tag_pages,
|
tag_pages: tag_pages,
|
||||||
year_pages: year_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(
|
pub fn render_posts(
|
||||||
db: Database,
|
db: Database,
|
||||||
post_contents: Dict(PostID, RenderedPost),
|
post_contents: Dict(PostID, CompiledPost),
|
||||||
renderers: Renderers,
|
views: Views,
|
||||||
) -> List(RenderedSinglePost) {
|
) -> List(RenderedSinglePost) {
|
||||||
let all_posts = database.get_posts_with_ids(db, ordered_tree.Desc)
|
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) {
|
|> list.map(fn(post_with_id) {
|
||||||
let assert Ok(content) = dict.get(post_contents, post_with_id.id)
|
let assert Ok(content) = dict.get(post_contents, post_with_id.id)
|
||||||
let rendered =
|
let rendered =
|
||||||
renderers.base(
|
views.base(views.single_post_full(content), post_with_id.post.title)
|
||||||
renderers.single_post_full(content),
|
|
||||||
post_with_id.post.title,
|
|
||||||
)
|
|
||||||
RenderedSinglePost(post_with_id.post, rendered)
|
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(
|
fn render_index_pages(
|
||||||
config: Configuration,
|
config: Configuration,
|
||||||
posts: List(database.PostWithID),
|
posts: List(database.PostWithID),
|
||||||
posts_with_contents: Dict(PostID, RenderedPost),
|
compiled_posts: Dict(PostID, CompiledPost),
|
||||||
renderers: Renderers,
|
views: Views,
|
||||||
) {
|
) {
|
||||||
pageify_posts(
|
pageify_posts(
|
||||||
posts,
|
posts,
|
||||||
config,
|
config,
|
||||||
posts_with_contents,
|
compiled_posts,
|
||||||
renderers,
|
views,
|
||||||
"",
|
"",
|
||||||
config.paths.index,
|
config.paths.index,
|
||||||
element.none(),
|
element.none(),
|
||||||
|
@ -111,8 +112,8 @@ fn render_index_pages(
|
||||||
fn render_tag_pages(
|
fn render_tag_pages(
|
||||||
config: Configuration,
|
config: Configuration,
|
||||||
db: Database,
|
db: Database,
|
||||||
posts_with_contents: Dict(PostID, RenderedPost),
|
compiled_posts: Dict(PostID, CompiledPost),
|
||||||
renderers: Renderers,
|
views: Views,
|
||||||
) {
|
) {
|
||||||
let tags = database.tags(db)
|
let tags = database.tags(db)
|
||||||
|
|
||||||
|
@ -121,8 +122,8 @@ fn render_tag_pages(
|
||||||
pageify_posts(
|
pageify_posts(
|
||||||
posts,
|
posts,
|
||||||
config,
|
config,
|
||||||
posts_with_contents,
|
compiled_posts,
|
||||||
renderers,
|
views,
|
||||||
tag,
|
tag,
|
||||||
config.paths.tag(tag),
|
config.paths.tag(tag),
|
||||||
element.none(),
|
element.none(),
|
||||||
|
@ -133,8 +134,8 @@ fn render_tag_pages(
|
||||||
fn render_year_pages(
|
fn render_year_pages(
|
||||||
config: Configuration,
|
config: Configuration,
|
||||||
db: Database,
|
db: Database,
|
||||||
posts_with_contents: Dict(PostID, RenderedPost),
|
compiled_posts: Dict(PostID, CompiledPost),
|
||||||
renderers: Renderers,
|
views: Views,
|
||||||
) {
|
) {
|
||||||
let years = database.years(db)
|
let years = database.years(db)
|
||||||
|
|
||||||
|
@ -153,8 +154,8 @@ fn render_year_pages(
|
||||||
pageify_posts(
|
pageify_posts(
|
||||||
posts,
|
posts,
|
||||||
config,
|
config,
|
||||||
posts_with_contents,
|
compiled_posts,
|
||||||
renderers,
|
views,
|
||||||
"Archives for " <> int.to_string(year),
|
"Archives for " <> int.to_string(year),
|
||||||
config.paths.year(year),
|
config.paths.year(year),
|
||||||
element.none(),
|
element.none(),
|
||||||
|
@ -165,8 +166,8 @@ fn render_year_pages(
|
||||||
fn render_month_pages(
|
fn render_month_pages(
|
||||||
config: Configuration,
|
config: Configuration,
|
||||||
db: Database,
|
db: Database,
|
||||||
posts_with_contents: Dict(PostID, RenderedPost),
|
compiled_posts: Dict(PostID, CompiledPost),
|
||||||
renderers: Renderers,
|
views: Views,
|
||||||
) {
|
) {
|
||||||
let years = database.years(db)
|
let years = database.years(db)
|
||||||
|
|
||||||
|
@ -179,8 +180,8 @@ fn render_month_pages(
|
||||||
pageify_posts(
|
pageify_posts(
|
||||||
posts,
|
posts,
|
||||||
config,
|
config,
|
||||||
posts_with_contents,
|
compiled_posts,
|
||||||
renderers,
|
views,
|
||||||
"Archives for "
|
"Archives for "
|
||||||
<> date.month_to_string(month)
|
<> date.month_to_string(month)
|
||||||
<> " "
|
<> " "
|
||||||
|
@ -195,22 +196,22 @@ fn render_month_pages(
|
||||||
|
|
||||||
fn render_feed(
|
fn render_feed(
|
||||||
posts: List(database.PostWithID),
|
posts: List(database.PostWithID),
|
||||||
posts_with_contents: Dict(PostID, RenderedPost),
|
compiled_posts: Dict(PostID, CompiledPost),
|
||||||
renderers: Renderers,
|
views: Views,
|
||||||
) {
|
) {
|
||||||
let posts =
|
let posts =
|
||||||
list.map(posts, fn(post) {
|
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
|
rendered
|
||||||
})
|
})
|
||||||
renderers.feed(posts)
|
views.feed(posts)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pageify_posts(
|
fn pageify_posts(
|
||||||
posts: List(PostWithID),
|
posts: List(PostWithID),
|
||||||
config: Configuration,
|
config: Configuration,
|
||||||
posts_with_contents: Dict(PostID, RenderedPost),
|
compiled_posts: Dict(PostID, CompiledPost),
|
||||||
renderers: Renderers,
|
views: Views,
|
||||||
title_prefix: String,
|
title_prefix: String,
|
||||||
root_path: String,
|
root_path: String,
|
||||||
extra_header: Element(Nil),
|
extra_header: Element(Nil),
|
||||||
|
@ -226,13 +227,13 @@ fn pageify_posts(
|
||||||
current_page: page,
|
current_page: page,
|
||||||
total_pages: total_pages,
|
total_pages: total_pages,
|
||||||
posts: list.map(page_posts, fn(post_with_id) {
|
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
|
post
|
||||||
}),
|
}),
|
||||||
extra_header: extra_header,
|
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)
|
ListPage(page: page, content: page_content)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,39 +1,35 @@
|
||||||
import gleam/option.{type Option}
|
|
||||||
import gleam/dict.{type Dict}
|
import gleam/dict.{type Dict}
|
||||||
import lustre/element.{type Element}
|
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/post.{type Post}
|
||||||
|
import gloss/models/page.{type Page}
|
||||||
import gloss/utils/date.{type Month}
|
import gloss/utils/date.{type Month}
|
||||||
|
|
||||||
pub type PostList =
|
pub type PostList =
|
||||||
List(PostID)
|
List(PostID)
|
||||||
|
|
||||||
pub type RenderedContent {
|
|
||||||
RenderedContent(full: String, short: Option(String))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type RenderedPost {
|
|
||||||
RenderedPost(orig: Post, content: RenderedContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type RenderedSinglePost {
|
pub type RenderedSinglePost {
|
||||||
RenderedSinglePost(orig: Post, content: Element(Nil))
|
RenderedSinglePost(orig: Post, content: Element(Nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type RenderedPage {
|
pub type RenderedPage {
|
||||||
|
RenderedPage(page: Page, content: Element(Nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type RenderedListPage {
|
||||||
ListPage(page: Int, content: Element(Nil))
|
ListPage(page: Int, content: Element(Nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Database {
|
pub type RenderDatabase {
|
||||||
Database(
|
RenderDatabase(
|
||||||
orig: OrigDatabase,
|
orig: Database,
|
||||||
posts: Dict(PostID, RenderedPost),
|
|
||||||
single_posts: List(RenderedSinglePost),
|
single_posts: List(RenderedSinglePost),
|
||||||
index: PostList,
|
index: PostList,
|
||||||
index_pages: List(RenderedPage),
|
pages: List(RenderedPage),
|
||||||
tag_pages: Dict(String, List(RenderedPage)),
|
index_pages: List(RenderedListPage),
|
||||||
year_pages: Dict(Int, List(RenderedPage)),
|
tag_pages: Dict(String, List(RenderedListPage)),
|
||||||
month_pages: Dict(#(Int, Month), List(RenderedPage)),
|
year_pages: Dict(Int, List(RenderedListPage)),
|
||||||
|
month_pages: Dict(#(Int, Month), List(RenderedListPage)),
|
||||||
feed: Element(Nil),
|
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/database.{type Database}
|
||||||
import gloss/models/post
|
import gloss/models/post
|
||||||
import gloss/config.{type Configuration}
|
import gloss/config.{type Configuration}
|
||||||
import gloss/rendering/database.{type RenderedPost} as _
|
import gloss/compiler.{type CompiledPost}
|
||||||
import gloss/utils/luxon
|
import gloss/utils/luxon
|
||||||
import gloss/utils/date
|
import gloss/utils/date
|
||||||
|
|
||||||
pub fn generate(_db: Database, config: Configuration) {
|
pub fn generate(_db: Database, config: Configuration) {
|
||||||
fn(posts: List(RenderedPost)) {
|
fn(posts: List(CompiledPost)) {
|
||||||
let now = luxon.utc_now()
|
let now = luxon.utc_now()
|
||||||
let #(posts, _) = list.split(posts, config.rendering.posts_in_feed)
|
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/element/html.{footer, nav}
|
||||||
import lustre/attribute.{attribute, class}
|
import lustre/attribute.{attribute, class}
|
||||||
import gloss/models/database.{type Database}
|
import gloss/models/database.{type Database}
|
||||||
import gloss/rendering/templates.{type ListInfo}
|
import gloss/rendering/views.{type ListInfo}
|
||||||
import gloss/rendering/templates/nav
|
import gloss/rendering/views/nav
|
||||||
import gloss/config.{type Configuration}
|
import gloss/config.{type Configuration}
|
||||||
|
|
||||||
pub fn generate(db: Database, config: Configuration) {
|
pub fn generate(db: Database, config: Configuration) {
|
||||||
let single_post_renderer =
|
let single_post_renderer = config.rendering.views.single_post_list(db, config)
|
||||||
config.rendering.templates.single_post_list(db, config)
|
|
||||||
|
|
||||||
fn(info: ListInfo) {
|
fn(info: ListInfo) {
|
||||||
let none = element.none()
|
let none = element.none()
|
|
@ -3,7 +3,7 @@ import gleam/int
|
||||||
import lustre/element/html.{a, li, span, ul}
|
import lustre/element/html.{a, li, span, ul}
|
||||||
import lustre/element.{text}
|
import lustre/element.{text}
|
||||||
import lustre/attribute.{attribute, href}
|
import lustre/attribute.{attribute, href}
|
||||||
import gloss/rendering/templates.{type ListInfo}
|
import gloss/rendering/views.{type ListInfo}
|
||||||
import gloss/config.{type Configuration}
|
import gloss/config.{type Configuration}
|
||||||
|
|
||||||
pub fn view(list_info: ListInfo, root_path: String, config: 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 lustre/attribute.{attribute, class, href}
|
||||||
import gloss/models/database.{type Database}
|
import gloss/models/database.{type Database}
|
||||||
import gloss/models/post.{type Post}
|
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/config.{type Configuration}
|
||||||
import gloss/utils/marked
|
|
||||||
import gloss/utils/date
|
import gloss/utils/date
|
||||||
import gloss/utils/time
|
import gloss/utils/time
|
||||||
import gloss/utils/luxon
|
import gloss/utils/luxon
|
||||||
|
@ -22,13 +21,7 @@ pub fn list_view(db: Database, config: Configuration) {
|
||||||
view(_, False, db, config)
|
view(_, False, db, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn content_renderer(post: Post) {
|
fn view(post: CompiledPost, is_full: Bool, _db: Database, config: Configuration) {
|
||||||
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) {
|
|
||||||
let post_url = config.paths.html(config.paths.single_post(post.orig))
|
let post_url = config.paths.html(config.paths.single_post(post.orig))
|
||||||
|
|
||||||
let content = case post.content.short, is_full {
|
let content = case post.content.short, is_full {
|
|
@ -4,7 +4,7 @@ import gleam/result
|
||||||
import lustre/ssg
|
import lustre/ssg
|
||||||
import lustre/ssg/xml
|
import lustre/ssg/xml
|
||||||
import lustre/element
|
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/models/post.{type Post}
|
||||||
import gloss/paths/post.{type PostPath} as _
|
import gloss/paths/post.{type PostPath} as _
|
||||||
import gloss/paths.{type PathConfiguration}
|
import gloss/paths.{type PathConfiguration}
|
||||||
|
@ -15,13 +15,13 @@ pub type PostPathGenerator =
|
||||||
fn(Post) -> PostPath
|
fn(Post) -> PostPath
|
||||||
|
|
||||||
pub type Writer =
|
pub type Writer =
|
||||||
fn(Database, PathConfiguration) -> Result(Nil, WriteError)
|
fn(RenderDatabase, PathConfiguration) -> Result(Nil, WriteError)
|
||||||
|
|
||||||
pub type WriteError {
|
pub type WriteError {
|
||||||
WriteError(err: ssg.BuildError)
|
WriteError(err: ssg.BuildError)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write(db: Database, path_conf: PathConfiguration) {
|
pub fn write(db: RenderDatabase, path_conf: PathConfiguration) {
|
||||||
let site =
|
let site =
|
||||||
ssg.new(default_output)
|
ssg.new(default_output)
|
||||||
|> ssg.add_static_dir("./assets")
|
|> ssg.add_static_dir("./assets")
|
||||||
|
@ -81,6 +81,11 @@ pub fn write(db: Database, path_conf: PathConfiguration) {
|
||||||
ssg.add_static_route(acc, path, post)
|
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
|
site
|
||||||
|> ssg.build()
|
|> ssg.build()
|
||||||
|> result.map_error(WriteError)
|
|> result.map_error(WriteError)
|
||||||
|
|
|
@ -22,6 +22,7 @@ pub fn main() {
|
||||||
|
|
||||||
pub fn build(config: Configuration) {
|
pub fn build(config: Configuration) {
|
||||||
use db <- result.try(builder.parse(config))
|
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)
|
builder.write(posts, config)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue