diff --git a/gleam.toml b/gleam.toml index e3760d9..56e3346 100644 --- a/gleam.toml +++ b/gleam.toml @@ -20,6 +20,7 @@ lustre_ssg = "~> 0.5.0" gleam_javascript = "~> 0.8" ranged_int = "~> 2.0" bigi = "~> 3.0" +simplifile = "~> 1.7" [dev-dependencies] gleeunit = "~> 1.0" diff --git a/manifest.toml b/manifest.toml index e7340e8..c7e8c96 100644 --- a/manifest.toml +++ b/manifest.toml @@ -27,3 +27,4 @@ gleeunit = { version = "~> 1.0" } lustre = { version = "~> 4.1" } lustre_ssg = { version = "~> 0.5.0" } ranged_int = { version = "~> 2.0" } +simplifile = { version = "~> 1.7"} diff --git a/src/ffi_buffer.mjs b/src/ffi_buffer.mjs deleted file mode 100644 index dd93afb..0000000 --- a/src/ffi_buffer.mjs +++ /dev/null @@ -1,3 +0,0 @@ -export function to_string(buffer) { - return buffer.toString("utf8"); -} diff --git a/src/ffi_exceptions.mjs b/src/ffi_exceptions.mjs deleted file mode 100644 index 1f7718f..0000000 --- a/src/ffi_exceptions.mjs +++ /dev/null @@ -1,9 +0,0 @@ -import { Ok, Error } from "./gleam.mjs"; - -export function resultify(callback) { - try { - return new Ok(callback()); - } catch (err) { - return new Error(err); - } -} diff --git a/src/ffi_fs.mjs b/src/ffi_fs.mjs deleted file mode 100644 index 08c371b..0000000 --- a/src/ffi_fs.mjs +++ /dev/null @@ -1,5 +0,0 @@ -import { mkdirSync } from "node:fs"; - -export function mkdirP(path) { - return mkdirSync(path, { recursive: true }); -} diff --git a/src/gloss/builder.gleam b/src/gloss/builder.gleam index eff1af8..bd59055 100644 --- a/src/gloss/builder.gleam +++ b/src/gloss/builder.gleam @@ -1,3 +1,6 @@ +//// The builder contains convenience functions to interface with the different +//// parts of gloss, using the configuration to control what is done. + import gleam/result import gloss/parser import gloss/rendering/database as render_database @@ -5,24 +8,29 @@ import gloss/config.{type Configuration} import gloss/models/database.{type Database} import gloss/compiler.{type CompileDatabase} +/// Something failed when building the blog. pub type BuildError { ParseError(err: parser.ParseError) WriteError(err: config.WriteError) } +/// Parse the blog's input files. pub fn parse(config: Configuration) { config.parser() |> result.map_error(ParseError) } +/// Compile the post and page content into HTML strings. pub fn compile(db: Database, config: Configuration) { config.compiling.database_compiler(db, config.compiling.item_compiler) } +/// Render the content into HTML pages. pub fn render(db: Database, compiled: CompileDatabase, config: Configuration) { config.rendering.renderer(db, compiled, config) } +/// Write the render database into files. pub fn write(posts: render_database.RenderDatabase, config: Configuration) { config.writer(posts, config) |> result.map_error(WriteError) diff --git a/src/gloss/compiler.gleam b/src/gloss/compiler.gleam index 8722fe1..80e7d25 100644 --- a/src/gloss/compiler.gleam +++ b/src/gloss/compiler.gleam @@ -1,3 +1,6 @@ +//// Compiling means turning post and page content into HTML. By default this +//// content is Markdown that is compiled with Marked.js. + import gleam/option import gleam/dict.{type Dict} import gleam/list @@ -7,29 +10,36 @@ import gloss/models/page.{type Page} import gloss/models/database.{type Database, type PostID} import gloss/utils/marked +/// Compiled post content: strings that contain HTML. pub type PostContent { PostContent(full: String, short: option.Option(String)) } +/// A post and its compiled content. pub type CompiledPost { CompiledPost(orig: Post, content: PostContent) } +/// A page and its compiled content. pub type CompiledPage { CompiledPage(orig: Page, content: String) } +/// A function to compile the given string content into an HTML string. pub type Compiler = fn(String, Database) -> String +/// Structure where the compilation results are stored. pub type CompileDatabase { CompileDatabase(posts: Dict(PostID, CompiledPost), pages: List(CompiledPage)) } +/// The default compiler that uses Marked.js with default settings. pub fn default_compiler(content: String, _db: Database) { marked.default_parse(content) } +/// Compile contents of the database using the given compiler. pub fn compile(db: Database, compiler: Compiler) { CompileDatabase( posts: compile_posts(db, compiler), @@ -37,6 +47,7 @@ pub fn compile(db: Database, compiler: Compiler) { ) } +/// Compile all posts in the database using the given compiler. pub fn compile_posts( db: Database, compiler: Compiler, @@ -59,6 +70,7 @@ pub fn compile_posts( dict.from_list(posts) } +/// Compile all pages in the database using the given compiler. pub fn compile_pages(db: Database, compiler: Compiler) { database.pages(db) |> list.map(fn(page) { diff --git a/src/gloss/internal/utils/meta_url.gleam b/src/gloss/internal/utils/meta_url.gleam new file mode 100644 index 0000000..0f407cb --- /dev/null +++ b/src/gloss/internal/utils/meta_url.gleam @@ -0,0 +1,2 @@ +@external(javascript, "../../../ffi_meta_url.mjs", "metaURL") +pub fn get() -> String diff --git a/src/gloss/utils/object.gleam b/src/gloss/internal/utils/object.gleam similarity index 51% rename from src/gloss/utils/object.gleam rename to src/gloss/internal/utils/object.gleam index 67248e3..af7eda6 100644 --- a/src/gloss/utils/object.gleam +++ b/src/gloss/internal/utils/object.gleam @@ -1,10 +1,10 @@ pub type Object -@external(javascript, "../../ffi_object.mjs", "create") +@external(javascript, "../../../ffi_object.mjs", "create") pub fn new() -> Object -@external(javascript, "../../ffi_object.mjs", "set") +@external(javascript, "../../../ffi_object.mjs", "set") pub fn set(object object: Object, prop prop: String, value value: a) -> Object -@external(javascript, "../../ffi_object.mjs", "get") +@external(javascript, "../../../ffi_object.mjs", "get") pub fn get(object object: Object, prop prop: String) -> b diff --git a/src/gloss/utils/path.gleam b/src/gloss/internal/utils/path.gleam similarity index 100% rename from src/gloss/utils/path.gleam rename to src/gloss/internal/utils/path.gleam diff --git a/src/gloss/parser.gleam b/src/gloss/parser.gleam index 11776d0..1eddcf2 100644 --- a/src/gloss/parser.gleam +++ b/src/gloss/parser.gleam @@ -2,7 +2,7 @@ import gleam/result import gleam/list import gleam/regex import gleam/string -import gloss/utils/fs +import simplifile import gloss/models/database.{type Database} import gloss/parser/common import gloss/parser/post @@ -15,7 +15,7 @@ pub type Parser = fn() -> Result(Database, ParseError) pub type ParseError { - FileError(path: String, err: fs.FSError) + FileError(path: String, err: simplifile.FileError) PostParseError(filename: String, err: common.ParseError) MenuParseError } @@ -28,7 +28,7 @@ pub fn default_parse() -> Result(Database, ParseError) { pub fn parse_posts(db: Database, path: String) -> Result(Database, ParseError) { use filenames <- result.try( - fs.readdir(path) + simplifile.read_directory(path) |> result.map_error(fn(err) { FileError(path, err) }), ) @@ -48,7 +48,7 @@ pub fn parse_posts(db: Database, path: String) -> Result(Database, ParseError) { result.all( list.map(filenames, fn(file) { use contents <- result.try( - fs.read_file(path <> "/" <> file) + simplifile.read(path <> "/" <> file) |> result.map_error(fn(err) { FileError(file, err) }), ) @@ -63,7 +63,7 @@ pub fn parse_posts(db: Database, path: String) -> Result(Database, ParseError) { pub fn parse_pages(db: Database, path: String) -> Result(Database, ParseError) { use filenames <- result.try( - fs.readdir(path) + simplifile.read_directory(path) |> result.map_error(fn(err) { FileError(path, err) }), ) @@ -74,7 +74,7 @@ pub fn parse_pages(db: Database, path: String) -> Result(Database, ParseError) { }) |> list.map(fn(file) { use contents <- result.try( - fs.read_file(path <> "/" <> file) + simplifile.read(path <> "/" <> file) |> result.map_error(fn(err) { FileError(file, err) }), ) @@ -87,10 +87,10 @@ pub fn parse_pages(db: Database, path: String) -> Result(Database, ParseError) { } pub fn parse_menu(db: Database, file: String) -> Result(Database, ParseError) { - case fs.exists(file) { + case simplifile.verify_is_file(file) { Ok(True) -> { use contents <- result.try( - fs.read_file(file) + simplifile.read(file) |> result.map_error(fn(err) { FileError(file, err) }), ) diff --git a/src/gloss/utils/buffer.gleam b/src/gloss/utils/buffer.gleam deleted file mode 100644 index 01fd842..0000000 --- a/src/gloss/utils/buffer.gleam +++ /dev/null @@ -1,4 +0,0 @@ -pub type Buffer - -@external(javascript, "../../ffi_buffer.mjs", "to_string") -pub fn to_string(buf buf: Buffer) -> String diff --git a/src/gloss/utils/date.gleam b/src/gloss/utils/date.gleam index 9df18a3..b40fdf8 100644 --- a/src/gloss/utils/date.gleam +++ b/src/gloss/utils/date.gleam @@ -19,14 +19,15 @@ pub type Month { Dec } -/// All months in order +/// All months in order. pub const months = [Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec] -/// A date with 1-indexed years and days +/// A date with 1-indexed years and days. pub type Date { Date(year: Int, month: Month, day: Day) } +/// Parse an integer into a month. pub fn parse_month(month_int: Int) -> Result(Month, Nil) { case month_int { 1 -> Ok(Jan) @@ -45,6 +46,7 @@ pub fn parse_month(month_int: Int) -> Result(Month, Nil) { } } +/// Get the number of days in a month in a given year. pub fn days_in_month(month: Month, year: Int) { case month { Jan -> 31 @@ -77,6 +79,7 @@ pub fn days_in_month(month: Month, year: Int) { } } +/// Check if a given date is valid. pub fn is_valid_date(date: Date) -> Bool { let day = day.to_int(date.day) use <- bool.guard(day < 1, False) @@ -97,6 +100,7 @@ pub fn compare(a: Date, b: Date) -> Order { } } +/// Convert a month to a 1-indexed int. pub fn month_to_int(month: Month) -> Int { case month { Jan -> 1 @@ -114,6 +118,7 @@ pub fn month_to_int(month: Month) -> Int { } } +/// Convert a month to an English month name string. pub fn month_to_string(month: Month) -> String { case month { Jan -> "January" @@ -131,6 +136,7 @@ pub fn month_to_string(month: Month) -> String { } } +/// Format a date in the format "2 Aug 2024". pub fn format(date: Date) -> String { int.to_string(day.to_int(date.day)) <> " " @@ -139,6 +145,7 @@ pub fn format(date: Date) -> String { <> int.to_string(date.year) } +/// Format a date in the ISO 8601 format. pub fn format_iso(date: Date) -> String { int.to_string(date.year) <> "-" diff --git a/src/gloss/utils/exceptions.gleam b/src/gloss/utils/exceptions.gleam deleted file mode 100644 index 6c9d977..0000000 --- a/src/gloss/utils/exceptions.gleam +++ /dev/null @@ -1,10 +0,0 @@ -/// A result (of type a) from an external function that can also raise an error -/// (of type b). -pub type CanRaise(a, b) { - CanRaise(value: a, error: b) -} - -/// Convert a callback function (that should call an external function) from one -/// that can raise to one that will return a Result. -@external(javascript, "../../ffi_exceptions.mjs", "resultify") -pub fn resultify(callback callback: fn() -> CanRaise(a, b)) -> Result(a, b) diff --git a/src/gloss/utils/fs.gleam b/src/gloss/utils/fs.gleam deleted file mode 100644 index 730f653..0000000 --- a/src/gloss/utils/fs.gleam +++ /dev/null @@ -1,63 +0,0 @@ -import gleam/result -import gleam/list -import gleam/javascript/array.{type Array} -import gloss/utils/buffer.{type Buffer} -import gloss/utils/exceptions.{type CanRaise} - -pub type FSError - -pub fn read_file(path: String) -> Result(String, FSError) { - use contents <- result.try(exceptions.resultify(fn() { do_read_file(path) })) - - Ok(buffer.to_string(contents)) -} - -pub fn readdir(path: String) -> Result(List(String), FSError) { - use files <- result.try(exceptions.resultify(fn() { do_readdir(path) })) - - Ok(list.map(array.to_list(files), buffer.to_string)) -} - -pub fn mkdir(path: String) -> Result(Nil, FSError) { - use _undefined <- result.try(exceptions.resultify(fn() { do_mkdir(path) })) - - Ok(Nil) -} - -pub fn mkdir_p(path: String) -> Result(String, FSError) { - use created <- result.try(exceptions.resultify(fn() { do_mkdir_p(path) })) - - Ok(created) -} - -pub fn exists(path: String) -> Result(Bool, FSError) { - use created <- result.try(exceptions.resultify(fn() { do_exists(path) })) - - Ok(created) -} - -pub fn write_file(path: String, data: String) -> Result(Nil, FSError) { - use _undefined <- result.try( - exceptions.resultify(fn() { do_write_file(path, data) }), - ) - - Ok(Nil) -} - -@external(javascript, "fs", "readFileSync") -fn do_read_file(path: String) -> CanRaise(Buffer, FSError) - -@external(javascript, "fs", "readdirSync") -fn do_readdir(path: String) -> CanRaise(Array(Buffer), FSError) - -@external(javascript, "fs", "mkdirSync") -fn do_mkdir(path: String) -> CanRaise(Nil, FSError) - -@external(javascript, "../../ffi_fs.mjs", "mkdirP") -fn do_mkdir_p(path: String) -> CanRaise(String, FSError) - -@external(javascript, "fs", "existsSync") -fn do_exists(path: String) -> CanRaise(Bool, FSError) - -@external(javascript, "fs", "writeFileSync") -fn do_write_file(path: String, data: String) -> CanRaise(Nil, FSError) diff --git a/src/gloss/utils/ints/day.gleam b/src/gloss/utils/ints/day.gleam index 88d3f34..a84e99e 100644 --- a/src/gloss/utils/ints/day.gleam +++ b/src/gloss/utils/ints/day.gleam @@ -1,3 +1,5 @@ +//// A day is a ranged integer from 1 to 31. + import bigi.{type BigInt} import ranged_int/interface.{type Interface, Interface} diff --git a/src/gloss/utils/ints/hour.gleam b/src/gloss/utils/ints/hour.gleam index b807fcc..d173afb 100644 --- a/src/gloss/utils/ints/hour.gleam +++ b/src/gloss/utils/ints/hour.gleam @@ -1,3 +1,5 @@ +//// An hour is a ranged integer from 0 to 23. + import bigi.{type BigInt} import ranged_int/interface.{type Interface, Interface} diff --git a/src/gloss/utils/ints/minute.gleam b/src/gloss/utils/ints/minute.gleam index 85d7c47..ee6ccf1 100644 --- a/src/gloss/utils/ints/minute.gleam +++ b/src/gloss/utils/ints/minute.gleam @@ -1,3 +1,5 @@ +//// A minute is a ranged integer from 0 to 59. + import bigi.{type BigInt} import ranged_int/interface.{type Interface, Interface} diff --git a/src/gloss/utils/luxon.gleam b/src/gloss/utils/luxon.gleam index 290e84c..6082731 100644 --- a/src/gloss/utils/luxon.gleam +++ b/src/gloss/utils/luxon.gleam @@ -1,13 +1,18 @@ +//// Bindings to the Luxon library. + import gloss/utils/date.{type Date} import gloss/utils/time.{type Time} +/// Luxon DateTime. pub type DateTime +/// Get a Luxon DateTime in the given timezone. pub fn date_time_in_zone(date: Date, time: Time, tz: String) { let datetime_str = date.format_iso(date) <> "T" <> time.format(time) do_date_time_in_zone(datetime_str, tz) } +/// Get the current time as UTC. @external(javascript, "../../ffi_luxon.mjs", "utcNow") pub fn utc_now() -> DateTime @@ -17,8 +22,6 @@ fn do_date_time_in_zone( tz: String, ) -> Result(DateTime, Nil) -@external(javascript, "../../ffi_luxon.mjs", "toRFC2822") -pub fn to_rfc_2822(dt: DateTime) -> String - +/// Format the DateTime as an ISO 8601 format string. @external(javascript, "../../ffi_luxon.mjs", "toISO") pub fn to_iso(dt: DateTime) -> String diff --git a/src/gloss/utils/marked.gleam b/src/gloss/utils/marked.gleam index a6d15c4..baf01ef 100644 --- a/src/gloss/utils/marked.gleam +++ b/src/gloss/utils/marked.gleam @@ -1,24 +1,32 @@ -import gloss/utils/object.{type Object} +//// Bindings to the marked.js library. +import gloss/internal/utils/object.{type Object} + +/// Marked.js options object. pub type Options = Object +/// Create a new options object. pub fn new_options() -> Options { object.new() } +/// Set the `mangle` option on or off. pub fn set_mangle(options: Options, do_mangle: Bool) -> Options { object.set(options, "mangle", do_mangle) } +/// Set the `headerIds` option on or off. pub fn set_header_ids(options: Options, ids: Bool) -> Options { object.set(options, "headerIds", ids) } +/// Set the `headerPrefix` option. pub fn set_header_prefix(options: Options, prefix: String) -> Options { object.set(options, "headerPrefix", prefix) } +/// Parse a string containing Markdown using the default options. pub fn default_parse(content: String) -> String { let options = new_options() @@ -29,5 +37,6 @@ pub fn default_parse(content: String) -> String { parse(content, options) } +/// Parse a string containing Markdown using Marked.js. @external(javascript, "../../priv/vendor/marked.esm.mjs", "parse") pub fn parse(content content: String, options options: Options) -> String diff --git a/src/gloss/utils/meta_url.gleam b/src/gloss/utils/meta_url.gleam deleted file mode 100644 index fb8eec2..0000000 --- a/src/gloss/utils/meta_url.gleam +++ /dev/null @@ -1,2 +0,0 @@ -@external(javascript, "../../ffi_meta_url.mjs", "metaURL") -pub fn get() -> String diff --git a/src/gloss/utils/priv.gleam b/src/gloss/utils/priv.gleam index 5ca2b54..fd2f897 100644 --- a/src/gloss/utils/priv.gleam +++ b/src/gloss/utils/priv.gleam @@ -1,7 +1,8 @@ import gleam/uri -import gloss/utils/meta_url -import gloss/utils/path +import gloss/internal/utils/meta_url +import gloss/internal/utils/path +/// Get the path to the `priv` directory of `gloss`. pub fn path() -> String { let assert Ok(meta_url) = uri.parse(meta_url.get()) diff --git a/src/gloss/utils/string.gleam b/src/gloss/utils/string.gleam deleted file mode 100644 index 4df7b43..0000000 --- a/src/gloss/utils/string.gleam +++ /dev/null @@ -1,9 +0,0 @@ -import gleam/string - -/// Split the given string at the given index -pub fn split_at(str: String, index: Int) -> #(String, String) { - let len = string.length(str) - let first = string.slice(str, 0, index) - let rest = string.slice(str, index, len - index) - #(first, rest) -} diff --git a/src/gloss/utils/time.gleam b/src/gloss/utils/time.gleam index 6606afc..e1e72dd 100644 --- a/src/gloss/utils/time.gleam +++ b/src/gloss/utils/time.gleam @@ -16,6 +16,7 @@ pub fn compare(a: Time, b: Time) -> Order { } } +/// Parse a time from an `hh:mm` format string. pub fn parse(str: String) { case string.split(str, ":") { [hours, minutes] -> @@ -31,12 +32,14 @@ pub fn parse(str: String) { } } +/// Format a time to an `hh:mm` format string. pub fn format(time: Time) -> String { pad(int.to_string(hour.to_int(time.hours))) <> ":" <> pad(int.to_string(minute.to_int(time.minutes))) } +/// Get a time at hour 0 and minute 0. pub fn nil_time() { let assert Ok(h) = hour.from_int(0) let assert Ok(m) = minute.from_int(0) diff --git a/src/gloss/utils/uniqid.gleam b/src/gloss/utils/uniqid.gleam index 7f655b3..ef10d85 100644 --- a/src/gloss/utils/uniqid.gleam +++ b/src/gloss/utils/uniqid.gleam @@ -1,16 +1,20 @@ import bigi.{type BigInt} +/// A unique ID. pub type UniqID = BigInt +/// Unique ID generator. pub opaque type Generator { Generator(id: BigInt) } +/// Get a new generator. pub fn new() -> Generator { Generator(bigi.zero()) } +/// Get a unique ID and a new generator, that can be used to generate a new ID. pub fn get(gen: Generator) -> #(UniqID, Generator) { let new = bigi.add(gen.id, bigi.from_int(1)) #(new, Generator(new)) diff --git a/src/gloss/writer.gleam b/src/gloss/writer.gleam index bf9a775..72a7518 100644 --- a/src/gloss/writer.gleam +++ b/src/gloss/writer.gleam @@ -7,11 +7,12 @@ import lustre/ssg/xml import lustre/element import gloss/rendering/database.{type RenderDatabase} as _ import gloss/config.{type Configuration, WriteError} +import gloss/utils/priv pub fn write(db: RenderDatabase, config: Configuration) { let site = ssg.new(config.output_path) - |> ssg.add_static_dir("./build/dev/javascript/gloss/priv/assets") + |> ssg.add_static_dir(priv.path() <> "/assets") let single_posts = db.single_posts