Update deps; WIP on comments

This commit is contained in:
2025-01-23 22:11:12 -05:00
parent 88841fd3f8
commit dc30716b83
15 changed files with 1454 additions and 1110 deletions
+154 -144
View File
@@ -3,29 +3,29 @@
open MyWebLog
open NodaTime
/// A category under which a post may be identified
/// <summary>A category under which a post may be identified</summary>
[<CLIMutable; NoComparison; NoEquality>]
type Category = {
/// The ID of the category
/// <summary>The ID of the category</summary>
Id: CategoryId
/// The ID of the web log to which the category belongs
/// <summary>The ID of the web log to which the category belongs</summary>
WebLogId: WebLogId
/// The displayed name
/// <summary>The displayed name</summary>
Name: string
/// The slug (used in category URLs)
/// <summary>The slug (used in category URLs)</summary>
Slug: string
/// A longer description of the category
/// <summary>A longer description of the category</summary>
Description: string option
/// The parent ID of this category (if a subcategory)
/// <summary>The parent ID of this category (if a subcategory)</summary>
ParentId: CategoryId option
} with
/// An empty category
/// <summary>An empty category</summary>
static member Empty =
{ Id = CategoryId.Empty
WebLogId = WebLogId.Empty
@@ -35,38 +35,38 @@ type Category = {
ParentId = None }
/// A comment on a post
/// <summary>A comment on a post</summary>
[<CLIMutable; NoComparison; NoEquality>]
type Comment = {
/// The ID of the comment
/// <summary>The ID of the comment</summary>
Id: CommentId
/// The ID of the post to which this comment applies
/// <summary>The ID of the post to which this comment applies</summary>
PostId: PostId
/// The ID of the comment to which this comment is a reply
/// <summary>The ID of the comment to which this comment is a reply</summary>
InReplyToId: CommentId option
/// The name of the commentor
/// <summary>The name of the commentor</summary>
Name: string
/// The e-mail address of the commentor
/// <summary>The e-mail address of the commentor</summary>
Email: string
/// The URL of the commentor's personal website
/// <summary>The URL of the commentor's personal website</summary>
Url: string option
/// The status of the comment
/// <summary>The status of the comment</summary>
Status: CommentStatus
/// When the comment was posted
/// <summary>When the comment was posted</summary>
PostedOn: Instant
/// The text of the comment
/// <summary>The text of the comment</summary>
Text: string
} with
/// An empty comment
/// <summary>An empty comment</summary>
static member Empty =
{ Id = CommentId.Empty
PostId = PostId.Empty
@@ -79,50 +79,50 @@ type Comment = {
Text = "" }
/// A page (text not associated with a date/time)
/// <summary>A page (text not associated with a date/time)</summary>
[<CLIMutable; NoComparison; NoEquality>]
type Page = {
/// The ID of this page
/// <summary>The ID of this page</summary>
Id: PageId
/// The ID of the web log to which this page belongs
/// <summary>The ID of the web log to which this page belongs</summary>
WebLogId: WebLogId
/// The ID of the author of this page
/// <summary>The ID of the author of this page</summary>
AuthorId: WebLogUserId
/// The title of the page
/// <summary>The title of the page</summary>
Title: string
/// The link at which this page is displayed
/// <summary>The link at which this page is displayed</summary>
Permalink: Permalink
/// When this page was published
/// <summary>When this page was published</summary>
PublishedOn: Instant
/// When this page was last updated
/// <summary>When this page was last updated</summary>
UpdatedOn: Instant
/// Whether this page shows as part of the web log's navigation
/// <summary>Whether this page shows as part of the web log's navigation</summary>
IsInPageList: bool
/// The template to use when rendering this page
/// <summary>The template to use when rendering this page</summary>
Template: string option
/// The current text of the page
/// <summary>The current text of the page</summary>
Text: string
/// Metadata for this page
/// <summary>Metadata for this page</summary>
Metadata: MetaItem list
/// Permalinks at which this page may have been previously served (useful for migrated content)
/// <summary>Permalinks at which this page may have been previously served (useful for migrated content)</summary>
PriorPermalinks: Permalink list
/// Revisions of this page
/// <summary>Revisions of this page</summary>
Revisions: Revision list
} with
/// An empty page
/// <summary>An empty page</summary>
static member Empty =
{ Id = PageId.Empty
WebLogId = WebLogId.Empty
@@ -139,59 +139,59 @@ type Page = {
Revisions = [] }
/// A web log post
/// <summary>A web log post</summary>
[<CLIMutable; NoComparison; NoEquality>]
type Post = {
/// The ID of this post
/// <summary>The ID of this post</summary>
Id: PostId
/// The ID of the web log to which this post belongs
/// <summary>The ID of the web log to which this post belongs</summary>
WebLogId: WebLogId
/// The ID of the author of this post
/// <summary>The ID of the author of this post</summary>
AuthorId: WebLogUserId
/// The status
/// <summary>The status</summary>
Status: PostStatus
/// The title
/// <summary>The title</summary>
Title: string
/// The link at which the post resides
/// <summary>The link at which the post resides</summary>
Permalink: Permalink
/// The instant on which the post was originally published
/// <summary>The instant on which the post was originally published</summary>
PublishedOn: Instant option
/// The instant on which the post was last updated
/// <summary>The instant on which the post was last updated</summary>
UpdatedOn: Instant
/// The template to use in displaying the post
/// <summary>The template to use in displaying the post</summary>
Template: string option
/// The text of the post in HTML (ready to display) format
/// <summary>The text of the post in HTML (ready to display) format</summary>
Text: string
/// The Ids of the categories to which this is assigned
/// <summary>The Ids of the categories to which this is assigned</summary>
CategoryIds: CategoryId list
/// The tags for the post
/// <summary>The tags for the post</summary>
Tags: string list
/// Podcast episode information for this post
/// <summary>Podcast episode information for this post</summary>
Episode: Episode option
/// Metadata for the post
/// <summary>Metadata for the post</summary>
Metadata: MetaItem list
/// Permalinks at which this post may have been previously served (useful for migrated content)
/// <summary>Permalinks at which this post may have been previously served (useful for migrated content)</summary>
PriorPermalinks: Permalink list
/// The revisions for this post
/// <summary>The revisions for this post</summary>
Revisions: Revision list
} with
/// An empty post
/// <summary>An empty post</summary>
static member Empty =
{ Id = PostId.Empty
WebLogId = WebLogId.Empty
@@ -211,136 +211,138 @@ type Post = {
Revisions = [] }
/// <summary>
/// A mapping between a tag and its URL value, used to translate restricted characters (ex. "#1" -> "number-1")
/// </summary>
[<CLIMutable; NoComparison; NoEquality>]
type TagMap = {
/// The ID of this tag mapping
/// <summary>The ID of this tag mapping</summary>
Id: TagMapId
/// The ID of the web log to which this tag mapping belongs
/// <summary>The ID of the web log to which this tag mapping belongs</summary>
WebLogId: WebLogId
/// The tag which should be mapped to a different value in links
/// <summary>The tag which should be mapped to a different value in links</summary>
Tag: string
/// The value by which the tag should be linked
/// <summary>The value by which the tag should be linked</summary>
UrlValue: string
} with
/// An empty tag mapping
/// <summary>An empty tag mapping</summary>
static member Empty =
{ Id = TagMapId.Empty; WebLogId = WebLogId.Empty; Tag = ""; UrlValue = "" }
/// A theme
/// <summary>A theme</summary>
[<CLIMutable; NoComparison; NoEquality>]
type Theme = {
/// The ID / path of the theme
/// <summary>The ID / path of the theme</summary>
Id: ThemeId
/// A long name of the theme
/// <summary>A long name of the theme</summary>
Name: string
/// The version of the theme
/// <summary>The version of the theme</summary>
Version: string
/// The templates for this theme
/// <summary>The templates for this theme</summary>
Templates: ThemeTemplate list
} with
/// An empty theme
/// <summary>An empty theme</summary>
static member Empty =
{ Id = ThemeId.Empty; Name = ""; Version = ""; Templates = [] }
/// A theme asset (a file served as part of a theme, at /themes/[theme]/[asset-path])
/// <summary>A theme asset (a file served as part of a theme, at /themes/[theme]/[asset-path])</summary>
[<CLIMutable; NoComparison; NoEquality>]
type ThemeAsset = {
/// The ID of the asset (consists of theme and path)
/// <summary>The ID of the asset (consists of theme and path)</summary>
Id: ThemeAssetId
/// The updated date (set from the file date from the ZIP archive)
/// <summary>The updated date (set from the file date from the ZIP archive)</summary>
UpdatedOn: Instant
/// The data for the asset
/// <summary>The data for the asset</summary>
Data: byte array
} with
/// An empty theme asset
/// <summary>An empty theme asset</summary>
static member Empty =
{ Id = ThemeAssetId.Empty; UpdatedOn = Noda.epoch; Data = [||] }
/// An uploaded file
/// <summary>An uploaded file</summary>
[<CLIMutable; NoComparison; NoEquality>]
type Upload = {
/// The ID of the upload
/// <summary>The ID of the upload</summary>
Id: UploadId
/// The ID of the web log to which this upload belongs
/// <summary>The ID of the web log to which this upload belongs</summary>
WebLogId: WebLogId
/// The link at which this upload is served
/// <summary>The link at which this upload is served</summary>
Path: Permalink
/// The updated date/time for this upload
/// <summary>The updated date/time for this upload</summary>
UpdatedOn: Instant
/// The data for the upload
/// <summary>The data for the upload</summary>
Data: byte array
} with
/// An empty upload
/// <summary>An empty upload</summary>
static member Empty =
{ Id = UploadId.Empty; WebLogId = WebLogId.Empty; Path = Permalink.Empty; UpdatedOn = Noda.epoch; Data = [||] }
open Newtonsoft.Json
/// A web log
/// <summary>A web log</summary>
[<CLIMutable; NoComparison; NoEquality>]
type WebLog = {
/// The ID of the web log
/// <summary>The ID of the web log</summary>
Id: WebLogId
/// The name of the web log
/// <summary>The name of the web log</summary>
Name: string
/// The slug of the web log
/// <summary>The slug of the web log</summary>
Slug: string
/// A subtitle for the web log
/// <summary>A subtitle for the web log</summary>
Subtitle: string option
/// The default page ("posts" or a page Id)
/// <summary>The default page ("posts" or a page Id)</summary>
DefaultPage: string
/// The number of posts to display on pages of posts
/// <summary>The number of posts to display on pages of posts</summary>
PostsPerPage: int
/// The ID of the theme (also the path within /themes)
/// <summary>The ID of the theme (also the path within /themes)</summary>
ThemeId: ThemeId
/// The URL base
/// <summary>The URL base</summary>
UrlBase: string
/// The time zone in which dates/times should be displayed
/// <summary>The time zone in which dates/times should be displayed</summary>
TimeZone: string
/// The RSS options for this web log
/// <summary>The RSS options for this web log</summary>
Rss: RssOptions
/// Whether to automatically load htmx
/// <summary>Whether to automatically load htmx</summary>
AutoHtmx: bool
/// Where uploads are placed
/// <summary>Where uploads are placed</summary>
Uploads: UploadDestination
/// Redirect rules for this weblog
/// <summary>Redirect rules for this weblog</summary>
RedirectRules: RedirectRule list
} with
/// An empty web log
/// <summary>An empty web log</summary>
static member Empty =
{ Id = WebLogId.Empty
Name = ""
@@ -355,8 +357,10 @@ type WebLog = {
AutoHtmx = false
Uploads = Database
RedirectRules = [] }
/// <summary>
/// Any extra path where this web log is hosted (blank if web log is hosted at the root of the domain)
/// </summary>
[<JsonIgnore>]
member this.ExtraPath =
let pathParts = this.UrlBase.Split "://"
@@ -365,16 +369,22 @@ type WebLog = {
else
let path = pathParts[1].Split "/"
if path.Length > 1 then $"""/{path |> Array.skip 1 |> String.concat "/"}""" else ""
/// Generate an absolute URL for the given link
/// <summary>Generate an absolute URL for the given link</summary>
/// <param name="permalink">The permalink for which an absolute URL should be generated</param>
/// <returns>An absolute URL for the given link</returns>
member this.AbsoluteUrl(permalink: Permalink) =
$"{this.UrlBase}/{permalink}"
/// Generate a relative URL for the given link
/// <summary>Generate a relative URL for the given link</summary>
/// <param name="permalink">The permalink for which a relative URL should be generated</param>
/// <returns>A relative URL for the given link</returns>
member this.RelativeUrl(permalink: Permalink) =
$"{this.ExtraPath}/{permalink}"
/// Convert an Instant (UTC reference) to the web log's local date/time
/// <summary>Convert an Instant (UTC reference) to the web log's local date/time</summary>
/// <param name="date">The UTC <c>Instant</c> to be converted</param>
/// <returns>The local date/time for this web log</returns>
member this.LocalTime(date: Instant) =
DateTimeZoneProviders.Tzdb.GetZoneOrNull this.TimeZone
|> Option.ofObj
@@ -382,44 +392,44 @@ type WebLog = {
|> Option.defaultValue (date.ToDateTimeUtc())
/// A user of the web log
/// <summary>A user of the web log</summary>
[<CLIMutable; NoComparison; NoEquality>]
type WebLogUser = {
/// The ID of the user
/// <summary>The ID of the user</summary>
Id: WebLogUserId
/// The ID of the web log to which this user belongs
/// <summary>The ID of the web log to which this user belongs</summary>
WebLogId: WebLogId
/// The user name (e-mail address)
/// <summary>The user name (e-mail address)</summary>
Email: string
/// The user's first name
/// <summary>The user's first name</summary>
FirstName: string
/// The user's last name
/// <summary>The user's last name</summary>
LastName: string
/// The user's preferred name
/// <summary>The user's preferred name</summary>
PreferredName: string
/// The hash of the user's password
/// <summary>The hash of the user's password</summary>
PasswordHash: string
/// The URL of the user's personal site
/// <summary>The URL of the user's personal site</summary>
Url: string option
/// The user's access level
/// <summary>The user's access level</summary>
AccessLevel: AccessLevel
/// When the user was created
/// <summary>When the user was created</summary>
CreatedOn: Instant
/// When the user last logged on
/// <summary>When the user last logged on</summary>
LastSeenOn: Instant option
} with
/// An empty web log user
/// <summary>An empty web log user</summary>
static member Empty =
{ Id = WebLogUserId.Empty
WebLogId = WebLogId.Empty
@@ -432,8 +442,8 @@ type WebLogUser = {
AccessLevel = Author
CreatedOn = Noda.epoch
LastSeenOn = None }
/// Get the user's displayed name
/// <summary>Get the user's displayed name</summary>
[<JsonIgnore>]
member this.DisplayName =
(seq { (match this.PreferredName with "" -> this.FirstName | n -> n); " "; this.LastName }
+3 -3
View File
@@ -7,11 +7,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Markdig" Version="0.39.1" />
<PackageReference Include="Markdig" Version="0.40.0" />
<PackageReference Include="Markdown.ColorCode" Version="2.3.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NodaTime" Version="3.2.0" />
<PackageReference Update="FSharp.Core" Version="9.0.100" />
<PackageReference Include="NodaTime" Version="3.2.1" />
<PackageReference Update="FSharp.Core" Version="9.0.101" />
</ItemGroup>
</Project>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff