From 6ec4103a3a84117a4072cc7ba0af38b0fadca818 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sun, 5 May 2024 02:18:20 +0100 Subject: [PATCH 01/12] refactor: separate Markdig extensions from project Also introduces .Common project to house common references and types --- .../Data/Blog/IBlogAuthor.cs | 2 +- .../Data/Blog/IBlogPost.cs | 2 +- .../Data/Blog/ILegacyComment.cs | 2 +- .../Data/Blog/IUser.cs | 2 +- .../Data/Mastodon/AttachmentType.cs | 2 +- .../Data/Mastodon/IMastodonStatus.cs | 31 +++ .../Data/Mastodon/MediaAttachment.cs | 2 +- .../Data/Visibility.cs | 2 +- .../Data/Web/BookState.cs | 2 +- .../Data/Web/IBlacklistEntry.cs | 2 +- .../Data/Web/IBook.cs | 2 +- .../Data/Web/ICodeSnippet.cs | 2 +- .../Data/Web/IProgrammingLanguage.cs | 2 +- .../Data/Web/IProject.cs | 2 +- .../Data/Web/ITemplate.cs | 2 +- .../Data/Web/ITutorialArticle.cs | 2 +- .../Data/Web/ITutorialFolder.cs | 2 +- .../Data/Web/ProjectStatus.cs | 2 +- OliverBooth.Common/OliverBooth.Common.csproj | 16 ++ .../Services/IBlogPostService.cs | 4 +- .../Services/IBlogUserService.cs | 4 +- .../Services/ICodeSnippetService.cs | 4 +- .../Services/IContactService.cs | 4 +- .../Services/IMastodonService.cs | 6 +- .../Services/IProgrammingLanguageService.cs | 14 ++ .../Services/IProjectService.cs | 4 +- .../Services/IReadingListService.cs | 4 +- .../Services/ITutorialService.cs | 8 +- .../Markdown/Callout/CalloutBlock.cs | 43 +++++ .../Markdown/Callout/CalloutExtension.cs | 32 ++++ .../Markdown/Callout/CalloutInlineParser.cs | 176 ++++++++++++++++++ .../Markdown/Callout/CalloutRenderer.cs | 119 ++++++++++++ .../Markdown/MarkdownExtensions.cs | 21 +++ .../Markdown/Template/TemplateExtension.cs | 4 +- .../Markdown/Template/TemplateInline.cs | 2 +- .../Markdown/Template/TemplateInlineParser.cs | 2 +- .../Markdown/Template/TemplateRenderer.cs | 4 +- .../Markdown/Timestamp/TimestampExtension.cs | 25 +++ .../Markdown/Timestamp/TimestampFormat.cs | 42 +++++ .../Markdown/Timestamp/TimestampInline.cs | 21 +++ .../Timestamp/TimestampInlineParser.cs | 91 +++++++++ .../Markdown/Timestamp/TimestampRenderer.cs | 55 ++++++ .../MarkdownPipelineExtensions.cs | 38 ++++ .../OliverBooth.Extensions.Markdig.csproj | 13 ++ .../Services/ITemplateService.cs | 6 +- OliverBooth.sln | 12 ++ .../Controllers/Blog/BlogApiController.cs | 4 +- OliverBooth/Controllers/Blog/RssController.cs | 4 +- OliverBooth/Controllers/FormattedBlacklist.cs | 4 +- OliverBooth/Data/Blog/BlogPost.cs | 2 + .../Configuration/BlogPostConfiguration.cs | 1 + OliverBooth/Data/Blog/LegacyComment.cs | 1 + OliverBooth/Data/Blog/User.cs | 1 + OliverBooth/Data/Mastodon/MastodonStatus.cs | 26 +-- OliverBooth/Data/Web/BlacklistEntry.cs | 2 + OliverBooth/Data/Web/Book.cs | 1 + OliverBooth/Data/Web/CodeSnippet.cs | 2 + .../Web/Configuration/BookConfiguration.cs | 1 + .../Web/Configuration/ProjectConfiguration.cs | 1 + .../TutorialArticleConfiguration.cs | 1 + .../TutorialFolderConfiguration.cs | 1 + OliverBooth/Data/Web/ProgrammingLanguage.cs | 2 + OliverBooth/Data/Web/Project.cs | 2 + OliverBooth/Data/Web/Template.cs | 2 + OliverBooth/Data/Web/TutorialArticle.cs | 2 + OliverBooth/Data/Web/TutorialFolder.cs | 3 + OliverBooth/Extensions/HtmlUtility.cs | 6 +- .../Template/CodeSnippetTemplateRenderer.cs | 5 +- .../Template/CustomTemplateRenderer.cs | 1 + OliverBooth/OliverBooth.csproj | 9 +- OliverBooth/Pages/Blog/Article.cshtml | 7 +- OliverBooth/Pages/Blog/Article.cshtml.cs | 4 +- OliverBooth/Pages/Blog/Index.cshtml | 18 +- OliverBooth/Pages/Blog/Index.cshtml.cs | 4 +- OliverBooth/Pages/Blog/RawArticle.cshtml.cs | 4 +- OliverBooth/Pages/Books.cshtml | 2 +- OliverBooth/Pages/Books.cshtml.cs | 4 +- OliverBooth/Pages/Contact/Blacklist.cshtml | 5 +- OliverBooth/Pages/Contact/Index.cshtml | 1 + OliverBooth/Pages/Contact/Result.cshtml | 1 + OliverBooth/Pages/Index.cshtml | 1 + .../Pages/Privacy/FiveOClockSomewhere.cshtml | 1 + OliverBooth/Pages/Privacy/GooglePlay.cshtml | 1 + OliverBooth/Pages/Privacy/Index.cshtml | 1 + OliverBooth/Pages/Projects/Index.cshtml | 5 +- OliverBooth/Pages/Projects/Project.cshtml | 5 +- OliverBooth/Pages/Projects/Project.cshtml.cs | 4 +- OliverBooth/Pages/Shared/_Layout.cshtml | 7 +- .../Pages/Shared/_MinimalLayout.cshtml | 1 + OliverBooth/Pages/Tutorials/Article.cshtml | 8 +- OliverBooth/Pages/Tutorials/Article.cshtml.cs | 4 +- OliverBooth/Pages/Tutorials/Index.cshtml | 7 +- OliverBooth/Pages/Tutorials/Index.cshtml.cs | 4 +- OliverBooth/Program.cs | 7 +- OliverBooth/Services/BlogPostService.cs | 4 +- OliverBooth/Services/BlogUserService.cs | 2 + OliverBooth/Services/CodeSnippetService.cs | 2 + OliverBooth/Services/ContactService.cs | 2 + OliverBooth/Services/MastodonService.cs | 4 +- .../Services/ProgrammingLanguageService.cs | 14 +- OliverBooth/Services/ProjectService.cs | 2 + OliverBooth/Services/ReadingListService.cs | 2 + OliverBooth/Services/TemplateService.cs | 5 +- OliverBooth/Services/TutorialService.cs | 5 +- 104 files changed, 932 insertions(+), 139 deletions(-) rename {OliverBooth => OliverBooth.Common}/Data/Blog/IBlogAuthor.cs (95%) rename {OliverBooth => OliverBooth.Common}/Data/Blog/IBlogPost.cs (98%) rename {OliverBooth => OliverBooth.Common}/Data/Blog/ILegacyComment.cs (97%) rename {OliverBooth => OliverBooth.Common}/Data/Blog/IUser.cs (97%) rename {OliverBooth => OliverBooth.Common}/Data/Mastodon/AttachmentType.cs (66%) create mode 100644 OliverBooth.Common/Data/Mastodon/IMastodonStatus.cs rename {OliverBooth => OliverBooth.Common}/Data/Mastodon/MediaAttachment.cs (92%) rename {OliverBooth => OliverBooth.Common}/Data/Visibility.cs (94%) rename {OliverBooth => OliverBooth.Common}/Data/Web/BookState.cs (90%) rename {OliverBooth => OliverBooth.Common}/Data/Web/IBlacklistEntry.cs (93%) rename {OliverBooth => OliverBooth.Common}/Data/Web/IBook.cs (95%) rename {OliverBooth => OliverBooth.Common}/Data/Web/ICodeSnippet.cs (93%) rename {OliverBooth => OliverBooth.Common}/Data/Web/IProgrammingLanguage.cs (92%) rename {OliverBooth => OliverBooth.Common}/Data/Web/IProject.cs (98%) rename {OliverBooth => OliverBooth.Common}/Data/Web/ITemplate.cs (92%) rename {OliverBooth => OliverBooth.Common}/Data/Web/ITutorialArticle.cs (98%) rename {OliverBooth => OliverBooth.Common}/Data/Web/ITutorialFolder.cs (96%) rename {OliverBooth => OliverBooth.Common}/Data/Web/ProjectStatus.cs (92%) create mode 100644 OliverBooth.Common/OliverBooth.Common.csproj rename {OliverBooth => OliverBooth.Common}/Services/IBlogPostService.cs (98%) rename {OliverBooth => OliverBooth.Common}/Services/IBlogUserService.cs (90%) rename {OliverBooth => OliverBooth.Common}/Services/ICodeSnippetService.cs (95%) rename {OliverBooth => OliverBooth.Common}/Services/IContactService.cs (77%) rename {OliverBooth => OliverBooth.Common}/Services/IMastodonService.cs (59%) create mode 100644 OliverBooth.Common/Services/IProgrammingLanguageService.cs rename {OliverBooth => OliverBooth.Common}/Services/IProjectService.cs (97%) rename {OliverBooth => OliverBooth.Common}/Services/IReadingListService.cs (85%) rename {OliverBooth => OliverBooth.Common}/Services/ITutorialService.cs (97%) create mode 100644 OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutBlock.cs create mode 100644 OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutExtension.cs create mode 100644 OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutInlineParser.cs create mode 100644 OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutRenderer.cs create mode 100644 OliverBooth.Extensions.Markdig/Markdown/MarkdownExtensions.cs rename {OliverBooth => OliverBooth.Extensions.Markdig}/Markdown/Template/TemplateExtension.cs (90%) rename {OliverBooth => OliverBooth.Extensions.Markdig}/Markdown/Template/TemplateInline.cs (95%) rename {OliverBooth => OliverBooth.Extensions.Markdig}/Markdown/Template/TemplateInlineParser.cs (99%) rename {OliverBooth => OliverBooth.Extensions.Markdig}/Markdown/Template/TemplateRenderer.cs (88%) create mode 100644 OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampExtension.cs create mode 100644 OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampFormat.cs create mode 100644 OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampInline.cs create mode 100644 OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampInlineParser.cs create mode 100644 OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampRenderer.cs create mode 100644 OliverBooth.Extensions.Markdig/MarkdownPipelineExtensions.cs create mode 100644 OliverBooth.Extensions.Markdig/OliverBooth.Extensions.Markdig.csproj rename {OliverBooth => OliverBooth.Extensions.Markdig}/Services/ITemplateService.cs (94%) diff --git a/OliverBooth/Data/Blog/IBlogAuthor.cs b/OliverBooth.Common/Data/Blog/IBlogAuthor.cs similarity index 95% rename from OliverBooth/Data/Blog/IBlogAuthor.cs rename to OliverBooth.Common/Data/Blog/IBlogAuthor.cs index 3f1c3e1..279da26 100644 --- a/OliverBooth/Data/Blog/IBlogAuthor.cs +++ b/OliverBooth.Common/Data/Blog/IBlogAuthor.cs @@ -1,4 +1,4 @@ -namespace OliverBooth.Data.Blog; +namespace OliverBooth.Common.Data.Blog; /// /// Represents the author of a blog post. diff --git a/OliverBooth/Data/Blog/IBlogPost.cs b/OliverBooth.Common/Data/Blog/IBlogPost.cs similarity index 98% rename from OliverBooth/Data/Blog/IBlogPost.cs rename to OliverBooth.Common/Data/Blog/IBlogPost.cs index bc1a2e5..093220c 100644 --- a/OliverBooth/Data/Blog/IBlogPost.cs +++ b/OliverBooth.Common/Data/Blog/IBlogPost.cs @@ -1,4 +1,4 @@ -namespace OliverBooth.Data.Blog; +namespace OliverBooth.Common.Data.Blog; /// /// Represents a blog post. diff --git a/OliverBooth/Data/Blog/ILegacyComment.cs b/OliverBooth.Common/Data/Blog/ILegacyComment.cs similarity index 97% rename from OliverBooth/Data/Blog/ILegacyComment.cs rename to OliverBooth.Common/Data/Blog/ILegacyComment.cs index 2f43dc2..2a5cc99 100644 --- a/OliverBooth/Data/Blog/ILegacyComment.cs +++ b/OliverBooth.Common/Data/Blog/ILegacyComment.cs @@ -1,4 +1,4 @@ -namespace OliverBooth.Data.Blog; +namespace OliverBooth.Common.Data.Blog; /// /// Represents a comment that was posted on a legacy comment framework. diff --git a/OliverBooth/Data/Blog/IUser.cs b/OliverBooth.Common/Data/Blog/IUser.cs similarity index 97% rename from OliverBooth/Data/Blog/IUser.cs rename to OliverBooth.Common/Data/Blog/IUser.cs index 912db14..80af1b8 100644 --- a/OliverBooth/Data/Blog/IUser.cs +++ b/OliverBooth.Common/Data/Blog/IUser.cs @@ -1,4 +1,4 @@ -namespace OliverBooth.Data.Blog; +namespace OliverBooth.Common.Data.Blog; /// /// Represents a user which can log in to the blog. diff --git a/OliverBooth/Data/Mastodon/AttachmentType.cs b/OliverBooth.Common/Data/Mastodon/AttachmentType.cs similarity index 66% rename from OliverBooth/Data/Mastodon/AttachmentType.cs rename to OliverBooth.Common/Data/Mastodon/AttachmentType.cs index 6d0b5e1..701db4e 100644 --- a/OliverBooth/Data/Mastodon/AttachmentType.cs +++ b/OliverBooth.Common/Data/Mastodon/AttachmentType.cs @@ -1,4 +1,4 @@ -namespace OliverBooth.Data.Mastodon; +namespace OliverBooth.Common.Data.Mastodon; public enum AttachmentType { diff --git a/OliverBooth.Common/Data/Mastodon/IMastodonStatus.cs b/OliverBooth.Common/Data/Mastodon/IMastodonStatus.cs new file mode 100644 index 0000000..f7bdc95 --- /dev/null +++ b/OliverBooth.Common/Data/Mastodon/IMastodonStatus.cs @@ -0,0 +1,31 @@ +namespace OliverBooth.Common.Data.Mastodon; + +/// +/// Represents a status on Mastodon. +/// +public interface IMastodonStatus +{ + /// + /// Gets the content of the status. + /// + /// The content. + string Content { get; } + + /// + /// Gets the date and time at which this status was posted. + /// + /// The post timestamp. + DateTimeOffset CreatedAt { get; } + + /// + /// Gets the media attachments for this status. + /// + /// The media attachments. + IReadOnlyList MediaAttachments { get; } + + /// + /// Gets the original URI of the status. + /// + /// The original URI. + Uri OriginalUri { get; } +} diff --git a/OliverBooth/Data/Mastodon/MediaAttachment.cs b/OliverBooth.Common/Data/Mastodon/MediaAttachment.cs similarity index 92% rename from OliverBooth/Data/Mastodon/MediaAttachment.cs rename to OliverBooth.Common/Data/Mastodon/MediaAttachment.cs index ddc3307..117be81 100644 --- a/OliverBooth/Data/Mastodon/MediaAttachment.cs +++ b/OliverBooth.Common/Data/Mastodon/MediaAttachment.cs @@ -1,4 +1,4 @@ -namespace OliverBooth.Data.Mastodon; +namespace OliverBooth.Common.Data.Mastodon; public sealed class MediaAttachment { diff --git a/OliverBooth/Data/Visibility.cs b/OliverBooth.Common/Data/Visibility.cs similarity index 94% rename from OliverBooth/Data/Visibility.cs rename to OliverBooth.Common/Data/Visibility.cs index 17b3e87..5bd0e92 100644 --- a/OliverBooth/Data/Visibility.cs +++ b/OliverBooth.Common/Data/Visibility.cs @@ -1,4 +1,4 @@ -namespace OliverBooth.Data; +namespace OliverBooth.Common.Data; /// /// An enumeration of the possible visibilities of a blog post. diff --git a/OliverBooth/Data/Web/BookState.cs b/OliverBooth.Common/Data/Web/BookState.cs similarity index 90% rename from OliverBooth/Data/Web/BookState.cs rename to OliverBooth.Common/Data/Web/BookState.cs index 637415a..29b8312 100644 --- a/OliverBooth/Data/Web/BookState.cs +++ b/OliverBooth.Common/Data/Web/BookState.cs @@ -1,4 +1,4 @@ -namespace OliverBooth.Data.Web; +namespace OliverBooth.Common.Data.Web; /// /// Represents the state of a book. diff --git a/OliverBooth/Data/Web/IBlacklistEntry.cs b/OliverBooth.Common/Data/Web/IBlacklistEntry.cs similarity index 93% rename from OliverBooth/Data/Web/IBlacklistEntry.cs rename to OliverBooth.Common/Data/Web/IBlacklistEntry.cs index 00516fb..6c55acc 100644 --- a/OliverBooth/Data/Web/IBlacklistEntry.cs +++ b/OliverBooth.Common/Data/Web/IBlacklistEntry.cs @@ -1,4 +1,4 @@ -namespace OliverBooth.Data.Web; +namespace OliverBooth.Common.Data.Web; /// /// Represents an entry in the blacklist. diff --git a/OliverBooth/Data/Web/IBook.cs b/OliverBooth.Common/Data/Web/IBook.cs similarity index 95% rename from OliverBooth/Data/Web/IBook.cs rename to OliverBooth.Common/Data/Web/IBook.cs index 5c71d85..77f5856 100644 --- a/OliverBooth/Data/Web/IBook.cs +++ b/OliverBooth.Common/Data/Web/IBook.cs @@ -1,4 +1,4 @@ -namespace OliverBooth.Data.Web; +namespace OliverBooth.Common.Data.Web; /// /// Represents a book. diff --git a/OliverBooth/Data/Web/ICodeSnippet.cs b/OliverBooth.Common/Data/Web/ICodeSnippet.cs similarity index 93% rename from OliverBooth/Data/Web/ICodeSnippet.cs rename to OliverBooth.Common/Data/Web/ICodeSnippet.cs index ba1d324..29ec491 100644 --- a/OliverBooth/Data/Web/ICodeSnippet.cs +++ b/OliverBooth.Common/Data/Web/ICodeSnippet.cs @@ -1,4 +1,4 @@ -namespace OliverBooth.Data.Web; +namespace OliverBooth.Common.Data.Web; /// /// Represents a code snippet. diff --git a/OliverBooth/Data/Web/IProgrammingLanguage.cs b/OliverBooth.Common/Data/Web/IProgrammingLanguage.cs similarity index 92% rename from OliverBooth/Data/Web/IProgrammingLanguage.cs rename to OliverBooth.Common/Data/Web/IProgrammingLanguage.cs index 039f31a..464b943 100644 --- a/OliverBooth/Data/Web/IProgrammingLanguage.cs +++ b/OliverBooth.Common/Data/Web/IProgrammingLanguage.cs @@ -1,4 +1,4 @@ -namespace OliverBooth.Data.Web; +namespace OliverBooth.Common.Data.Web; /// /// Represents a programming language. diff --git a/OliverBooth/Data/Web/IProject.cs b/OliverBooth.Common/Data/Web/IProject.cs similarity index 98% rename from OliverBooth/Data/Web/IProject.cs rename to OliverBooth.Common/Data/Web/IProject.cs index f2cc0d8..0b41719 100644 --- a/OliverBooth/Data/Web/IProject.cs +++ b/OliverBooth.Common/Data/Web/IProject.cs @@ -1,4 +1,4 @@ -namespace OliverBooth.Data.Web; +namespace OliverBooth.Common.Data.Web; /// /// Represents a project. diff --git a/OliverBooth/Data/Web/ITemplate.cs b/OliverBooth.Common/Data/Web/ITemplate.cs similarity index 92% rename from OliverBooth/Data/Web/ITemplate.cs rename to OliverBooth.Common/Data/Web/ITemplate.cs index 50ad893..b3bd1e5 100644 --- a/OliverBooth/Data/Web/ITemplate.cs +++ b/OliverBooth.Common/Data/Web/ITemplate.cs @@ -1,4 +1,4 @@ -namespace OliverBooth.Data.Web; +namespace OliverBooth.Common.Data.Web; /// /// Represents a template. diff --git a/OliverBooth/Data/Web/ITutorialArticle.cs b/OliverBooth.Common/Data/Web/ITutorialArticle.cs similarity index 98% rename from OliverBooth/Data/Web/ITutorialArticle.cs rename to OliverBooth.Common/Data/Web/ITutorialArticle.cs index a53b289..7d27e63 100644 --- a/OliverBooth/Data/Web/ITutorialArticle.cs +++ b/OliverBooth.Common/Data/Web/ITutorialArticle.cs @@ -1,4 +1,4 @@ -namespace OliverBooth.Data.Web; +namespace OliverBooth.Common.Data.Web; /// /// Represents a tutorial article. diff --git a/OliverBooth/Data/Web/ITutorialFolder.cs b/OliverBooth.Common/Data/Web/ITutorialFolder.cs similarity index 96% rename from OliverBooth/Data/Web/ITutorialFolder.cs rename to OliverBooth.Common/Data/Web/ITutorialFolder.cs index 65a3cd2..0544d85 100644 --- a/OliverBooth/Data/Web/ITutorialFolder.cs +++ b/OliverBooth.Common/Data/Web/ITutorialFolder.cs @@ -1,4 +1,4 @@ -namespace OliverBooth.Data.Web; +namespace OliverBooth.Common.Data.Web; /// /// Represents a folder for tutorial articles. diff --git a/OliverBooth/Data/Web/ProjectStatus.cs b/OliverBooth.Common/Data/Web/ProjectStatus.cs similarity index 92% rename from OliverBooth/Data/Web/ProjectStatus.cs rename to OliverBooth.Common/Data/Web/ProjectStatus.cs index a3cbf49..a7e4cb1 100644 --- a/OliverBooth/Data/Web/ProjectStatus.cs +++ b/OliverBooth.Common/Data/Web/ProjectStatus.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace OliverBooth.Data.Web; +namespace OliverBooth.Common.Data.Web; /// /// Represents the status of a project. diff --git a/OliverBooth.Common/OliverBooth.Common.csproj b/OliverBooth.Common/OliverBooth.Common.csproj new file mode 100644 index 0000000..f127848 --- /dev/null +++ b/OliverBooth.Common/OliverBooth.Common.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + + + + + + + + + + diff --git a/OliverBooth/Services/IBlogPostService.cs b/OliverBooth.Common/Services/IBlogPostService.cs similarity index 98% rename from OliverBooth/Services/IBlogPostService.cs rename to OliverBooth.Common/Services/IBlogPostService.cs index 5947f19..29b13e3 100644 --- a/OliverBooth/Services/IBlogPostService.cs +++ b/OliverBooth.Common/Services/IBlogPostService.cs @@ -1,7 +1,7 @@ using System.Diagnostics.CodeAnalysis; -using OliverBooth.Data.Blog; +using OliverBooth.Common.Data.Blog; -namespace OliverBooth.Services; +namespace OliverBooth.Common.Services; /// /// Represents a service for managing blog posts. diff --git a/OliverBooth/Services/IBlogUserService.cs b/OliverBooth.Common/Services/IBlogUserService.cs similarity index 90% rename from OliverBooth/Services/IBlogUserService.cs rename to OliverBooth.Common/Services/IBlogUserService.cs index 0a117ae..83577e3 100644 --- a/OliverBooth/Services/IBlogUserService.cs +++ b/OliverBooth.Common/Services/IBlogUserService.cs @@ -1,7 +1,7 @@ using System.Diagnostics.CodeAnalysis; -using OliverBooth.Data.Blog; +using OliverBooth.Common.Data.Blog; -namespace OliverBooth.Services; +namespace OliverBooth.Common.Services; /// /// Represents a service for managing users. diff --git a/OliverBooth/Services/ICodeSnippetService.cs b/OliverBooth.Common/Services/ICodeSnippetService.cs similarity index 95% rename from OliverBooth/Services/ICodeSnippetService.cs rename to OliverBooth.Common/Services/ICodeSnippetService.cs index b81cb88..d065ce0 100644 --- a/OliverBooth/Services/ICodeSnippetService.cs +++ b/OliverBooth.Common/Services/ICodeSnippetService.cs @@ -1,7 +1,7 @@ using System.Diagnostics.CodeAnalysis; -using OliverBooth.Data.Web; +using OliverBooth.Common.Data.Web; -namespace OliverBooth.Services; +namespace OliverBooth.Common.Services; /// /// Represents a service which can fetch multi-language code snippets. diff --git a/OliverBooth/Services/IContactService.cs b/OliverBooth.Common/Services/IContactService.cs similarity index 77% rename from OliverBooth/Services/IContactService.cs rename to OliverBooth.Common/Services/IContactService.cs index 800a3e7..38b1e9c 100644 --- a/OliverBooth/Services/IContactService.cs +++ b/OliverBooth.Common/Services/IContactService.cs @@ -1,6 +1,6 @@ -using OliverBooth.Data.Web; +using OliverBooth.Common.Data.Web; -namespace OliverBooth.Services; +namespace OliverBooth.Common.Services; /// /// Represents a service for managing contact information. diff --git a/OliverBooth/Services/IMastodonService.cs b/OliverBooth.Common/Services/IMastodonService.cs similarity index 59% rename from OliverBooth/Services/IMastodonService.cs rename to OliverBooth.Common/Services/IMastodonService.cs index 2126d46..dc38a51 100644 --- a/OliverBooth/Services/IMastodonService.cs +++ b/OliverBooth.Common/Services/IMastodonService.cs @@ -1,6 +1,6 @@ -using OliverBooth.Data.Mastodon; +using OliverBooth.Common.Data.Mastodon; -namespace OliverBooth.Services; +namespace OliverBooth.Common.Services; public interface IMastodonService { @@ -8,5 +8,5 @@ public interface IMastodonService /// Gets the latest status posted to Mastodon. /// /// The latest status. - MastodonStatus GetLatestStatus(); + IMastodonStatus GetLatestStatus(); } \ No newline at end of file diff --git a/OliverBooth.Common/Services/IProgrammingLanguageService.cs b/OliverBooth.Common/Services/IProgrammingLanguageService.cs new file mode 100644 index 0000000..dc0acd0 --- /dev/null +++ b/OliverBooth.Common/Services/IProgrammingLanguageService.cs @@ -0,0 +1,14 @@ +namespace OliverBooth.Common.Services; + +/// +/// Represents a service which can perform programming language lookup. +/// +public interface IProgrammingLanguageService +{ + /// + /// Returns the human-readable name of a language. + /// + /// The alias of the language. + /// The human-readable name, or if the name could not be found. + string GetLanguageName(string alias); +} diff --git a/OliverBooth/Services/IProjectService.cs b/OliverBooth.Common/Services/IProjectService.cs similarity index 97% rename from OliverBooth/Services/IProjectService.cs rename to OliverBooth.Common/Services/IProjectService.cs index 16ae771..66cdbaa 100644 --- a/OliverBooth/Services/IProjectService.cs +++ b/OliverBooth.Common/Services/IProjectService.cs @@ -1,7 +1,7 @@ using System.Diagnostics.CodeAnalysis; -using OliverBooth.Data.Web; +using OliverBooth.Common.Data.Web; -namespace OliverBooth.Services; +namespace OliverBooth.Common.Services; /// /// Represents a service for interacting with projects. diff --git a/OliverBooth/Services/IReadingListService.cs b/OliverBooth.Common/Services/IReadingListService.cs similarity index 85% rename from OliverBooth/Services/IReadingListService.cs rename to OliverBooth.Common/Services/IReadingListService.cs index 78cc4f2..6f59791 100644 --- a/OliverBooth/Services/IReadingListService.cs +++ b/OliverBooth.Common/Services/IReadingListService.cs @@ -1,6 +1,6 @@ -using OliverBooth.Data.Web; +using OliverBooth.Common.Data.Web; -namespace OliverBooth.Services; +namespace OliverBooth.Common.Services; /// /// Represents a service which fetches books from the reading list. diff --git a/OliverBooth/Services/ITutorialService.cs b/OliverBooth.Common/Services/ITutorialService.cs similarity index 97% rename from OliverBooth/Services/ITutorialService.cs rename to OliverBooth.Common/Services/ITutorialService.cs index 1366172..11ddc62 100644 --- a/OliverBooth/Services/ITutorialService.cs +++ b/OliverBooth.Common/Services/ITutorialService.cs @@ -1,9 +1,9 @@ using System.Diagnostics.CodeAnalysis; -using OliverBooth.Data; -using OliverBooth.Data.Blog; -using OliverBooth.Data.Web; +using OliverBooth.Common.Data; +using OliverBooth.Common.Data.Blog; +using OliverBooth.Common.Data.Web; -namespace OliverBooth.Services; +namespace OliverBooth.Common.Services; /// /// Represents a service which can retrieve tutorial articles. diff --git a/OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutBlock.cs b/OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutBlock.cs new file mode 100644 index 0000000..fdf484a --- /dev/null +++ b/OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutBlock.cs @@ -0,0 +1,43 @@ +using Markdig.Helpers; +using Markdig.Syntax; + +namespace OliverBooth.Extensions.Markdig.Markdown.Callout; + +/// +/// Represents a callout block. +/// +internal sealed class CalloutBlock : QuoteBlock +{ + /// + /// Initializes a new instance of the class. + /// + /// The type of the callout. + public CalloutBlock(StringSlice type) : base(null) + { + Type = type; + } + + /// + /// Gets or sets a value indicating whether this callout is foldable. + /// + /// if this callout is foldable; otherwise, . + public bool Foldable { get; set; } + + /// + /// Gets or sets the title of the callout. + /// + /// The title of the callout. + public StringSlice Title { get; set; } + + /// + /// Gets or sets the trailing whitespace trivia. + /// + /// The trailing whitespace trivia. + public StringSlice TrailingWhitespaceTrivia { get; set; } + + /// + /// Gets or sets the type of the callout. + /// + /// The type of the callout. + public StringSlice Type { get; set; } +} diff --git a/OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutExtension.cs b/OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutExtension.cs new file mode 100644 index 0000000..eda4718 --- /dev/null +++ b/OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutExtension.cs @@ -0,0 +1,32 @@ +using Markdig; +using Markdig.Parsers.Inlines; +using Markdig.Renderers; +using Markdig.Renderers.Html; + +namespace OliverBooth.Extensions.Markdig.Markdown.Callout; + +/// +/// Extension for adding Obsidian-style callouts to a Markdown pipeline. +/// +internal sealed class CalloutExtension : IMarkdownExtension +{ + /// + public void Setup(MarkdownPipelineBuilder pipeline) + { + var parser = pipeline.InlineParsers.Find(); + if (parser is null) + { + pipeline.InlineParsers.InsertBefore(new CalloutInlineParser()); + } + } + + /// + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + var blockRenderer = renderer.ObjectRenderers.FindExact(); + if (blockRenderer is null) + { + renderer.ObjectRenderers.InsertBefore(new CalloutRenderer(pipeline)); + } + } +} diff --git a/OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutInlineParser.cs b/OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutInlineParser.cs new file mode 100644 index 0000000..9434011 --- /dev/null +++ b/OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutInlineParser.cs @@ -0,0 +1,176 @@ +using System.Reflection; +using Cysharp.Text; +using Markdig.Helpers; +using Markdig.Parsers; +using Markdig.Renderers.Html; +using Markdig.Syntax; + +namespace OliverBooth.Extensions.Markdig.Markdown.Callout; + +/// +/// An inline parser for Obsidian-style callouts ([!NOTE] etc.) +/// +internal sealed class CalloutInlineParser : InlineParser +{ + // ugly hack to access internal method + private static readonly MethodInfo ReplaceParentContainerMethod = + typeof(InlineProcessor).GetMethod("ReplaceParentContainer", BindingFlags.Instance | BindingFlags.NonPublic)!; + + /// + /// Initializes a new instance of the class. + /// + public CalloutInlineParser() + { + OpeningCharacters = ['[']; + } + + /// + public override bool Match(InlineProcessor processor, ref StringSlice slice) + { + // We expect the alert to be the first child of a quote block. Example: + // > [!NOTE] + // > This is a note + if (processor.Block is not ParagraphBlock { Parent: QuoteBlock quoteBlock } paragraphBlock || + paragraphBlock.Inline?.FirstChild != null) + { + return false; + } + + StringSlice cache = slice; + char current = slice.NextChar(); + + if (current != '!') + { + slice = cache; + return false; + } + + current = slice.NextChar(); // skip ! + + int start = slice.Start; + int end = start; + + while (current.IsAlphaUpper()) + { + end = slice.Start; + current = slice.NextChar(); + } + + if (current != ']' || start == end) + { + slice = cache; + return false; + } + + var type = new StringSlice(slice.Text, start, end); + current = slice.NextChar(); // skip ] + start = slice.Start; + + bool fold = false; + if (current == '-') + { + fold = true; + current = slice.NextChar(); // skip - + start = slice.Start; + } + + ReadTitle(current, ref slice, out StringSlice title, out end); + + var callout = new CalloutBlock(type) + { + Foldable = fold, + Span = quoteBlock.Span, + TrailingWhitespaceTrivia = new StringSlice(slice.Text, start, end), + Line = quoteBlock.Line, + Column = quoteBlock.Column, + Title = title + }; + + AddAttributes(callout, type); + ReplaceQuoteBlock(processor, quoteBlock, callout); + return true; + } + + private static void ReadTitle(char startChar, ref StringSlice slice, out StringSlice title, out int end) + { + using Utf16ValueStringBuilder builder = ZString.CreateStringBuilder(); + + char current = startChar; + while (true) + { + if (current is not ('\0' or '\r' or '\n')) + { + builder.Append(current); + current = slice.NextChar(); + continue; + } + + end = slice.Start; + if (HandleCharacter(ref slice, ref end, ref current)) + { + continue; + } + + break; + } + + title = new StringSlice(builder.ToString(), 0, builder.Length); + } + + private static bool HandleCharacter(ref StringSlice slice, ref int end, ref char current) + { + switch (current) + { + case '\r': + current = slice.NextChar(); // skip \r + + if (current is not ('\0' or '\n')) + { + return true; + } + + end = slice.Start; + if (current == '\n') + { + slice.NextChar(); // skip \n + } + + break; + + case '\n': + slice.NextChar(); // skip \n + break; + } + + return false; + } + + private static void AddAttributes(IMarkdownObject callout, StringSlice type) + { + HtmlAttributes attributes = callout.GetAttributes(); + attributes.AddClass("callout"); + attributes.AddProperty("data-callout", type.AsSpan().ToString().ToLowerInvariant()); + } + + private static void ReplaceQuoteBlock(InlineProcessor processor, QuoteBlock quoteBlock, CalloutBlock callout) + { + ContainerBlock? parentQuoteBlock = quoteBlock.Parent; + if (parentQuoteBlock is null) + { + return; + } + + int indexOfQuoteBlock = parentQuoteBlock.IndexOf(quoteBlock); + parentQuoteBlock[indexOfQuoteBlock] = callout; + + while (quoteBlock.Count > 0) + { + var block = quoteBlock[0]; + quoteBlock.RemoveAt(0); + callout.Add(block); + } + + ReplaceParentContainerMethod.Invoke(processor, [quoteBlock, callout]); + // ReplaceParentContainer(processor, quoteBlock, callout); + } +} diff --git a/OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutRenderer.cs b/OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutRenderer.cs new file mode 100644 index 0000000..49dd2f2 --- /dev/null +++ b/OliverBooth.Extensions.Markdig/Markdown/Callout/CalloutRenderer.cs @@ -0,0 +1,119 @@ +using HtmlAgilityPack; +using Humanizer; +using Markdig; +using Markdig.Renderers; +using Markdig.Renderers.Html; + +namespace OliverBooth.Extensions.Markdig.Markdown.Callout; + +/// +/// Represents an HTML renderer which renders a . +/// +internal sealed class CalloutRenderer : HtmlObjectRenderer +{ + private readonly MarkdownPipeline _pipeline; + + private static readonly Dictionary CalloutTypes = new() + { + ["NOTE"] = "pencil", + ["ABSTRACT"] = "clipboard-list", + ["INFO"] = "info", + ["TODO"] = "circle-check", + ["TIP"] = "flame", + ["IMPORTANT"] = "flame", + ["SUCCESS"] = "check", + ["QUESTION"] = "circle-help", + ["WARNING"] = "triangle-alert", + ["FAILURE"] = "x", + ["DANGER"] = "zap", + ["BUG"] = "bug", + ["EXAMPLE"] = "list", + ["CITE"] = "quote", + ["UPDATE"] = "calendar-check", + }; + + public CalloutRenderer(MarkdownPipeline pipeline) + { + _pipeline = pipeline; + } + + /// + protected override void Write(HtmlRenderer renderer, CalloutBlock block) + { + renderer.EnsureLine(); + if (renderer.EnableHtmlForBlock) + { + RenderAsHtml(renderer, block, _pipeline); + } + else + { + RenderAsText(renderer, block); + } + + renderer.EnsureLine(); + } + + private static void RenderAsHtml(HtmlRenderer renderer, CalloutBlock block, MarkdownPipeline pipeline) + { + string title = block.Title.Text; + ReadOnlySpan type = block.Type.AsSpan(); + Span upperType = stackalloc char[type.Length]; + type.ToUpperInvariant(upperType); + + if (!CalloutTypes.TryGetValue(upperType.ToString(), out string? lucideClass)) + { + lucideClass = "pencil"; + } + + var typeString = type.ToString().ToLowerInvariant(); + + renderer.Write($"
'); + renderer.Write("
"); + + string calloutTitle = title.Length == 0 ? typeString.Humanize(LetterCasing.Sentence) : title; + WriteTitle(renderer, pipeline, calloutTitle); + + if (block.Foldable) + { + renderer.Write(""); + } + + renderer.WriteLine("
"); + renderer.Write("
"); + renderer.WriteChildren(block); + renderer.WriteLine("
"); + renderer.WriteLine("
"); + renderer.EnsureLine(); + } + + private static void WriteTitle(TextRendererBase renderer, MarkdownPipeline pipeline, string calloutTitle) + { + string html = global::Markdig.Markdown.ToHtml(calloutTitle, pipeline); + var document = new HtmlDocument(); + document.LoadHtml(html); + if (document.DocumentNode.FirstChild is { Name: "p" } child) + { + // ugly hack to remove

tag generated by Markdig + document.DocumentNode.InnerHtml = child.InnerHtml; + } + + document.Save(renderer.Writer); + } + + private static void RenderAsText(HtmlRenderer renderer, CalloutBlock block) + { + string title = block.Title.Text; + ReadOnlySpan type = block.Type.AsSpan(); + renderer.WriteLine(title.Length == 0 ? type.ToString().ToUpperInvariant() : title.ToUpperInvariant()); + renderer.WriteChildren(block); + renderer.EnsureLine(); + } +} diff --git a/OliverBooth.Extensions.Markdig/Markdown/MarkdownExtensions.cs b/OliverBooth.Extensions.Markdig/Markdown/MarkdownExtensions.cs new file mode 100644 index 0000000..cd54de4 --- /dev/null +++ b/OliverBooth.Extensions.Markdig/Markdown/MarkdownExtensions.cs @@ -0,0 +1,21 @@ +using Markdig; +using OliverBooth.Extensions.Markdig.Markdown.Callout; + +namespace OliverBooth.Extensions.Markdig.Markdown; + +///

+/// Extension methods for . +/// +internal static class MarkdownExtensions +{ + /// + /// Uses this extension to enable Obsidian-style callouts. + /// + /// The pipeline. + /// The modified pipeline. + public static MarkdownPipelineBuilder UseCallouts(this MarkdownPipelineBuilder pipeline) + { + pipeline.Extensions.AddIfNotAlready(); + return pipeline; + } +} diff --git a/OliverBooth/Markdown/Template/TemplateExtension.cs b/OliverBooth.Extensions.Markdig/Markdown/Template/TemplateExtension.cs similarity index 90% rename from OliverBooth/Markdown/Template/TemplateExtension.cs rename to OliverBooth.Extensions.Markdig/Markdown/Template/TemplateExtension.cs index ec1ea0d..c160735 100644 --- a/OliverBooth/Markdown/Template/TemplateExtension.cs +++ b/OliverBooth.Extensions.Markdig/Markdown/Template/TemplateExtension.cs @@ -1,8 +1,8 @@ using Markdig; using Markdig.Renderers; -using OliverBooth.Services; +using OliverBooth.Extensions.Markdig.Services; -namespace OliverBooth.Markdown.Template; +namespace OliverBooth.Extensions.Markdig.Markdown.Template; /// /// Represents a Markdown extension that adds support for MediaWiki-style templates. diff --git a/OliverBooth/Markdown/Template/TemplateInline.cs b/OliverBooth.Extensions.Markdig/Markdown/Template/TemplateInline.cs similarity index 95% rename from OliverBooth/Markdown/Template/TemplateInline.cs rename to OliverBooth.Extensions.Markdig/Markdown/Template/TemplateInline.cs index a50f3b8..6b19845 100644 --- a/OliverBooth/Markdown/Template/TemplateInline.cs +++ b/OliverBooth.Extensions.Markdig/Markdown/Template/TemplateInline.cs @@ -1,6 +1,6 @@ using Markdig.Syntax.Inlines; -namespace OliverBooth.Markdown.Template; +namespace OliverBooth.Extensions.Markdig.Markdown.Template; /// /// Represents a Markdown inline element that represents a MediaWiki-style template. diff --git a/OliverBooth/Markdown/Template/TemplateInlineParser.cs b/OliverBooth.Extensions.Markdig/Markdown/Template/TemplateInlineParser.cs similarity index 99% rename from OliverBooth/Markdown/Template/TemplateInlineParser.cs rename to OliverBooth.Extensions.Markdig/Markdown/Template/TemplateInlineParser.cs index 57385b3..953f415 100644 --- a/OliverBooth/Markdown/Template/TemplateInlineParser.cs +++ b/OliverBooth.Extensions.Markdig/Markdown/Template/TemplateInlineParser.cs @@ -2,7 +2,7 @@ using Cysharp.Text; using Markdig.Helpers; using Markdig.Parsers; -namespace OliverBooth.Markdown.Template; +namespace OliverBooth.Extensions.Markdig.Markdown.Template; /// /// Represents a Markdown inline parser that handles MediaWiki-style templates. diff --git a/OliverBooth/Markdown/Template/TemplateRenderer.cs b/OliverBooth.Extensions.Markdig/Markdown/Template/TemplateRenderer.cs similarity index 88% rename from OliverBooth/Markdown/Template/TemplateRenderer.cs rename to OliverBooth.Extensions.Markdig/Markdown/Template/TemplateRenderer.cs index 9e70fb2..1c71480 100644 --- a/OliverBooth/Markdown/Template/TemplateRenderer.cs +++ b/OliverBooth.Extensions.Markdig/Markdown/Template/TemplateRenderer.cs @@ -1,8 +1,8 @@ using Markdig.Renderers; using Markdig.Renderers.Html; -using OliverBooth.Services; +using OliverBooth.Extensions.Markdig.Services; -namespace OliverBooth.Markdown.Template; +namespace OliverBooth.Extensions.Markdig.Markdown.Template; /// /// Represents a Markdown object renderer that handles elements. diff --git a/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampExtension.cs b/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampExtension.cs new file mode 100644 index 0000000..0a9dac3 --- /dev/null +++ b/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampExtension.cs @@ -0,0 +1,25 @@ +using Markdig; +using Markdig.Renderers; + +namespace OliverBooth.Extensions.Markdig.Markdown.Timestamp; + +/// +/// Represents a Markdig extension that supports Discord-style timestamps. +/// +public class TimestampExtension : IMarkdownExtension +{ + /// + public void Setup(MarkdownPipelineBuilder pipeline) + { + pipeline.InlineParsers.AddIfNotAlready(); + } + + /// + public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) + { + if (renderer is HtmlRenderer htmlRenderer) + { + htmlRenderer.ObjectRenderers.AddIfNotAlready(); + } + } +} diff --git a/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampFormat.cs b/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampFormat.cs new file mode 100644 index 0000000..49530be --- /dev/null +++ b/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampFormat.cs @@ -0,0 +1,42 @@ +namespace OliverBooth.Extensions.Markdig.Markdown.Timestamp; + +/// +/// An enumeration of timestamp formats. +/// +public enum TimestampFormat +{ + /// + /// Short time format. Example: 12:00 + /// + ShortTime = 't', + + /// + /// Long time format. Example: 12:00:00 + /// + LongTime = 'T', + + /// + /// Short date format. Example: 1/1/2000 + /// + ShortDate = 'd', + + /// + /// Long date format. Example: 1 January 2000 + /// + LongDate = 'D', + + /// + /// Short date/time format. Example: 1 January 2000 at 12:00 + /// + LongDateShortTime = 'f', + + /// + /// Long date/time format. Example: Saturday, 1 January 2000 at 12:00 + /// + LongDateTime = 'F', + + /// + /// Relative date/time format. Example: 1 second ago + /// + Relative = 'R', +} diff --git a/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampInline.cs b/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampInline.cs new file mode 100644 index 0000000..d780ad9 --- /dev/null +++ b/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampInline.cs @@ -0,0 +1,21 @@ +using Markdig.Syntax.Inlines; + +namespace OliverBooth.Extensions.Markdig.Markdown.Timestamp; + +/// +/// Represents a Markdown inline element that contains a timestamp. +/// +public sealed class TimestampInline : Inline +{ + /// + /// Gets or sets the format. + /// + /// The format. + public TimestampFormat Format { get; set; } + + /// + /// Gets or sets the timestamp. + /// + /// The timestamp. + public DateTimeOffset Timestamp { get; set; } +} diff --git a/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampInlineParser.cs b/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampInlineParser.cs new file mode 100644 index 0000000..ceaaec5 --- /dev/null +++ b/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampInlineParser.cs @@ -0,0 +1,91 @@ +using Markdig.Helpers; +using Markdig.Parsers; + +namespace OliverBooth.Extensions.Markdig.Markdown.Timestamp; + +/// +/// Represents a Markdown inline parser that matches Discord-style timestamps. +/// +public sealed class TimestampInlineParser : InlineParser +{ + /// + /// Initializes a new instance of the class. + /// + public TimestampInlineParser() + { + OpeningCharacters = new[] { '<' }; + } + + /// + public override bool Match(InlineProcessor processor, ref StringSlice slice) + { + // Previous char must be a space + if (!slice.PeekCharExtra(-1).IsWhiteSpaceOrZero()) + { + return false; + } + + ReadOnlySpan span = slice.Text.AsSpan(slice.Start, slice.Length); + + if (!TryConsumeTimestamp(span, out ReadOnlySpan rawTimestamp, out char format)) + { + return false; + } + + if (!long.TryParse(rawTimestamp, out long timestamp)) + { + return false; + } + + bool hasFormat = format != '\0'; + processor.Inline = new TimestampInline + { + Format = (TimestampFormat)format, + Timestamp = DateTimeOffset.FromUnixTimeSeconds(timestamp) + }; + + int paddingCount = hasFormat ? 6 : 4; // or optionally + slice.Start += rawTimestamp.Length + paddingCount; + return true; + } + + private bool TryConsumeTimestamp(ReadOnlySpan source, + out ReadOnlySpan timestamp, + out char format) + { + timestamp = default; + format = default; + + if (!source.StartsWith("') == -1) + { + timestamp = default; + return false; + } + + int delimiterIndex = timestamp.IndexOf(':'); + if (delimiterIndex == 0) + { + // invalid format + timestamp = default; + return false; + } + + if (delimiterIndex == -1) + { + // no format, default to relative + format = 'R'; + timestamp = timestamp[..^1]; // trim > + } + else + { + // use specified format + format = timestamp[^2]; + timestamp = timestamp[..^3]; + } + + return true; + } +} diff --git a/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampRenderer.cs b/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampRenderer.cs new file mode 100644 index 0000000..ceda5ac --- /dev/null +++ b/OliverBooth.Extensions.Markdig/Markdown/Timestamp/TimestampRenderer.cs @@ -0,0 +1,55 @@ +using System.ComponentModel; +using Humanizer; +using Markdig.Renderers; +using Markdig.Renderers.Html; + +namespace OliverBooth.Extensions.Markdig.Markdown.Timestamp; + +/// +/// Represents a Markdown object renderer that renders elements. +/// +public sealed class TimestampRenderer : HtmlObjectRenderer +{ + /// + protected override void Write(HtmlRenderer renderer, TimestampInline obj) + { + DateTimeOffset timestamp = obj.Timestamp; + TimestampFormat format = obj.Format; + + renderer.Write(""); + + switch (format) + { + case TimestampFormat.LongDate: + renderer.Write(timestamp.ToString("d MMMM yyyy")); + break; + + case TimestampFormat.LongDateShortTime: + renderer.Write(timestamp.ToString(@"d MMMM yyyy \a\t HH:mm")); + break; + + case TimestampFormat.LongDateTime: + renderer.Write(timestamp.ToString(@"dddd, d MMMM yyyy \a\t HH:mm")); + break; + + case TimestampFormat.Relative: + renderer.Write(timestamp.Humanize()); + break; + + case var _ when !Enum.IsDefined(format): + throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(TimestampFormat)); + + default: + renderer.Write(timestamp.ToString(((char)format).ToString())); + break; + } + + renderer.Write(""); + } +} diff --git a/OliverBooth.Extensions.Markdig/MarkdownPipelineExtensions.cs b/OliverBooth.Extensions.Markdig/MarkdownPipelineExtensions.cs new file mode 100644 index 0000000..20ea173 --- /dev/null +++ b/OliverBooth.Extensions.Markdig/MarkdownPipelineExtensions.cs @@ -0,0 +1,38 @@ +using Markdig; +using OliverBooth.Extensions.Markdig.Markdown.Template; +using OliverBooth.Extensions.Markdig.Services; + +namespace OliverBooth.Extensions.Markdig; + +/// +/// Extension methods for . +/// +public static class MarkdownPipelineExtensions +{ + /// + /// Enables the use of Wiki-style templates in this pipeline. + /// + /// The Markdig markdown pipeline builder. + /// The template service responsible for fetching and rendering templates. + /// The modified Markdig markdown pipeline builder. + /// + /// is . + /// -or- + /// is . + /// + public static MarkdownPipelineBuilder UseTemplates(this MarkdownPipelineBuilder builder, ITemplateService templateService) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (templateService is null) + { + throw new ArgumentNullException(nameof(templateService)); + } + + builder.Use(new TemplateExtension(templateService)); + return builder; + } +} diff --git a/OliverBooth.Extensions.Markdig/OliverBooth.Extensions.Markdig.csproj b/OliverBooth.Extensions.Markdig/OliverBooth.Extensions.Markdig.csproj new file mode 100644 index 0000000..9a76817 --- /dev/null +++ b/OliverBooth.Extensions.Markdig/OliverBooth.Extensions.Markdig.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/OliverBooth/Services/ITemplateService.cs b/OliverBooth.Extensions.Markdig/Services/ITemplateService.cs similarity index 94% rename from OliverBooth/Services/ITemplateService.cs rename to OliverBooth.Extensions.Markdig/Services/ITemplateService.cs index 5fa9239..30c8b86 100644 --- a/OliverBooth/Services/ITemplateService.cs +++ b/OliverBooth.Extensions.Markdig/Services/ITemplateService.cs @@ -1,8 +1,8 @@ using System.Diagnostics.CodeAnalysis; -using OliverBooth.Data.Web; -using OliverBooth.Markdown.Template; +using OliverBooth.Common.Data.Web; +using OliverBooth.Extensions.Markdig.Markdown.Template; -namespace OliverBooth.Services; +namespace OliverBooth.Extensions.Markdig.Services; /// /// Represents a service that renders MediaWiki-style templates. diff --git a/OliverBooth.sln b/OliverBooth.sln index e2ba2dd..ea184df 100644 --- a/OliverBooth.sln +++ b/OliverBooth.sln @@ -13,6 +13,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution global.json = global.json EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OliverBooth.Extensions.Markdig", "OliverBooth.Extensions.Markdig\OliverBooth.Extensions.Markdig.csproj", "{3B012CD2-3201-41A0-BEF9-8E0B6247BB7E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OliverBooth.Common", "OliverBooth.Common\OliverBooth.Common.csproj", "{AD231E0F-FAED-4661-963F-EB22F858E148}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -23,6 +27,14 @@ Global {A58A6FA3-480C-400B-822A-3786741BF39C}.Debug|Any CPU.Build.0 = Debug|Any CPU {A58A6FA3-480C-400B-822A-3786741BF39C}.Release|Any CPU.ActiveCfg = Release|Any CPU {A58A6FA3-480C-400B-822A-3786741BF39C}.Release|Any CPU.Build.0 = Release|Any CPU + {3B012CD2-3201-41A0-BEF9-8E0B6247BB7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3B012CD2-3201-41A0-BEF9-8E0B6247BB7E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B012CD2-3201-41A0-BEF9-8E0B6247BB7E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3B012CD2-3201-41A0-BEF9-8E0B6247BB7E}.Release|Any CPU.Build.0 = Release|Any CPU + {AD231E0F-FAED-4661-963F-EB22F858E148}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD231E0F-FAED-4661-963F-EB22F858E148}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD231E0F-FAED-4661-963F-EB22F858E148}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD231E0F-FAED-4661-963F-EB22F858E148}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution EndGlobalSection diff --git a/OliverBooth/Controllers/Blog/BlogApiController.cs b/OliverBooth/Controllers/Blog/BlogApiController.cs index 21eb070..98674e3 100644 --- a/OliverBooth/Controllers/Blog/BlogApiController.cs +++ b/OliverBooth/Controllers/Blog/BlogApiController.cs @@ -1,7 +1,7 @@ using Humanizer; using Microsoft.AspNetCore.Mvc; -using OliverBooth.Data.Blog; -using OliverBooth.Services; +using OliverBooth.Common.Data.Blog; +using OliverBooth.Common.Services; namespace OliverBooth.Controllers.Blog; diff --git a/OliverBooth/Controllers/Blog/RssController.cs b/OliverBooth/Controllers/Blog/RssController.cs index 83b1a08..04cd16d 100644 --- a/OliverBooth/Controllers/Blog/RssController.cs +++ b/OliverBooth/Controllers/Blog/RssController.cs @@ -1,8 +1,8 @@ using System.Xml.Serialization; using Microsoft.AspNetCore.Mvc; -using OliverBooth.Data.Blog; +using OliverBooth.Common.Data.Blog; +using OliverBooth.Common.Services; using OliverBooth.Data.Blog.Rss; -using OliverBooth.Services; namespace OliverBooth.Controllers.Blog; diff --git a/OliverBooth/Controllers/FormattedBlacklist.cs b/OliverBooth/Controllers/FormattedBlacklist.cs index bce6143..5186ec9 100644 --- a/OliverBooth/Controllers/FormattedBlacklist.cs +++ b/OliverBooth/Controllers/FormattedBlacklist.cs @@ -1,7 +1,7 @@ using System.Text; using Microsoft.AspNetCore.Mvc; -using OliverBooth.Data.Web; -using OliverBooth.Services; +using OliverBooth.Common.Data.Web; +using OliverBooth.Common.Services; namespace OliverBooth.Controllers; diff --git a/OliverBooth/Data/Blog/BlogPost.cs b/OliverBooth/Data/Blog/BlogPost.cs index 59067b5..f84292a 100644 --- a/OliverBooth/Data/Blog/BlogPost.cs +++ b/OliverBooth/Data/Blog/BlogPost.cs @@ -1,4 +1,6 @@ using System.ComponentModel.DataAnnotations.Schema; +using OliverBooth.Common.Data; +using OliverBooth.Common.Data.Blog; using SmartFormat; namespace OliverBooth.Data.Blog; diff --git a/OliverBooth/Data/Blog/Configuration/BlogPostConfiguration.cs b/OliverBooth/Data/Blog/Configuration/BlogPostConfiguration.cs index 1ecde2f..fd7085b 100644 --- a/OliverBooth/Data/Blog/Configuration/BlogPostConfiguration.cs +++ b/OliverBooth/Data/Blog/Configuration/BlogPostConfiguration.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using OliverBooth.Common.Data; namespace OliverBooth.Data.Blog.Configuration; diff --git a/OliverBooth/Data/Blog/LegacyComment.cs b/OliverBooth/Data/Blog/LegacyComment.cs index a020979..cd8b615 100644 --- a/OliverBooth/Data/Blog/LegacyComment.cs +++ b/OliverBooth/Data/Blog/LegacyComment.cs @@ -1,4 +1,5 @@ using System.Web; +using OliverBooth.Common.Data.Blog; namespace OliverBooth.Data.Blog; diff --git a/OliverBooth/Data/Blog/User.cs b/OliverBooth/Data/Blog/User.cs index dcd50ab..1b6ce40 100644 --- a/OliverBooth/Data/Blog/User.cs +++ b/OliverBooth/Data/Blog/User.cs @@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Security.Cryptography; using System.Text; using Cysharp.Text; +using OliverBooth.Common.Data.Blog; namespace OliverBooth.Data.Blog; diff --git a/OliverBooth/Data/Mastodon/MastodonStatus.cs b/OliverBooth/Data/Mastodon/MastodonStatus.cs index a4d3ebf..4276c02 100644 --- a/OliverBooth/Data/Mastodon/MastodonStatus.cs +++ b/OliverBooth/Data/Mastodon/MastodonStatus.cs @@ -1,34 +1,24 @@ using System.Text.Json.Serialization; +using OliverBooth.Common.Data.Mastodon; namespace OliverBooth.Data.Mastodon; -public sealed class MastodonStatus +/// +internal sealed class MastodonStatus : IMastodonStatus { - /// - /// Gets the content of the status. - /// - /// The content. + /// [JsonPropertyName("content")] public string Content { get; set; } = string.Empty; - /// - /// Gets the date and time at which this status was posted. - /// - /// The post timestamp. + /// [JsonPropertyName("created_at")] public DateTimeOffset CreatedAt { get; set; } - /// - /// Gets the media attachments for this status. - /// - /// The media attachments. + /// [JsonPropertyName("media_attachments")] - public IReadOnlyList MediaAttachments { get; set; } = ArraySegment.Empty; + public IReadOnlyList MediaAttachments { get; set; } = ArraySegment.Empty; - /// - /// Gets the original URI of the status. - /// - /// The original URI. + /// [JsonPropertyName("url")] public Uri OriginalUri { get; set; } = null!; } diff --git a/OliverBooth/Data/Web/BlacklistEntry.cs b/OliverBooth/Data/Web/BlacklistEntry.cs index 374d9c2..a0f68d3 100644 --- a/OliverBooth/Data/Web/BlacklistEntry.cs +++ b/OliverBooth/Data/Web/BlacklistEntry.cs @@ -1,3 +1,5 @@ +using OliverBooth.Common.Data.Web; + namespace OliverBooth.Data.Web; /// diff --git a/OliverBooth/Data/Web/Book.cs b/OliverBooth/Data/Web/Book.cs index 278aa4e..10e41dd 100644 --- a/OliverBooth/Data/Web/Book.cs +++ b/OliverBooth/Data/Web/Book.cs @@ -1,4 +1,5 @@ using NetBarcode; +using OliverBooth.Common.Data.Web; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Processing; diff --git a/OliverBooth/Data/Web/CodeSnippet.cs b/OliverBooth/Data/Web/CodeSnippet.cs index 19d29c1..13fb751 100644 --- a/OliverBooth/Data/Web/CodeSnippet.cs +++ b/OliverBooth/Data/Web/CodeSnippet.cs @@ -1,3 +1,5 @@ +using OliverBooth.Common.Data.Web; + namespace OliverBooth.Data.Web; /// diff --git a/OliverBooth/Data/Web/Configuration/BookConfiguration.cs b/OliverBooth/Data/Web/Configuration/BookConfiguration.cs index f27a1d2..fca84f9 100644 --- a/OliverBooth/Data/Web/Configuration/BookConfiguration.cs +++ b/OliverBooth/Data/Web/Configuration/BookConfiguration.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using OliverBooth.Common.Data.Web; namespace OliverBooth.Data.Web.Configuration; diff --git a/OliverBooth/Data/Web/Configuration/ProjectConfiguration.cs b/OliverBooth/Data/Web/Configuration/ProjectConfiguration.cs index 08ef5d9..8949763 100644 --- a/OliverBooth/Data/Web/Configuration/ProjectConfiguration.cs +++ b/OliverBooth/Data/Web/Configuration/ProjectConfiguration.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using OliverBooth.Common.Data.Web; namespace OliverBooth.Data.Web.Configuration; diff --git a/OliverBooth/Data/Web/Configuration/TutorialArticleConfiguration.cs b/OliverBooth/Data/Web/Configuration/TutorialArticleConfiguration.cs index b86a5b5..4caf190 100644 --- a/OliverBooth/Data/Web/Configuration/TutorialArticleConfiguration.cs +++ b/OliverBooth/Data/Web/Configuration/TutorialArticleConfiguration.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using OliverBooth.Common.Data; namespace OliverBooth.Data.Web.Configuration; diff --git a/OliverBooth/Data/Web/Configuration/TutorialFolderConfiguration.cs b/OliverBooth/Data/Web/Configuration/TutorialFolderConfiguration.cs index af1eef2..136d9c3 100644 --- a/OliverBooth/Data/Web/Configuration/TutorialFolderConfiguration.cs +++ b/OliverBooth/Data/Web/Configuration/TutorialFolderConfiguration.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using OliverBooth.Common.Data; namespace OliverBooth.Data.Web.Configuration; diff --git a/OliverBooth/Data/Web/ProgrammingLanguage.cs b/OliverBooth/Data/Web/ProgrammingLanguage.cs index 53746d9..9a4135f 100644 --- a/OliverBooth/Data/Web/ProgrammingLanguage.cs +++ b/OliverBooth/Data/Web/ProgrammingLanguage.cs @@ -1,3 +1,5 @@ +using OliverBooth.Common.Data.Web; + namespace OliverBooth.Data.Web; /// diff --git a/OliverBooth/Data/Web/Project.cs b/OliverBooth/Data/Web/Project.cs index 05761a7..37b6b9e 100644 --- a/OliverBooth/Data/Web/Project.cs +++ b/OliverBooth/Data/Web/Project.cs @@ -1,3 +1,5 @@ +using OliverBooth.Common.Data.Web; + namespace OliverBooth.Data.Web; /// diff --git a/OliverBooth/Data/Web/Template.cs b/OliverBooth/Data/Web/Template.cs index cf029a1..29355c5 100644 --- a/OliverBooth/Data/Web/Template.cs +++ b/OliverBooth/Data/Web/Template.cs @@ -1,3 +1,5 @@ +using OliverBooth.Common.Data.Web; + namespace OliverBooth.Data.Web; /// diff --git a/OliverBooth/Data/Web/TutorialArticle.cs b/OliverBooth/Data/Web/TutorialArticle.cs index 43ef096..a1f9801 100644 --- a/OliverBooth/Data/Web/TutorialArticle.cs +++ b/OliverBooth/Data/Web/TutorialArticle.cs @@ -1,4 +1,6 @@ using System.ComponentModel.DataAnnotations.Schema; +using OliverBooth.Common.Data; +using OliverBooth.Common.Data.Web; namespace OliverBooth.Data.Web; diff --git a/OliverBooth/Data/Web/TutorialFolder.cs b/OliverBooth/Data/Web/TutorialFolder.cs index 26c5bc1..1f69ef6 100644 --- a/OliverBooth/Data/Web/TutorialFolder.cs +++ b/OliverBooth/Data/Web/TutorialFolder.cs @@ -1,3 +1,6 @@ +using OliverBooth.Common.Data; +using OliverBooth.Common.Data.Web; + namespace OliverBooth.Data.Web; /// diff --git a/OliverBooth/Extensions/HtmlUtility.cs b/OliverBooth/Extensions/HtmlUtility.cs index 94e3d6a..556cb6d 100644 --- a/OliverBooth/Extensions/HtmlUtility.cs +++ b/OliverBooth/Extensions/HtmlUtility.cs @@ -1,8 +1,8 @@ using System.Web; using Cysharp.Text; -using OliverBooth.Data.Blog; -using OliverBooth.Data.Web; -using OliverBooth.Services; +using OliverBooth.Common.Data.Blog; +using OliverBooth.Common.Data.Web; +using OliverBooth.Common.Services; namespace OliverBooth.Extensions; diff --git a/OliverBooth/Markdown/Template/CodeSnippetTemplateRenderer.cs b/OliverBooth/Markdown/Template/CodeSnippetTemplateRenderer.cs index e809801..f93abd8 100644 --- a/OliverBooth/Markdown/Template/CodeSnippetTemplateRenderer.cs +++ b/OliverBooth/Markdown/Template/CodeSnippetTemplateRenderer.cs @@ -1,8 +1,9 @@ using System.Diagnostics; using System.Text; using Markdig; -using OliverBooth.Data.Web; -using OliverBooth.Services; +using OliverBooth.Common.Data.Web; +using OliverBooth.Common.Services; +using OliverBooth.Extensions.Markdig.Markdown.Template; namespace OliverBooth.Markdown.Template; diff --git a/OliverBooth/Markdown/Template/CustomTemplateRenderer.cs b/OliverBooth/Markdown/Template/CustomTemplateRenderer.cs index 1d3f122..9fbbc2a 100644 --- a/OliverBooth/Markdown/Template/CustomTemplateRenderer.cs +++ b/OliverBooth/Markdown/Template/CustomTemplateRenderer.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using OliverBooth.Data.Web; +using OliverBooth.Extensions.Markdig.Markdown.Template; namespace OliverBooth.Markdown.Template; diff --git a/OliverBooth/OliverBooth.csproj b/OliverBooth/OliverBooth.csproj index 2922e7a..463e9d9 100644 --- a/OliverBooth/OliverBooth.csproj +++ b/OliverBooth/OliverBooth.csproj @@ -30,11 +30,8 @@ - - - @@ -48,7 +45,11 @@ - + + + + + diff --git a/OliverBooth/Pages/Blog/Article.cshtml b/OliverBooth/Pages/Blog/Article.cshtml index bdc3ee5..e34da53 100644 --- a/OliverBooth/Pages/Blog/Article.cshtml +++ b/OliverBooth/Pages/Blog/Article.cshtml @@ -1,9 +1,10 @@ @page "/blog/{year:int}/{month:int}/{day:int}/{slug}" @using Humanizer @using Markdig -@using OliverBooth.Data -@using OliverBooth.Data.Blog -@using OliverBooth.Services +@using Microsoft.AspNetCore.Mvc.TagHelpers +@using OliverBooth.Common.Data +@using OliverBooth.Common.Data.Blog +@using OliverBooth.Common.Services @inject IBlogPostService BlogPostService @inject MarkdownPipeline MarkdownPipeline @model Article diff --git a/OliverBooth/Pages/Blog/Article.cshtml.cs b/OliverBooth/Pages/Blog/Article.cshtml.cs index 17a9337..64d6cc8 100644 --- a/OliverBooth/Pages/Blog/Article.cshtml.cs +++ b/OliverBooth/Pages/Blog/Article.cshtml.cs @@ -1,8 +1,8 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.Primitives; -using OliverBooth.Data.Blog; -using OliverBooth.Services; +using OliverBooth.Common.Data.Blog; +using OliverBooth.Common.Services; using BC = BCrypt.Net.BCrypt; namespace OliverBooth.Pages.Blog; diff --git a/OliverBooth/Pages/Blog/Index.cshtml b/OliverBooth/Pages/Blog/Index.cshtml index 0229989..23024e7 100644 --- a/OliverBooth/Pages/Blog/Index.cshtml +++ b/OliverBooth/Pages/Blog/Index.cshtml @@ -1,13 +1,13 @@ @page @using Humanizer -@using OliverBooth.Data.Mastodon -@using OliverBooth.Services +@using OliverBooth.Common.Data.Mastodon +@using OliverBooth.Common.Services @model Index @inject IMastodonService MastodonService @{ ViewData["Title"] = "Blog"; - MastodonStatus latestStatus = MastodonService.GetLatestStatus(); + IMastodonStatus latestStatus = MastodonService.GetLatestStatus(); }
@@ -18,16 +18,22 @@ switch (attachment.Type) { case AttachmentType.Audio: -

+

+ +

break; case AttachmentType.Video: -

+

+ +

break; case AttachmentType.Image: case AttachmentType.GifV: -

+

+ +

break; } } diff --git a/OliverBooth/Pages/Blog/Index.cshtml.cs b/OliverBooth/Pages/Blog/Index.cshtml.cs index 97104b6..5fa0b34 100644 --- a/OliverBooth/Pages/Blog/Index.cshtml.cs +++ b/OliverBooth/Pages/Blog/Index.cshtml.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using OliverBooth.Data.Blog; -using OliverBooth.Services; +using OliverBooth.Common.Data.Blog; +using OliverBooth.Common.Services; namespace OliverBooth.Pages.Blog; diff --git a/OliverBooth/Pages/Blog/RawArticle.cshtml.cs b/OliverBooth/Pages/Blog/RawArticle.cshtml.cs index 1fc34eb..adbd374 100644 --- a/OliverBooth/Pages/Blog/RawArticle.cshtml.cs +++ b/OliverBooth/Pages/Blog/RawArticle.cshtml.cs @@ -1,8 +1,8 @@ using Cysharp.Text; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using OliverBooth.Data.Blog; -using OliverBooth.Services; +using OliverBooth.Common.Data.Blog; +using OliverBooth.Common.Services; namespace OliverBooth.Pages.Blog; diff --git a/OliverBooth/Pages/Books.cshtml b/OliverBooth/Pages/Books.cshtml index dd83d7e..f7bbb79 100644 --- a/OliverBooth/Pages/Books.cshtml +++ b/OliverBooth/Pages/Books.cshtml @@ -1,5 +1,5 @@ @page -@using OliverBooth.Data.Web +@using OliverBooth.Common.Data.Web @model OliverBooth.Pages.Books @{ ViewData["Title"] = "Reading List"; diff --git a/OliverBooth/Pages/Books.cshtml.cs b/OliverBooth/Pages/Books.cshtml.cs index 6cbc5af..6668ed3 100644 --- a/OliverBooth/Pages/Books.cshtml.cs +++ b/OliverBooth/Pages/Books.cshtml.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc.RazorPages; -using OliverBooth.Data.Web; -using OliverBooth.Services; +using OliverBooth.Common.Data.Web; +using OliverBooth.Common.Services; namespace OliverBooth.Pages; diff --git a/OliverBooth/Pages/Contact/Blacklist.cshtml b/OliverBooth/Pages/Contact/Blacklist.cshtml index a0101ac..20feb7a 100644 --- a/OliverBooth/Pages/Contact/Blacklist.cshtml +++ b/OliverBooth/Pages/Contact/Blacklist.cshtml @@ -1,6 +1,7 @@ @page -@using OliverBooth.Data.Web -@using OliverBooth.Services +@using Microsoft.AspNetCore.Mvc.TagHelpers +@using OliverBooth.Common.Data.Web +@using OliverBooth.Common.Services @inject IContactService ContactService @{ ViewData["Title"] = "Blacklist"; diff --git a/OliverBooth/Pages/Contact/Index.cshtml b/OliverBooth/Pages/Contact/Index.cshtml index 12d5f70..95bdd86 100644 --- a/OliverBooth/Pages/Contact/Index.cshtml +++ b/OliverBooth/Pages/Contact/Index.cshtml @@ -1,4 +1,5 @@ @page +@using Microsoft.AspNetCore.Mvc.TagHelpers @{ ViewData["Title"] = "Contact"; } diff --git a/OliverBooth/Pages/Contact/Result.cshtml b/OliverBooth/Pages/Contact/Result.cshtml index 65de504..bc5b79d 100644 --- a/OliverBooth/Pages/Contact/Result.cshtml +++ b/OliverBooth/Pages/Contact/Result.cshtml @@ -1,4 +1,5 @@ @page +@using Microsoft.AspNetCore.Mvc.TagHelpers @model OliverBooth.Pages.Contact.Result @{ diff --git a/OliverBooth/Pages/Index.cshtml b/OliverBooth/Pages/Index.cshtml index 0f85b37..89821cc 100644 --- a/OliverBooth/Pages/Index.cshtml +++ b/OliverBooth/Pages/Index.cshtml @@ -1,4 +1,5 @@ @page +@using Microsoft.AspNetCore.Mvc.TagHelpers
diff --git a/OliverBooth/Pages/Privacy/FiveOClockSomewhere.cshtml b/OliverBooth/Pages/Privacy/FiveOClockSomewhere.cshtml index 5c5365a..07fe6a5 100644 --- a/OliverBooth/Pages/Privacy/FiveOClockSomewhere.cshtml +++ b/OliverBooth/Pages/Privacy/FiveOClockSomewhere.cshtml @@ -1,4 +1,5 @@ @page "/privacy/five-oclock-somewhere" +@using Microsoft.AspNetCore.Mvc.TagHelpers @{ ViewData["Title"] = "It's 5 O'Clock Somewhere Privacy Policy"; } diff --git a/OliverBooth/Pages/Privacy/GooglePlay.cshtml b/OliverBooth/Pages/Privacy/GooglePlay.cshtml index 37caca9..9d340c5 100644 --- a/OliverBooth/Pages/Privacy/GooglePlay.cshtml +++ b/OliverBooth/Pages/Privacy/GooglePlay.cshtml @@ -1,4 +1,5 @@ @page "/privacy/google-play" +@using Microsoft.AspNetCore.Mvc.TagHelpers @{ ViewData["Title"] = "Google Play Privacy Policy"; } diff --git a/OliverBooth/Pages/Privacy/Index.cshtml b/OliverBooth/Pages/Privacy/Index.cshtml index d4afbd3..d5ef608 100644 --- a/OliverBooth/Pages/Privacy/Index.cshtml +++ b/OliverBooth/Pages/Privacy/Index.cshtml @@ -1,4 +1,5 @@ @page +@using Microsoft.AspNetCore.Mvc.TagHelpers @{ ViewData["Title"] = "Privacy Policy"; } diff --git a/OliverBooth/Pages/Projects/Index.cshtml b/OliverBooth/Pages/Projects/Index.cshtml index 90468e5..a9e8f77 100644 --- a/OliverBooth/Pages/Projects/Index.cshtml +++ b/OliverBooth/Pages/Projects/Index.cshtml @@ -1,6 +1,7 @@ @page -@using OliverBooth.Data.Web -@using OliverBooth.Services +@using Microsoft.AspNetCore.Mvc.TagHelpers +@using OliverBooth.Common.Data.Web +@using OliverBooth.Common.Services @inject IProjectService ProjectService @{ ViewData["Title"] = "Projects"; diff --git a/OliverBooth/Pages/Projects/Project.cshtml b/OliverBooth/Pages/Projects/Project.cshtml index b9c9933..80141dc 100644 --- a/OliverBooth/Pages/Projects/Project.cshtml +++ b/OliverBooth/Pages/Projects/Project.cshtml @@ -1,7 +1,8 @@ @page "/project/{slug}" @using Markdig -@using OliverBooth.Data.Web -@using OliverBooth.Services +@using Microsoft.AspNetCore.Mvc.TagHelpers +@using OliverBooth.Common.Data.Web +@using OliverBooth.Common.Services @model Project @inject IProjectService ProjectService @inject MarkdownPipeline MarkdownPipeline diff --git a/OliverBooth/Pages/Projects/Project.cshtml.cs b/OliverBooth/Pages/Projects/Project.cshtml.cs index 87782ec..a762e6d 100644 --- a/OliverBooth/Pages/Projects/Project.cshtml.cs +++ b/OliverBooth/Pages/Projects/Project.cshtml.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Mvc.RazorPages; -using OliverBooth.Data.Web; -using OliverBooth.Services; +using OliverBooth.Common.Data.Web; +using OliverBooth.Common.Services; namespace OliverBooth.Pages.Projects; diff --git a/OliverBooth/Pages/Shared/_Layout.cshtml b/OliverBooth/Pages/Shared/_Layout.cshtml index db84eee..4622e8f 100644 --- a/OliverBooth/Pages/Shared/_Layout.cshtml +++ b/OliverBooth/Pages/Shared/_Layout.cshtml @@ -1,7 +1,8 @@ -@using OliverBooth.Data.Blog -@using OliverBooth.Data.Web +@using Microsoft.AspNetCore.Mvc.TagHelpers +@using OliverBooth.Common.Data.Blog +@using OliverBooth.Common.Data.Web +@using OliverBooth.Common.Services @using OliverBooth.Extensions -@using OliverBooth.Services @inject IBlogPostService BlogPostService @inject ITutorialService TutorialService @{ diff --git a/OliverBooth/Pages/Shared/_MinimalLayout.cshtml b/OliverBooth/Pages/Shared/_MinimalLayout.cshtml index 48fd1a8..b40e15f 100644 --- a/OliverBooth/Pages/Shared/_MinimalLayout.cshtml +++ b/OliverBooth/Pages/Shared/_MinimalLayout.cshtml @@ -1,3 +1,4 @@ +@using Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/OliverBooth/Pages/Tutorials/Article.cshtml b/OliverBooth/Pages/Tutorials/Article.cshtml index 2970273..0433dbb 100644 --- a/OliverBooth/Pages/Tutorials/Article.cshtml +++ b/OliverBooth/Pages/Tutorials/Article.cshtml @@ -2,10 +2,10 @@ @using Humanizer @using Markdig @using Microsoft.AspNetCore.Mvc.TagHelpers -@using OliverBooth.Data -@using OliverBooth.Data.Blog -@using OliverBooth.Data.Web -@using OliverBooth.Services +@using OliverBooth.Common.Data +@using OliverBooth.Common.Data.Blog +@using OliverBooth.Common.Data.Web +@using OliverBooth.Common.Services @inject ITutorialService TutorialService @inject MarkdownPipeline MarkdownPipeline @model Article diff --git a/OliverBooth/Pages/Tutorials/Article.cshtml.cs b/OliverBooth/Pages/Tutorials/Article.cshtml.cs index 7f5f6c7..2b87435 100644 --- a/OliverBooth/Pages/Tutorials/Article.cshtml.cs +++ b/OliverBooth/Pages/Tutorials/Article.cshtml.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using OliverBooth.Data.Web; -using OliverBooth.Services; +using OliverBooth.Common.Data.Web; +using OliverBooth.Common.Services; namespace OliverBooth.Pages.Tutorials; diff --git a/OliverBooth/Pages/Tutorials/Index.cshtml b/OliverBooth/Pages/Tutorials/Index.cshtml index aaa2346..06f7a7a 100644 --- a/OliverBooth/Pages/Tutorials/Index.cshtml +++ b/OliverBooth/Pages/Tutorials/Index.cshtml @@ -1,8 +1,9 @@ @page "/tutorials/{**slug}" @using System.Text -@using OliverBooth.Data -@using OliverBooth.Data.Web -@using OliverBooth.Services +@using Microsoft.AspNetCore.Mvc.TagHelpers +@using OliverBooth.Common.Data +@using OliverBooth.Common.Data.Web +@using OliverBooth.Common.Services @model Index @inject ITutorialService TutorialService @{ diff --git a/OliverBooth/Pages/Tutorials/Index.cshtml.cs b/OliverBooth/Pages/Tutorials/Index.cshtml.cs index ac08919..23892fd 100644 --- a/OliverBooth/Pages/Tutorials/Index.cshtml.cs +++ b/OliverBooth/Pages/Tutorials/Index.cshtml.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using OliverBooth.Data.Web; -using OliverBooth.Services; +using OliverBooth.Common.Data.Web; +using OliverBooth.Common.Services; namespace OliverBooth.Pages.Tutorials; diff --git a/OliverBooth/Program.cs b/OliverBooth/Program.cs index 6360e7a..006a3b7 100644 --- a/OliverBooth/Program.cs +++ b/OliverBooth/Program.cs @@ -1,11 +1,12 @@ using AspNetCore.ReCaptcha; using Markdig; +using OliverBooth.Common.Services; using OliverBooth.Data.Blog; using OliverBooth.Data.Web; using OliverBooth.Extensions; +using OliverBooth.Extensions.Markdig; +using OliverBooth.Extensions.Markdig.Services; using OliverBooth.Markdown; -using OliverBooth.Markdown.Callout; -using OliverBooth.Markdown.Template; using OliverBooth.Markdown.Timestamp; using OliverBooth.Services; using Serilog; @@ -25,7 +26,7 @@ builder.Logging.AddSerilog(); builder.Services.AddSingleton(provider => new MarkdownPipelineBuilder() .Use() - .Use(new TemplateExtension(provider.GetRequiredService())) + .UseTemplates(provider.GetRequiredService()) // we have our own "alert blocks" .UseCallouts() diff --git a/OliverBooth/Services/BlogPostService.cs b/OliverBooth/Services/BlogPostService.cs index 38985d3..2190f09 100644 --- a/OliverBooth/Services/BlogPostService.cs +++ b/OliverBooth/Services/BlogPostService.cs @@ -2,7 +2,9 @@ using System.Diagnostics.CodeAnalysis; using Humanizer; using Markdig; using Microsoft.EntityFrameworkCore; -using OliverBooth.Data; +using OliverBooth.Common.Data; +using OliverBooth.Common.Data.Blog; +using OliverBooth.Common.Services; using OliverBooth.Data.Blog; namespace OliverBooth.Services; diff --git a/OliverBooth/Services/BlogUserService.cs b/OliverBooth/Services/BlogUserService.cs index 9b797eb..f7e9137 100644 --- a/OliverBooth/Services/BlogUserService.cs +++ b/OliverBooth/Services/BlogUserService.cs @@ -1,6 +1,8 @@ using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore; +using OliverBooth.Common.Data.Blog; +using OliverBooth.Common.Services; using OliverBooth.Data.Blog; namespace OliverBooth.Services; diff --git a/OliverBooth/Services/CodeSnippetService.cs b/OliverBooth/Services/CodeSnippetService.cs index 643f6ea..0c1f6d6 100644 --- a/OliverBooth/Services/CodeSnippetService.cs +++ b/OliverBooth/Services/CodeSnippetService.cs @@ -1,5 +1,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore; +using OliverBooth.Common.Data.Web; +using OliverBooth.Common.Services; using OliverBooth.Data.Web; namespace OliverBooth.Services; diff --git a/OliverBooth/Services/ContactService.cs b/OliverBooth/Services/ContactService.cs index de2b591..21cb751 100644 --- a/OliverBooth/Services/ContactService.cs +++ b/OliverBooth/Services/ContactService.cs @@ -1,4 +1,6 @@ using Microsoft.EntityFrameworkCore; +using OliverBooth.Common.Data.Web; +using OliverBooth.Common.Services; using OliverBooth.Data.Web; namespace OliverBooth.Services; diff --git a/OliverBooth/Services/MastodonService.cs b/OliverBooth/Services/MastodonService.cs index 121a6cc..b169e2b 100644 --- a/OliverBooth/Services/MastodonService.cs +++ b/OliverBooth/Services/MastodonService.cs @@ -1,6 +1,8 @@ using System.Text.Json; using System.Text.Json.Serialization; using HtmlAgilityPack; +using OliverBooth.Common.Data.Mastodon; +using OliverBooth.Common.Services; using OliverBooth.Data.Mastodon; namespace OliverBooth.Services; @@ -21,7 +23,7 @@ internal sealed class MastodonService : IMastodonService } /// - public MastodonStatus GetLatestStatus() + public IMastodonStatus GetLatestStatus() { string token = Environment.GetEnvironmentVariable("MASTODON_TOKEN") ?? string.Empty; string account = Environment.GetEnvironmentVariable("MASTODON_ACCOUNT") ?? string.Empty; diff --git a/OliverBooth/Services/ProgrammingLanguageService.cs b/OliverBooth/Services/ProgrammingLanguageService.cs index 3f82bbc..d0f1c4d 100644 --- a/OliverBooth/Services/ProgrammingLanguageService.cs +++ b/OliverBooth/Services/ProgrammingLanguageService.cs @@ -1,21 +1,9 @@ using Microsoft.EntityFrameworkCore; +using OliverBooth.Common.Services; using OliverBooth.Data.Web; namespace OliverBooth.Services; -/// -/// Represents a service which can perform programming language lookup. -/// -public interface IProgrammingLanguageService -{ - /// - /// Returns the human-readable name of a language. - /// - /// The alias of the language. - /// The human-readable name, or if the name could not be found. - string GetLanguageName(string alias); -} - /// internal sealed class ProgrammingLanguageService : IProgrammingLanguageService { diff --git a/OliverBooth/Services/ProjectService.cs b/OliverBooth/Services/ProjectService.cs index 6f75ca5..75eff1b 100644 --- a/OliverBooth/Services/ProjectService.cs +++ b/OliverBooth/Services/ProjectService.cs @@ -2,6 +2,8 @@ using System.Diagnostics.CodeAnalysis; using Humanizer; using Markdig; using Microsoft.EntityFrameworkCore; +using OliverBooth.Common.Data.Web; +using OliverBooth.Common.Services; using OliverBooth.Data.Web; namespace OliverBooth.Services; diff --git a/OliverBooth/Services/ReadingListService.cs b/OliverBooth/Services/ReadingListService.cs index 54da5fb..705f056 100644 --- a/OliverBooth/Services/ReadingListService.cs +++ b/OliverBooth/Services/ReadingListService.cs @@ -1,4 +1,6 @@ using Microsoft.EntityFrameworkCore; +using OliverBooth.Common.Data.Web; +using OliverBooth.Common.Services; using OliverBooth.Data.Web; namespace OliverBooth.Services; diff --git a/OliverBooth/Services/TemplateService.cs b/OliverBooth/Services/TemplateService.cs index 623a1fd..90bc47a 100644 --- a/OliverBooth/Services/TemplateService.cs +++ b/OliverBooth/Services/TemplateService.cs @@ -1,9 +1,10 @@ using System.Buffers.Binary; -using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; -using Markdig; using Microsoft.EntityFrameworkCore; +using OliverBooth.Common.Data.Web; using OliverBooth.Data.Web; +using OliverBooth.Extensions.Markdig.Markdown.Template; +using OliverBooth.Extensions.Markdig.Services; using OliverBooth.Formatting; using OliverBooth.Markdown.Template; using SmartFormat; diff --git a/OliverBooth/Services/TutorialService.cs b/OliverBooth/Services/TutorialService.cs index 3fd21ed..4a1e387 100644 --- a/OliverBooth/Services/TutorialService.cs +++ b/OliverBooth/Services/TutorialService.cs @@ -3,7 +3,10 @@ using Cysharp.Text; using Humanizer; using Markdig; using Microsoft.EntityFrameworkCore; -using OliverBooth.Data; +using OliverBooth.Common.Data; +using OliverBooth.Common.Data.Blog; +using OliverBooth.Common.Data.Web; +using OliverBooth.Common.Services; using OliverBooth.Data.Blog; using OliverBooth.Data.Web; From dec9307f1d11fa766d428764e24251b81f4b362a Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sun, 5 May 2024 02:21:40 +0100 Subject: [PATCH 02/12] refactor: amend 6ec4103a3a84117a4072cc7ba0af38b0fadca818 Remove unused Markdown classes from within core web app project --- .../Markdown/MarkdownExtensions.cs | 21 --- .../MarkdownPipelineExtensions.cs | 18 ++ OliverBooth/Markdown/Callout/CalloutBlock.cs | 43 ----- .../Markdown/Callout/CalloutExtension.cs | 32 ---- .../Markdown/Callout/CalloutInlineParser.cs | 176 ------------------ .../Markdown/Callout/CalloutRenderer.cs | 119 ------------ OliverBooth/Markdown/MarkdownExtensions.cs | 21 --- .../Markdown/Timestamp/TimestampExtension.cs | 25 --- .../Markdown/Timestamp/TimestampFormat.cs | 42 ----- .../Markdown/Timestamp/TimestampInline.cs | 21 --- .../Timestamp/TimestampInlineParser.cs | 91 --------- .../Markdown/Timestamp/TimestampRenderer.cs | 55 ------ OliverBooth/Program.cs | 3 +- 13 files changed, 19 insertions(+), 648 deletions(-) delete mode 100644 OliverBooth.Extensions.Markdig/Markdown/MarkdownExtensions.cs delete mode 100644 OliverBooth/Markdown/Callout/CalloutBlock.cs delete mode 100644 OliverBooth/Markdown/Callout/CalloutExtension.cs delete mode 100644 OliverBooth/Markdown/Callout/CalloutInlineParser.cs delete mode 100644 OliverBooth/Markdown/Callout/CalloutRenderer.cs delete mode 100644 OliverBooth/Markdown/MarkdownExtensions.cs delete mode 100644 OliverBooth/Markdown/Timestamp/TimestampExtension.cs delete mode 100644 OliverBooth/Markdown/Timestamp/TimestampFormat.cs delete mode 100644 OliverBooth/Markdown/Timestamp/TimestampInline.cs delete mode 100644 OliverBooth/Markdown/Timestamp/TimestampInlineParser.cs delete mode 100644 OliverBooth/Markdown/Timestamp/TimestampRenderer.cs diff --git a/OliverBooth.Extensions.Markdig/Markdown/MarkdownExtensions.cs b/OliverBooth.Extensions.Markdig/Markdown/MarkdownExtensions.cs deleted file mode 100644 index cd54de4..0000000 --- a/OliverBooth.Extensions.Markdig/Markdown/MarkdownExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Markdig; -using OliverBooth.Extensions.Markdig.Markdown.Callout; - -namespace OliverBooth.Extensions.Markdig.Markdown; - -/// -/// Extension methods for . -/// -internal static class MarkdownExtensions -{ - /// - /// Uses this extension to enable Obsidian-style callouts. - /// - /// The pipeline. - /// The modified pipeline. - public static MarkdownPipelineBuilder UseCallouts(this MarkdownPipelineBuilder pipeline) - { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; - } -} diff --git a/OliverBooth.Extensions.Markdig/MarkdownPipelineExtensions.cs b/OliverBooth.Extensions.Markdig/MarkdownPipelineExtensions.cs index 20ea173..5426937 100644 --- a/OliverBooth.Extensions.Markdig/MarkdownPipelineExtensions.cs +++ b/OliverBooth.Extensions.Markdig/MarkdownPipelineExtensions.cs @@ -1,4 +1,5 @@ using Markdig; +using OliverBooth.Extensions.Markdig.Markdown.Callout; using OliverBooth.Extensions.Markdig.Markdown.Template; using OliverBooth.Extensions.Markdig.Services; @@ -9,6 +10,23 @@ namespace OliverBooth.Extensions.Markdig; ///
public static class MarkdownPipelineExtensions { + /// + /// Enables the use of Obsidian-style callouts in this pipeline. + /// + /// The Markdig markdown pipeline builder. + /// The modified Markdig markdown pipeline builder. + /// is . + public static MarkdownPipelineBuilder UseCallouts(this MarkdownPipelineBuilder builder) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Extensions.AddIfNotAlready(); + return builder; + } + /// /// Enables the use of Wiki-style templates in this pipeline. /// diff --git a/OliverBooth/Markdown/Callout/CalloutBlock.cs b/OliverBooth/Markdown/Callout/CalloutBlock.cs deleted file mode 100644 index 5c01675..0000000 --- a/OliverBooth/Markdown/Callout/CalloutBlock.cs +++ /dev/null @@ -1,43 +0,0 @@ -using Markdig.Helpers; -using Markdig.Syntax; - -namespace OliverBooth.Markdown.Callout; - -/// -/// Represents a callout block. -/// -internal sealed class CalloutBlock : QuoteBlock -{ - /// - /// Initializes a new instance of the class. - /// - /// The type of the callout. - public CalloutBlock(StringSlice type) : base(null) - { - Type = type; - } - - /// - /// Gets or sets a value indicating whether this callout is foldable. - /// - /// if this callout is foldable; otherwise, . - public bool Foldable { get; set; } - - /// - /// Gets or sets the title of the callout. - /// - /// The title of the callout. - public StringSlice Title { get; set; } - - /// - /// Gets or sets the trailing whitespace trivia. - /// - /// The trailing whitespace trivia. - public StringSlice TrailingWhitespaceTrivia { get; set; } - - /// - /// Gets or sets the type of the callout. - /// - /// The type of the callout. - public StringSlice Type { get; set; } -} diff --git a/OliverBooth/Markdown/Callout/CalloutExtension.cs b/OliverBooth/Markdown/Callout/CalloutExtension.cs deleted file mode 100644 index c9731a2..0000000 --- a/OliverBooth/Markdown/Callout/CalloutExtension.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Markdig; -using Markdig.Parsers.Inlines; -using Markdig.Renderers; -using Markdig.Renderers.Html; - -namespace OliverBooth.Markdown.Callout; - -/// -/// Extension for adding Obsidian-style callouts to a Markdown pipeline. -/// -internal sealed class CalloutExtension : IMarkdownExtension -{ - /// - public void Setup(MarkdownPipelineBuilder pipeline) - { - var parser = pipeline.InlineParsers.Find(); - if (parser is null) - { - pipeline.InlineParsers.InsertBefore(new CalloutInlineParser()); - } - } - - /// - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) - { - var blockRenderer = renderer.ObjectRenderers.FindExact(); - if (blockRenderer is null) - { - renderer.ObjectRenderers.InsertBefore(new CalloutRenderer(pipeline)); - } - } -} diff --git a/OliverBooth/Markdown/Callout/CalloutInlineParser.cs b/OliverBooth/Markdown/Callout/CalloutInlineParser.cs deleted file mode 100644 index ce7ddc5..0000000 --- a/OliverBooth/Markdown/Callout/CalloutInlineParser.cs +++ /dev/null @@ -1,176 +0,0 @@ -using System.Reflection; -using Cysharp.Text; -using Markdig.Helpers; -using Markdig.Parsers; -using Markdig.Renderers.Html; -using Markdig.Syntax; - -namespace OliverBooth.Markdown.Callout; - -/// -/// An inline parser for Obsidian-style callouts ([!NOTE] etc.) -/// -internal sealed class CalloutInlineParser : InlineParser -{ - // ugly hack to access internal method - private static readonly MethodInfo ReplaceParentContainerMethod = - typeof(InlineProcessor).GetMethod("ReplaceParentContainer", BindingFlags.Instance | BindingFlags.NonPublic)!; - - /// - /// Initializes a new instance of the class. - /// - public CalloutInlineParser() - { - OpeningCharacters = ['[']; - } - - /// - public override bool Match(InlineProcessor processor, ref StringSlice slice) - { - // We expect the alert to be the first child of a quote block. Example: - // > [!NOTE] - // > This is a note - if (processor.Block is not ParagraphBlock { Parent: QuoteBlock quoteBlock } paragraphBlock || - paragraphBlock.Inline?.FirstChild != null) - { - return false; - } - - StringSlice cache = slice; - char current = slice.NextChar(); - - if (current != '!') - { - slice = cache; - return false; - } - - current = slice.NextChar(); // skip ! - - int start = slice.Start; - int end = start; - - while (current.IsAlphaUpper()) - { - end = slice.Start; - current = slice.NextChar(); - } - - if (current != ']' || start == end) - { - slice = cache; - return false; - } - - var type = new StringSlice(slice.Text, start, end); - current = slice.NextChar(); // skip ] - start = slice.Start; - - bool fold = false; - if (current == '-') - { - fold = true; - current = slice.NextChar(); // skip - - start = slice.Start; - } - - ReadTitle(current, ref slice, out StringSlice title, out end); - - var callout = new CalloutBlock(type) - { - Foldable = fold, - Span = quoteBlock.Span, - TrailingWhitespaceTrivia = new StringSlice(slice.Text, start, end), - Line = quoteBlock.Line, - Column = quoteBlock.Column, - Title = title - }; - - AddAttributes(callout, type); - ReplaceQuoteBlock(processor, quoteBlock, callout); - return true; - } - - private static void ReadTitle(char startChar, ref StringSlice slice, out StringSlice title, out int end) - { - using Utf16ValueStringBuilder builder = ZString.CreateStringBuilder(); - - char current = startChar; - while (true) - { - if (current is not ('\0' or '\r' or '\n')) - { - builder.Append(current); - current = slice.NextChar(); - continue; - } - - end = slice.Start; - if (HandleCharacter(ref slice, ref end, ref current)) - { - continue; - } - - break; - } - - title = new StringSlice(builder.ToString(), 0, builder.Length); - } - - private static bool HandleCharacter(ref StringSlice slice, ref int end, ref char current) - { - switch (current) - { - case '\r': - current = slice.NextChar(); // skip \r - - if (current is not ('\0' or '\n')) - { - return true; - } - - end = slice.Start; - if (current == '\n') - { - slice.NextChar(); // skip \n - } - - break; - - case '\n': - slice.NextChar(); // skip \n - break; - } - - return false; - } - - private static void AddAttributes(IMarkdownObject callout, StringSlice type) - { - HtmlAttributes attributes = callout.GetAttributes(); - attributes.AddClass("callout"); - attributes.AddProperty("data-callout", type.AsSpan().ToString().ToLowerInvariant()); - } - - private static void ReplaceQuoteBlock(InlineProcessor processor, QuoteBlock quoteBlock, CalloutBlock callout) - { - ContainerBlock? parentQuoteBlock = quoteBlock.Parent; - if (parentQuoteBlock is null) - { - return; - } - - int indexOfQuoteBlock = parentQuoteBlock.IndexOf(quoteBlock); - parentQuoteBlock[indexOfQuoteBlock] = callout; - - while (quoteBlock.Count > 0) - { - var block = quoteBlock[0]; - quoteBlock.RemoveAt(0); - callout.Add(block); - } - - ReplaceParentContainerMethod.Invoke(processor, [quoteBlock, callout]); - // ReplaceParentContainer(processor, quoteBlock, callout); - } -} diff --git a/OliverBooth/Markdown/Callout/CalloutRenderer.cs b/OliverBooth/Markdown/Callout/CalloutRenderer.cs deleted file mode 100644 index f2e4f77..0000000 --- a/OliverBooth/Markdown/Callout/CalloutRenderer.cs +++ /dev/null @@ -1,119 +0,0 @@ -using HtmlAgilityPack; -using Humanizer; -using Markdig; -using Markdig.Renderers; -using Markdig.Renderers.Html; - -namespace OliverBooth.Markdown.Callout; - -/// -/// Represents an HTML renderer which renders a . -/// -internal sealed class CalloutRenderer : HtmlObjectRenderer -{ - private readonly MarkdownPipeline _pipeline; - - private static readonly Dictionary CalloutTypes = new() - { - ["NOTE"] = "pencil", - ["ABSTRACT"] = "clipboard-list", - ["INFO"] = "info", - ["TODO"] = "circle-check", - ["TIP"] = "flame", - ["IMPORTANT"] = "flame", - ["SUCCESS"] = "check", - ["QUESTION"] = "circle-help", - ["WARNING"] = "triangle-alert", - ["FAILURE"] = "x", - ["DANGER"] = "zap", - ["BUG"] = "bug", - ["EXAMPLE"] = "list", - ["CITE"] = "quote", - ["UPDATE"] = "calendar-check", - }; - - public CalloutRenderer(MarkdownPipeline pipeline) - { - _pipeline = pipeline; - } - - /// - protected override void Write(HtmlRenderer renderer, CalloutBlock block) - { - renderer.EnsureLine(); - if (renderer.EnableHtmlForBlock) - { - RenderAsHtml(renderer, block, _pipeline); - } - else - { - RenderAsText(renderer, block); - } - - renderer.EnsureLine(); - } - - private static void RenderAsHtml(HtmlRenderer renderer, CalloutBlock block, MarkdownPipeline pipeline) - { - string title = block.Title.Text; - ReadOnlySpan type = block.Type.AsSpan(); - Span upperType = stackalloc char[type.Length]; - type.ToUpperInvariant(upperType); - - if (!CalloutTypes.TryGetValue(upperType.ToString(), out string? lucideClass)) - { - lucideClass = "pencil"; - } - - var typeString = type.ToString().ToLowerInvariant(); - - renderer.Write($"
'); - renderer.Write("
"); - - string calloutTitle = title.Length == 0 ? typeString.Humanize(LetterCasing.Sentence) : title; - WriteTitle(renderer, pipeline, calloutTitle); - - if (block.Foldable) - { - renderer.Write(""); - } - - renderer.WriteLine("
"); - renderer.Write("
"); - renderer.WriteChildren(block); - renderer.WriteLine("
"); - renderer.WriteLine("
"); - renderer.EnsureLine(); - } - - private static void WriteTitle(TextRendererBase renderer, MarkdownPipeline pipeline, string calloutTitle) - { - string html = Markdig.Markdown.ToHtml(calloutTitle, pipeline); - var document = new HtmlDocument(); - document.LoadHtml(html); - if (document.DocumentNode.FirstChild is { Name: "p" } child) - { - // ugly hack to remove

tag generated by Markdig - document.DocumentNode.InnerHtml = child.InnerHtml; - } - - document.Save(renderer.Writer); - } - - private static void RenderAsText(HtmlRenderer renderer, CalloutBlock block) - { - string title = block.Title.Text; - ReadOnlySpan type = block.Type.AsSpan(); - renderer.WriteLine(title.Length == 0 ? type.ToString().ToUpperInvariant() : title.ToUpperInvariant()); - renderer.WriteChildren(block); - renderer.EnsureLine(); - } -} diff --git a/OliverBooth/Markdown/MarkdownExtensions.cs b/OliverBooth/Markdown/MarkdownExtensions.cs deleted file mode 100644 index d63d869..0000000 --- a/OliverBooth/Markdown/MarkdownExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Markdig; -using OliverBooth.Markdown.Callout; - -namespace OliverBooth.Markdown; - -///

-/// Extension methods for . -/// -internal static class MarkdownExtensions -{ - /// - /// Uses this extension to enable Obsidian-style callouts. - /// - /// The pipeline. - /// The modified pipeline. - public static MarkdownPipelineBuilder UseCallouts(this MarkdownPipelineBuilder pipeline) - { - pipeline.Extensions.AddIfNotAlready(); - return pipeline; - } -} diff --git a/OliverBooth/Markdown/Timestamp/TimestampExtension.cs b/OliverBooth/Markdown/Timestamp/TimestampExtension.cs deleted file mode 100644 index 45ae2df..0000000 --- a/OliverBooth/Markdown/Timestamp/TimestampExtension.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Markdig; -using Markdig.Renderers; - -namespace OliverBooth.Markdown.Timestamp; - -/// -/// Represents a Markdig extension that supports Discord-style timestamps. -/// -public class TimestampExtension : IMarkdownExtension -{ - /// - public void Setup(MarkdownPipelineBuilder pipeline) - { - pipeline.InlineParsers.AddIfNotAlready(); - } - - /// - public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) - { - if (renderer is HtmlRenderer htmlRenderer) - { - htmlRenderer.ObjectRenderers.AddIfNotAlready(); - } - } -} diff --git a/OliverBooth/Markdown/Timestamp/TimestampFormat.cs b/OliverBooth/Markdown/Timestamp/TimestampFormat.cs deleted file mode 100644 index 29eca6b..0000000 --- a/OliverBooth/Markdown/Timestamp/TimestampFormat.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace OliverBooth.Markdown.Timestamp; - -/// -/// An enumeration of timestamp formats. -/// -public enum TimestampFormat -{ - /// - /// Short time format. Example: 12:00 - /// - ShortTime = 't', - - /// - /// Long time format. Example: 12:00:00 - /// - LongTime = 'T', - - /// - /// Short date format. Example: 1/1/2000 - /// - ShortDate = 'd', - - /// - /// Long date format. Example: 1 January 2000 - /// - LongDate = 'D', - - /// - /// Short date/time format. Example: 1 January 2000 at 12:00 - /// - LongDateShortTime = 'f', - - /// - /// Long date/time format. Example: Saturday, 1 January 2000 at 12:00 - /// - LongDateTime = 'F', - - /// - /// Relative date/time format. Example: 1 second ago - /// - Relative = 'R', -} diff --git a/OliverBooth/Markdown/Timestamp/TimestampInline.cs b/OliverBooth/Markdown/Timestamp/TimestampInline.cs deleted file mode 100644 index 615ee03..0000000 --- a/OliverBooth/Markdown/Timestamp/TimestampInline.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Markdig.Syntax.Inlines; - -namespace OliverBooth.Markdown.Timestamp; - -/// -/// Represents a Markdown inline element that contains a timestamp. -/// -public sealed class TimestampInline : Inline -{ - /// - /// Gets or sets the format. - /// - /// The format. - public TimestampFormat Format { get; set; } - - /// - /// Gets or sets the timestamp. - /// - /// The timestamp. - public DateTimeOffset Timestamp { get; set; } -} diff --git a/OliverBooth/Markdown/Timestamp/TimestampInlineParser.cs b/OliverBooth/Markdown/Timestamp/TimestampInlineParser.cs deleted file mode 100644 index 414fc48..0000000 --- a/OliverBooth/Markdown/Timestamp/TimestampInlineParser.cs +++ /dev/null @@ -1,91 +0,0 @@ -using Markdig.Helpers; -using Markdig.Parsers; - -namespace OliverBooth.Markdown.Timestamp; - -/// -/// Represents a Markdown inline parser that matches Discord-style timestamps. -/// -public sealed class TimestampInlineParser : InlineParser -{ - /// - /// Initializes a new instance of the class. - /// - public TimestampInlineParser() - { - OpeningCharacters = new[] { '<' }; - } - - /// - public override bool Match(InlineProcessor processor, ref StringSlice slice) - { - // Previous char must be a space - if (!slice.PeekCharExtra(-1).IsWhiteSpaceOrZero()) - { - return false; - } - - ReadOnlySpan span = slice.Text.AsSpan(slice.Start, slice.Length); - - if (!TryConsumeTimestamp(span, out ReadOnlySpan rawTimestamp, out char format)) - { - return false; - } - - if (!long.TryParse(rawTimestamp, out long timestamp)) - { - return false; - } - - bool hasFormat = format != '\0'; - processor.Inline = new TimestampInline - { - Format = (TimestampFormat)format, - Timestamp = DateTimeOffset.FromUnixTimeSeconds(timestamp) - }; - - int paddingCount = hasFormat ? 6 : 4; // or optionally - slice.Start += rawTimestamp.Length + paddingCount; - return true; - } - - private bool TryConsumeTimestamp(ReadOnlySpan source, - out ReadOnlySpan timestamp, - out char format) - { - timestamp = default; - format = default; - - if (!source.StartsWith("') == -1) - { - timestamp = default; - return false; - } - - int delimiterIndex = timestamp.IndexOf(':'); - if (delimiterIndex == 0) - { - // invalid format - timestamp = default; - return false; - } - - if (delimiterIndex == -1) - { - // no format, default to relative - format = 'R'; - timestamp = timestamp[..^1]; // trim > - } - else - { - // use specified format - format = timestamp[^2]; - timestamp = timestamp[..^3]; - } - - return true; - } -} diff --git a/OliverBooth/Markdown/Timestamp/TimestampRenderer.cs b/OliverBooth/Markdown/Timestamp/TimestampRenderer.cs deleted file mode 100644 index b2c7f3d..0000000 --- a/OliverBooth/Markdown/Timestamp/TimestampRenderer.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.ComponentModel; -using Humanizer; -using Markdig.Renderers; -using Markdig.Renderers.Html; - -namespace OliverBooth.Markdown.Timestamp; - -/// -/// Represents a Markdown object renderer that renders elements. -/// -public sealed class TimestampRenderer : HtmlObjectRenderer -{ - /// - protected override void Write(HtmlRenderer renderer, TimestampInline obj) - { - DateTimeOffset timestamp = obj.Timestamp; - TimestampFormat format = obj.Format; - - renderer.Write(""); - - switch (format) - { - case TimestampFormat.LongDate: - renderer.Write(timestamp.ToString("d MMMM yyyy")); - break; - - case TimestampFormat.LongDateShortTime: - renderer.Write(timestamp.ToString(@"d MMMM yyyy \a\t HH:mm")); - break; - - case TimestampFormat.LongDateTime: - renderer.Write(timestamp.ToString(@"dddd, d MMMM yyyy \a\t HH:mm")); - break; - - case TimestampFormat.Relative: - renderer.Write(timestamp.Humanize()); - break; - - case var _ when !Enum.IsDefined(format): - throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(TimestampFormat)); - - default: - renderer.Write(timestamp.ToString(((char)format).ToString())); - break; - } - - renderer.Write(""); - } -} diff --git a/OliverBooth/Program.cs b/OliverBooth/Program.cs index 006a3b7..84203ce 100644 --- a/OliverBooth/Program.cs +++ b/OliverBooth/Program.cs @@ -5,9 +5,8 @@ using OliverBooth.Data.Blog; using OliverBooth.Data.Web; using OliverBooth.Extensions; using OliverBooth.Extensions.Markdig; +using OliverBooth.Extensions.Markdig.Markdown.Timestamp; using OliverBooth.Extensions.Markdig.Services; -using OliverBooth.Markdown; -using OliverBooth.Markdown.Timestamp; using OliverBooth.Services; using Serilog; From 99ff3124c3bd079e19bc09489fbc4766c02c00d7 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sun, 5 May 2024 02:26:50 +0100 Subject: [PATCH 03/12] refactor: move SmartFormat extensions to separate project --- .../DateFormatter.cs | 2 +- .../MarkdownFormatter.cs | 3 ++- .../OliverBooth.Extensions.SmartFormat.csproj | 19 +++++++++++++++++++ OliverBooth.sln | 6 ++++++ OliverBooth/OliverBooth.csproj | 2 +- OliverBooth/Services/TemplateService.cs | 2 +- 6 files changed, 30 insertions(+), 4 deletions(-) rename {OliverBooth/Formatting => OliverBooth.Extensions.SmartFormat}/DateFormatter.cs (95%) rename {OliverBooth/Formatting => OliverBooth.Extensions.SmartFormat}/MarkdownFormatter.cs (92%) create mode 100644 OliverBooth.Extensions.SmartFormat/OliverBooth.Extensions.SmartFormat.csproj diff --git a/OliverBooth/Formatting/DateFormatter.cs b/OliverBooth.Extensions.SmartFormat/DateFormatter.cs similarity index 95% rename from OliverBooth/Formatting/DateFormatter.cs rename to OliverBooth.Extensions.SmartFormat/DateFormatter.cs index 6109cab..7b98f26 100644 --- a/OliverBooth/Formatting/DateFormatter.cs +++ b/OliverBooth.Extensions.SmartFormat/DateFormatter.cs @@ -1,7 +1,7 @@ using System.Globalization; using SmartFormat.Core.Extensions; -namespace OliverBooth.Formatting; +namespace OliverBooth.Extensions.SmartFormat; /// /// Represents a SmartFormat formatter that formats a date. diff --git a/OliverBooth/Formatting/MarkdownFormatter.cs b/OliverBooth.Extensions.SmartFormat/MarkdownFormatter.cs similarity index 92% rename from OliverBooth/Formatting/MarkdownFormatter.cs rename to OliverBooth.Extensions.SmartFormat/MarkdownFormatter.cs index c682e2c..2f38c96 100644 --- a/OliverBooth/Formatting/MarkdownFormatter.cs +++ b/OliverBooth.Extensions.SmartFormat/MarkdownFormatter.cs @@ -1,7 +1,8 @@ using Markdig; +using Microsoft.Extensions.DependencyInjection; using SmartFormat.Core.Extensions; -namespace OliverBooth.Formatting; +namespace OliverBooth.Extensions.SmartFormat; /// /// Represents a SmartFormat formatter that formats markdown. diff --git a/OliverBooth.Extensions.SmartFormat/OliverBooth.Extensions.SmartFormat.csproj b/OliverBooth.Extensions.SmartFormat/OliverBooth.Extensions.SmartFormat.csproj new file mode 100644 index 0000000..af76520 --- /dev/null +++ b/OliverBooth.Extensions.SmartFormat/OliverBooth.Extensions.SmartFormat.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/OliverBooth.sln b/OliverBooth.sln index ea184df..1d052f5 100644 --- a/OliverBooth.sln +++ b/OliverBooth.sln @@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OliverBooth.Extensions.Mark EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OliverBooth.Common", "OliverBooth.Common\OliverBooth.Common.csproj", "{AD231E0F-FAED-4661-963F-EB22F858E148}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OliverBooth.Extensions.SmartFormat", "OliverBooth.Extensions.SmartFormat\OliverBooth.Extensions.SmartFormat.csproj", "{9D56FA9B-B95B-460D-8745-41AABAA8BF61}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -35,6 +37,10 @@ Global {AD231E0F-FAED-4661-963F-EB22F858E148}.Debug|Any CPU.Build.0 = Debug|Any CPU {AD231E0F-FAED-4661-963F-EB22F858E148}.Release|Any CPU.ActiveCfg = Release|Any CPU {AD231E0F-FAED-4661-963F-EB22F858E148}.Release|Any CPU.Build.0 = Release|Any CPU + {9D56FA9B-B95B-460D-8745-41AABAA8BF61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9D56FA9B-B95B-460D-8745-41AABAA8BF61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9D56FA9B-B95B-460D-8745-41AABAA8BF61}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9D56FA9B-B95B-460D-8745-41AABAA8BF61}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution EndGlobalSection diff --git a/OliverBooth/OliverBooth.csproj b/OliverBooth/OliverBooth.csproj index 463e9d9..b00cc91 100644 --- a/OliverBooth/OliverBooth.csproj +++ b/OliverBooth/OliverBooth.csproj @@ -42,7 +42,6 @@ - @@ -50,6 +49,7 @@ + diff --git a/OliverBooth/Services/TemplateService.cs b/OliverBooth/Services/TemplateService.cs index 90bc47a..091da99 100644 --- a/OliverBooth/Services/TemplateService.cs +++ b/OliverBooth/Services/TemplateService.cs @@ -5,7 +5,7 @@ using OliverBooth.Common.Data.Web; using OliverBooth.Data.Web; using OliverBooth.Extensions.Markdig.Markdown.Template; using OliverBooth.Extensions.Markdig.Services; -using OliverBooth.Formatting; +using OliverBooth.Extensions.SmartFormat; using OliverBooth.Markdown.Template; using SmartFormat; using SmartFormat.Extensions; From 2ec2c0befcd4c059b3394ea4fe706cb372021031 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sun, 5 May 2024 02:27:21 +0100 Subject: [PATCH 04/12] chore: move Markdig reference to Markdig extension project --- OliverBooth.Common/OliverBooth.Common.csproj | 1 - .../OliverBooth.Extensions.Markdig.csproj | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/OliverBooth.Common/OliverBooth.Common.csproj b/OliverBooth.Common/OliverBooth.Common.csproj index f127848..5f634e5 100644 --- a/OliverBooth.Common/OliverBooth.Common.csproj +++ b/OliverBooth.Common/OliverBooth.Common.csproj @@ -9,7 +9,6 @@ - diff --git a/OliverBooth.Extensions.Markdig/OliverBooth.Extensions.Markdig.csproj b/OliverBooth.Extensions.Markdig/OliverBooth.Extensions.Markdig.csproj index 9a76817..5f35f98 100644 --- a/OliverBooth.Extensions.Markdig/OliverBooth.Extensions.Markdig.csproj +++ b/OliverBooth.Extensions.Markdig/OliverBooth.Extensions.Markdig.csproj @@ -6,6 +6,10 @@ enable + + + + From 435cae95db4639cdfedcd6a6a01afd5b6e3d6a49 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sun, 5 May 2024 13:48:01 +0100 Subject: [PATCH 05/12] refactor: move Mastodon status to partial --- OliverBooth/Pages/Blog/Index.cshtml | 39 +---------------- .../Shared/Partials/_MastodonStatus.cshtml | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+), 38 deletions(-) create mode 100644 OliverBooth/Pages/Shared/Partials/_MastodonStatus.cshtml diff --git a/OliverBooth/Pages/Blog/Index.cshtml b/OliverBooth/Pages/Blog/Index.cshtml index 23024e7..248464f 100644 --- a/OliverBooth/Pages/Blog/Index.cshtml +++ b/OliverBooth/Pages/Blog/Index.cshtml @@ -1,49 +1,12 @@ @page -@using Humanizer -@using OliverBooth.Common.Data.Mastodon @using OliverBooth.Common.Services @model Index -@inject IMastodonService MastodonService @{ ViewData["Title"] = "Blog"; - IMastodonStatus latestStatus = MastodonService.GetLatestStatus(); } -
-
- @Html.Raw(latestStatus.Content) - @foreach (MediaAttachment attachment in latestStatus.MediaAttachments) - { - switch (attachment.Type) - { - case AttachmentType.Audio: -

- -

- break; - - case AttachmentType.Video: -

- -

- break; - - case AttachmentType.Image: - case AttachmentType.GifV: -

- -

- break; - } - } -
- -
+@await Html.PartialAsync("Partials/_MastodonStatus")
@await Html.PartialAsync("_LoadingSpinner") diff --git a/OliverBooth/Pages/Shared/Partials/_MastodonStatus.cshtml b/OliverBooth/Pages/Shared/Partials/_MastodonStatus.cshtml new file mode 100644 index 0000000..0a309ad --- /dev/null +++ b/OliverBooth/Pages/Shared/Partials/_MastodonStatus.cshtml @@ -0,0 +1,42 @@ +@using Humanizer +@using OliverBooth.Common.Data.Mastodon +@using OliverBooth.Common.Services +@inject IMastodonService MastodonService +@{ + IMastodonStatus latestStatus = MastodonService.GetLatestStatus(); +} + +
+
+ @Html.Raw(latestStatus.Content) + @foreach (MediaAttachment attachment in latestStatus.MediaAttachments) + { + switch (attachment.Type) + { + case AttachmentType.Audio: +

+ +

+ break; + + case AttachmentType.Video: +

+ +

+ break; + + case AttachmentType.Image: + case AttachmentType.GifV: +

+ +

+ break; + } + } +
+ +
From 435a69b27a8cfbc573c49abfb3bda16ff8ae0f98 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Sun, 5 May 2024 18:13:06 +0100 Subject: [PATCH 06/12] perf: add pagination to blog post list removes the need for API controller accessed via JS --- .../Services/IBlogPostService.cs | 13 +- .../Controllers/Blog/BlogApiController.cs | 102 --------- OliverBooth/OliverBooth.csproj | 8 +- OliverBooth/Pages/Blog/Index.cshtml | 44 ++-- OliverBooth/Pages/Blog/Index.cshtml.cs | 2 +- OliverBooth/Pages/Blog/List.cshtml | 23 ++ OliverBooth/Pages/Blog/List.cshtml.cs | 32 +++ .../Pages/Shared/Partials/PageTabsUtility.cs | 205 ++++++++++++++++++ .../Pages/Shared/Partials/_BlogCard.cshtml | 56 +++++ .../Pages/Shared/Partials/_PageTabs.cshtml | 21 ++ OliverBooth/Program.cs | 1 - OliverBooth/Services/BlogPostService.cs | 13 +- src/scss/app.scss | 14 +- src/scss/blog.scss | 49 +++++ src/ts/API.ts | 44 ---- src/ts/Author.ts | 25 --- src/ts/BlogPost.ts | 99 --------- src/ts/BlogUrl.ts | 32 --- src/ts/UI.ts | 49 ----- src/ts/app.ts | 65 ------ 20 files changed, 431 insertions(+), 466 deletions(-) delete mode 100644 OliverBooth/Controllers/Blog/BlogApiController.cs create mode 100644 OliverBooth/Pages/Blog/List.cshtml create mode 100644 OliverBooth/Pages/Blog/List.cshtml.cs create mode 100644 OliverBooth/Pages/Shared/Partials/PageTabsUtility.cs create mode 100644 OliverBooth/Pages/Shared/Partials/_BlogCard.cshtml create mode 100644 OliverBooth/Pages/Shared/Partials/_PageTabs.cshtml create mode 100644 src/scss/blog.scss delete mode 100644 src/ts/API.ts delete mode 100644 src/ts/Author.ts delete mode 100644 src/ts/BlogPost.ts delete mode 100644 src/ts/BlogUrl.ts diff --git a/OliverBooth.Common/Services/IBlogPostService.cs b/OliverBooth.Common/Services/IBlogPostService.cs index 29b13e3..5517539 100644 --- a/OliverBooth.Common/Services/IBlogPostService.cs +++ b/OliverBooth.Common/Services/IBlogPostService.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using OliverBooth.Common.Data; using OliverBooth.Common.Data.Blog; namespace OliverBooth.Common.Services; @@ -22,8 +23,9 @@ public interface IBlogPostService /// /// Returns the total number of blog posts. /// + /// The post visibility filter. /// The total number of blog posts. - int GetBlogPostCount(); + int GetBlogPostCount(Visibility visibility = Visibility.None); /// /// Returns a collection of blog posts from the specified page, optionally limiting the number of posts @@ -62,6 +64,15 @@ public interface IBlogPostService /// The next blog post from the specified blog post. IBlogPost? GetNextPost(IBlogPost blogPost); + /// + /// Returns the number of pages needed to render all blog posts, using the specified as an + /// indicator of how many posts are allowed per page. + /// + /// The page size. Defaults to 10. + /// The post visibility filter. + /// The page count. + int GetPageCount(int pageSize = 10, Visibility visibility = Visibility.None); + /// /// Returns the previous blog post from the specified blog post. /// diff --git a/OliverBooth/Controllers/Blog/BlogApiController.cs b/OliverBooth/Controllers/Blog/BlogApiController.cs deleted file mode 100644 index 98674e3..0000000 --- a/OliverBooth/Controllers/Blog/BlogApiController.cs +++ /dev/null @@ -1,102 +0,0 @@ -using Humanizer; -using Microsoft.AspNetCore.Mvc; -using OliverBooth.Common.Data.Blog; -using OliverBooth.Common.Services; - -namespace OliverBooth.Controllers.Blog; - -/// -/// Represents a controller for the blog API. -/// -[ApiController] -[Route("api/blog")] -[Produces("application/json")] -public sealed class BlogApiController : ControllerBase -{ - private readonly IBlogPostService _blogPostService; - private readonly IBlogUserService _userService; - - /// - /// Initializes a new instance of the class. - /// - /// The . - /// The . - public BlogApiController(IBlogPostService blogPostService, IBlogUserService userService) - { - _blogPostService = blogPostService; - _userService = userService; - } - - [Route("count")] - public IActionResult Count() - { - return Ok(new { count = _blogPostService.GetBlogPostCount() }); - } - - [HttpGet("posts/{page:int?}")] - public IActionResult GetAllBlogPosts(int page = 0) - { - const int itemsPerPage = 10; - IReadOnlyList allPosts = _blogPostService.GetBlogPosts(page, itemsPerPage); - return Ok(allPosts.Select(post => CreatePostObject(post))); - } - - [HttpGet("posts/tagged/{tag}/{page:int?}")] - public IActionResult GetTaggedBlogPosts(string tag, int page = 0) - { - const int itemsPerPage = 10; - tag = tag.Replace('-', ' ').ToLowerInvariant(); - - IReadOnlyList allPosts = _blogPostService.GetBlogPosts(page, itemsPerPage); - allPosts = allPosts.Where(post => post.Tags.Contains(tag)).ToList(); - return Ok(allPosts.Select(post => CreatePostObject(post))); - } - - [HttpGet("author/{id:guid}")] - public IActionResult GetAuthor(Guid id) - { - if (!_userService.TryGetUser(id, out IUser? author)) return NotFound(); - - return Ok(new - { - id = author.Id, - name = author.DisplayName, - avatarUrl = author.AvatarUrl, - }); - } - - [HttpGet("post/{id:guid?}")] - public IActionResult GetPost(Guid id) - { - if (!_blogPostService.TryGetPost(id, out IBlogPost? post)) return NotFound(); - return Ok(CreatePostObject(post, true)); - } - - private object CreatePostObject(IBlogPost post, bool includeContent = false) - { - return new - { - id = post.Id, - commentsEnabled = post.EnableComments, - identifier = post.GetDisqusIdentifier(), - author = post.Author.Id, - title = post.Title, - published = post.Published.ToUnixTimeSeconds(), - updated = post.Updated?.ToUnixTimeSeconds(), - formattedPublishDate = post.Published.ToString("dddd, d MMMM yyyy HH:mm"), - formattedUpdateDate = post.Updated?.ToString("dddd, d MMMM yyyy HH:mm"), - humanizedTimestamp = post.Updated?.Humanize() ?? post.Published.Humanize(), - excerpt = _blogPostService.RenderExcerpt(post, out bool trimmed), - content = includeContent ? _blogPostService.RenderPost(post) : null, - trimmed, - tags = post.Tags.Select(t => t.Replace(' ', '-')), - url = new - { - year = post.Published.ToString("yyyy"), - month = post.Published.ToString("MM"), - day = post.Published.ToString("dd"), - slug = post.Slug - } - }; - } -} diff --git a/OliverBooth/OliverBooth.csproj b/OliverBooth/OliverBooth.csproj index b00cc91..521bc81 100644 --- a/OliverBooth/OliverBooth.csproj +++ b/OliverBooth/OliverBooth.csproj @@ -49,7 +49,13 @@ - + + + + + + _PageTabs.cshtml + diff --git a/OliverBooth/Pages/Blog/Index.cshtml b/OliverBooth/Pages/Blog/Index.cshtml index 248464f..7b8220f 100644 --- a/OliverBooth/Pages/Blog/Index.cshtml +++ b/OliverBooth/Pages/Blog/Index.cshtml @@ -1,6 +1,9 @@ @page +@using OliverBooth.Common.Data +@using OliverBooth.Common.Data.Blog @using OliverBooth.Common.Services @model Index +@inject IBlogPostService BlogPostService @{ ViewData["Title"] = "Blog"; @@ -9,36 +12,15 @@ @await Html.PartialAsync("Partials/_MastodonStatus")
- @await Html.PartialAsync("_LoadingSpinner") + @foreach (IBlogPost post in BlogPostService.GetBlogPosts(0)) + { + @await Html.PartialAsync("Partials/_BlogCard", post) + }
- \ No newline at end of file +@await Html.PartialAsync("Partials/_PageTabs", new ViewDataDictionary(ViewData) +{ + ["UrlRoot"] = "/blog", + ["Page"] = 0, + ["PageCount"] = BlogPostService.GetPageCount(visibility: Visibility.Published) +}) \ No newline at end of file diff --git a/OliverBooth/Pages/Blog/Index.cshtml.cs b/OliverBooth/Pages/Blog/Index.cshtml.cs index 5fa0b34..378ceb2 100644 --- a/OliverBooth/Pages/Blog/Index.cshtml.cs +++ b/OliverBooth/Pages/Blog/Index.cshtml.cs @@ -36,7 +36,7 @@ public class Index : PageModel return _blogPostService.TryGetPost(wpPostId, out IBlogPost? post) ? RedirectToPost(post) : NotFound(); } - private IActionResult RedirectToPost(IBlogPost post) + private RedirectResult RedirectToPost(IBlogPost post) { var route = new { diff --git a/OliverBooth/Pages/Blog/List.cshtml b/OliverBooth/Pages/Blog/List.cshtml new file mode 100644 index 0000000..61fb3d8 --- /dev/null +++ b/OliverBooth/Pages/Blog/List.cshtml @@ -0,0 +1,23 @@ +@page "/blog/page/{pageNumber:int}" +@model List +@using OliverBooth.Common.Data +@using OliverBooth.Common.Data.Blog +@using OliverBooth.Common.Services + +@inject IBlogPostService BlogPostService + +@await Html.PartialAsync("Partials/_MastodonStatus") + +
+ @foreach (IBlogPost post in BlogPostService.GetBlogPosts(Model.PageNumber)) + { + @await Html.PartialAsync("Partials/_BlogCard", post) + } +
+ +@await Html.PartialAsync("Partials/_PageTabs", new ViewDataDictionary(ViewData) +{ + ["UrlRoot"] = "/blog", + ["Page"] = Model.PageNumber, + ["PageCount"] = BlogPostService.GetPageCount(visibility: Visibility.Published) +}) \ No newline at end of file diff --git a/OliverBooth/Pages/Blog/List.cshtml.cs b/OliverBooth/Pages/Blog/List.cshtml.cs new file mode 100644 index 0000000..f0aa6dd --- /dev/null +++ b/OliverBooth/Pages/Blog/List.cshtml.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace OliverBooth.Pages.Blog; + +/// +/// Represents a class which defines the model for the /blog/page/# route. +/// +public class List : PageModel +{ + /// + /// Gets the requested page number. + /// + /// The requested page number. + public int PageNumber { get; private set; } + + /// + /// Handles the incoming GET request to the page. + /// + /// The requested page number, starting from 1. + /// + public IActionResult OnGet([FromRoute(Name = "pageNumber")] int page = 1) + { + if (page < 2) + { + return RedirectToPage("Index"); + } + + PageNumber = page; + return Page(); + } +} diff --git a/OliverBooth/Pages/Shared/Partials/PageTabsUtility.cs b/OliverBooth/Pages/Shared/Partials/PageTabsUtility.cs new file mode 100644 index 0000000..833cd77 --- /dev/null +++ b/OliverBooth/Pages/Shared/Partials/PageTabsUtility.cs @@ -0,0 +1,205 @@ +using Cysharp.Text; +using HtmlAgilityPack; + +namespace OliverBooth.Pages.Shared.Partials; + +/// +/// Provides methods for displaying pagination tabs. +/// +public class PageTabsUtility +{ + private string _urlRoot = string.Empty; + + /// + /// Gets or sets the current page number. + /// + /// The current page number. + public int CurrentPage { get; set; } = 1; + + /// + /// Gets or sets the page count. + /// + /// The page count. + public int PageCount { get; set; } = 1; + + /// + /// Gets or sets the URL root. + /// + /// The URL root. + public string UrlRoot + { + get => _urlRoot; + set => _urlRoot = string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim(); + } + + /// + /// Shows the bound chevrons for the specified bounds type. + /// + /// The bounds type to display. + /// An HTML string containing the elements representing the bound chevrons. + public string ShowBounds(BoundsType bounds) + { + return bounds switch + { + BoundsType.Lower => ShowLowerBound(), + BoundsType.Upper => ShowUpperBound(PageCount), + _ => string.Empty + }; + } + + /// + /// Shows the specified page tab. + /// + /// The tab to display. + /// An HTML string containing the element for the specified page tab. + public string ShowTab(int tab) + { + var document = new HtmlDocument(); + HtmlNode listItem = document.CreateElement("li"); + HtmlNode pageLink; + listItem.AddClass("page-item"); + + switch (tab) + { + case 0: + // show ... to indicate truncation + pageLink = document.CreateElement("span"); + pageLink.InnerHtml = "..."; + break; + + case var _ when CurrentPage == tab: + listItem.AddClass("active"); + listItem.SetAttributeValue("aria-current", "page"); + + pageLink = document.CreateElement("span"); + pageLink.InnerHtml = tab.ToString(); + break; + + default: + pageLink = document.CreateElement("a"); + pageLink.SetAttributeValue("href", GetLinkForPage(tab)); + pageLink.InnerHtml = tab.ToString(); + break; + } + + pageLink.AddClass("page-link"); + listItem.AppendChild(pageLink); + + document.DocumentNode.AppendChild(listItem); + return document.DocumentNode.InnerHtml; + } + + /// + /// Shows the paginated tab window. + /// + /// An HTML string representing the page tabs. + public string ShowTabWindow() + { + using Utf16ValueStringBuilder builder = ZString.CreateStringBuilder(); + + int windowLowerBound = Math.Max(CurrentPage - 2, 1); + int windowUpperBound = Math.Min(CurrentPage + 2, PageCount); + + if (windowLowerBound > 2) + { + // show lower truncation ... + builder.AppendLine(ShowTab(0)); + } + + for (int pageIndex = windowLowerBound; pageIndex <= windowUpperBound; pageIndex++) + { + if (pageIndex == 1 || pageIndex == PageCount) + { + // don't show bounds, these are explicitly written + continue; + } + + builder.AppendLine(ShowTab(pageIndex)); + } + + if (windowUpperBound < PageCount - 1) + { + // show upper truncation ... + builder.AppendLine(ShowTab(0)); + } + + return builder.ToString(); + } + + private string GetLinkForPage(int page) + { + // page 1 doesn't use /page/n route + return page == 1 ? _urlRoot : $"{_urlRoot}/page/{page}"; + } + + private string ShowLowerBound() + { + if (CurrentPage <= 1) + { + return string.Empty; + } + + var document = new HtmlDocument(); + HtmlNode listItem = document.CreateElement("li"); + listItem.AddClass("page-item"); + + HtmlNode pageLink = document.CreateElement("a"); + listItem.AppendChild(pageLink); + pageLink.AddClass("page-link"); + pageLink.SetAttributeValue("href", UrlRoot); + pageLink.InnerHtml = "≪"; + + document.DocumentNode.AppendChild(listItem); + + listItem = document.CreateElement("li"); + listItem.AddClass("page-item"); + + pageLink = document.CreateElement("a"); + listItem.AppendChild(pageLink); + pageLink.AddClass("page-link"); + pageLink.InnerHtml = "<"; + pageLink.SetAttributeValue("href", GetLinkForPage(CurrentPage - 1)); + + document.DocumentNode.AppendChild(listItem); + + return document.DocumentNode.InnerHtml; + } + + private string ShowUpperBound(int pageCount) + { + if (CurrentPage >= pageCount) + { + return string.Empty; + } + + var document = new HtmlDocument(); + + HtmlNode pageLink = document.CreateElement("a"); + pageLink.AddClass("page-link"); + pageLink.SetAttributeValue("href", GetLinkForPage(CurrentPage + 1)); + pageLink.InnerHtml = ">"; + + HtmlNode listItem = document.CreateElement("li"); + listItem.AddClass("page-item"); + listItem.AppendChild(pageLink); + document.DocumentNode.AppendChild(listItem); + + pageLink = document.CreateElement("a"); + pageLink.AddClass("page-link"); + pageLink.SetAttributeValue("href", GetLinkForPage(pageCount)); + pageLink.InnerHtml = "≫"; + + listItem = document.CreateElement("li"); + listItem.AddClass("page-item"); + listItem.AppendChild(pageLink); + document.DocumentNode.AppendChild(listItem); + + return document.DocumentNode.InnerHtml; + } + + public enum BoundsType + { + Lower, + Upper + } +} diff --git a/OliverBooth/Pages/Shared/Partials/_BlogCard.cshtml b/OliverBooth/Pages/Shared/Partials/_BlogCard.cshtml new file mode 100644 index 0000000..29bd396 --- /dev/null +++ b/OliverBooth/Pages/Shared/Partials/_BlogCard.cshtml @@ -0,0 +1,56 @@ +@using Humanizer +@using OliverBooth.Common.Data.Blog +@using OliverBooth.Common.Services +@model IBlogPost +@inject IBlogPostService BlogPostService + +@{ + IBlogAuthor author = Model.Author; + DateTimeOffset published = Model.Published; + DateTimeOffset? updated = Model.Updated; + DateTimeOffset time = updated ?? published; + string verb = updated is null ? "Published" : "Updated"; +} + +
+

+ + @Model.Title + +

+ +

+ @author.DisplayName + @author.DisplayName + • + @verb @time.Humanize() +

+ +
+ @Html.Raw(BlogPostService.RenderExcerpt(Model, out bool trimmed)) +
+ + @if (trimmed || Model.Excerpt is not null) + { +

+ + Read more... + +

+ } + +
+ + @foreach (string tag in Model.Tags) + { + @tag + } +
\ No newline at end of file diff --git a/OliverBooth/Pages/Shared/Partials/_PageTabs.cshtml b/OliverBooth/Pages/Shared/Partials/_PageTabs.cshtml new file mode 100644 index 0000000..6293a4f --- /dev/null +++ b/OliverBooth/Pages/Shared/Partials/_PageTabs.cshtml @@ -0,0 +1,21 @@ +@{ + var urlRoot = ViewData["UrlRoot"]?.ToString() ?? string.Empty; + var page = (int)(ViewData["Page"] ?? 1); + var pageCount = (int)(ViewData["PageCount"] ?? 1); + + var utility = new PageTabsUtility + { + CurrentPage = page, + PageCount = pageCount, + UrlRoot = urlRoot + }; +} + \ No newline at end of file diff --git a/OliverBooth/Program.cs b/OliverBooth/Program.cs index 84203ce..0da5115 100644 --- a/OliverBooth/Program.cs +++ b/OliverBooth/Program.cs @@ -89,7 +89,6 @@ if (!app.Environment.IsDevelopment()) } app.UseHttpsRedirection(); -app.UseStatusCodePagesWithRedirects("/error/{0}"); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); diff --git a/OliverBooth/Services/BlogPostService.cs b/OliverBooth/Services/BlogPostService.cs index 2190f09..e844024 100644 --- a/OliverBooth/Services/BlogPostService.cs +++ b/OliverBooth/Services/BlogPostService.cs @@ -36,10 +36,12 @@ internal sealed class BlogPostService : IBlogPostService } /// - public int GetBlogPostCount() + public int GetBlogPostCount(Visibility visibility = Visibility.None) { using BlogContext context = _dbContextFactory.CreateDbContext(); - return context.BlogPosts.Count(); + return visibility == Visibility.None + ? context.BlogPosts.Count() + : context.BlogPosts.Count(p => p.Visibility == visibility); } /// @@ -100,6 +102,13 @@ internal sealed class BlogPostService : IBlogPostService .FirstOrDefault(post => post.Published > blogPost.Published); } + /// + public int GetPageCount(int pageSize = 10, Visibility visibility = Visibility.None) + { + float postCount = GetBlogPostCount(visibility); + return (int)MathF.Ceiling(postCount / pageSize); + } + /// public IBlogPost? GetPreviousPost(IBlogPost blogPost) { diff --git a/src/scss/app.scss b/src/scss/app.scss index df43c57..e01d695 100644 --- a/src/scss/app.scss +++ b/src/scss/app.scss @@ -1,4 +1,5 @@ @import "markdown"; +@import "blog"; html, body { background: #121212; @@ -229,19 +230,6 @@ article { } } -.blog-card { - transition: all 0.2s ease-in-out; - - &:hover { - transform: scale(1.05); - } - - article { - background: none; - padding: 0; - } -} - code:not([class*="language-"]) { background: #1e1e1e !important; color: #dcdcda !important; diff --git a/src/scss/blog.scss b/src/scss/blog.scss new file mode 100644 index 0000000..0b76ce6 --- /dev/null +++ b/src/scss/blog.scss @@ -0,0 +1,49 @@ +$blog-card-bg: #333333; +$blog-card-gutter: 20px; +$border-radius: 3px; + +div.blog-card { + background: $blog-card-bg; + margin-bottom: $blog-card-gutter; + padding: $blog-card-gutter; + border-radius: $border-radius; + + :last-child { + margin-bottom: 0; + } + + article { + padding: 0; + margin: 0; + } +} + + +ul.pagination { + border: none; + + li { + a, span { + border-radius: $border-radius !important; + border: none; + + &:link, &:visited, &:active { + color: #007ec6; + } + } + + &.active a, &.active span { + background: #007ec6; + } + + &:not(.active) a, &:not(.active) span { + background: none; + } + + &:hover { + a { + color: #ffffff !important; + } + } + } +} \ No newline at end of file diff --git a/src/ts/API.ts b/src/ts/API.ts deleted file mode 100644 index a1e070d..0000000 --- a/src/ts/API.ts +++ /dev/null @@ -1,44 +0,0 @@ -import BlogPost from "./BlogPost"; -import Author from "./Author"; - -class API { - private static readonly BASE_URL: string = "/api"; - private static readonly BLOG_URL: string = "/blog"; - - static async getBlogPostCount(): Promise { - const response = await API.getResponse(`count`); - return response.count; - } - - static async getBlogPost(id: string): Promise { - const response = await API.getResponse(`post/${id}`); - return new BlogPost(response); - } - - static async getBlogPosts(page: number): Promise { - const response = await API.getResponse(`posts/${page}`); - return response.map(obj => new BlogPost(obj)); - } - - static async getBlogPostsByTag(tag: string, page: number): Promise { - const response = await API.getResponse(`posts/tagged/${tag}/${page}`); - return response.map(obj => new BlogPost(obj)); - } - - static async getAuthor(id: string): Promise { - const response = await API.getResponse(`author/${id}`); - return new Author(response); - } - - private static async getResponse(url: string): Promise { - const response = await fetch(`${API.BASE_URL + API.BLOG_URL}/${url}`); - if (response.status !== 200) { - throw new Error("Invalid response from server"); - } - - const text = await response.text(); - return JSON.parse(text); - } -} - -export default API; \ No newline at end of file diff --git a/src/ts/Author.ts b/src/ts/Author.ts deleted file mode 100644 index 8faf1a5..0000000 --- a/src/ts/Author.ts +++ /dev/null @@ -1,25 +0,0 @@ -class Author { - private readonly _id: string; - private readonly _name: string; - private readonly _avatarUrl: string; - - constructor(json: any) { - this._id = json.id; - this._name = json.name; - this._avatarUrl = json.avatarUrl; - } - - get id(): string { - return this._id; - } - - get name(): string { - return this._name; - } - - get avatarUrl(): string { - return this._avatarUrl; - } -} - -export default Author; \ No newline at end of file diff --git a/src/ts/BlogPost.ts b/src/ts/BlogPost.ts deleted file mode 100644 index 1186964..0000000 --- a/src/ts/BlogPost.ts +++ /dev/null @@ -1,99 +0,0 @@ -import BlogUrl from "./BlogUrl"; - -class BlogPost { - private readonly _id: string; - private readonly _commentsEnabled: boolean; - private readonly _title: string; - private readonly _excerpt: string; - private readonly _content: string; - private readonly _authorId: string; - private readonly _published: Date; - private readonly _updated?: Date; - private readonly _url: BlogUrl; - private readonly _trimmed: boolean; - private readonly _identifier: string; - private readonly _humanizedTimestamp: string; - private readonly _formattedPublishDate: string; - private readonly _formattedUpdateDate: string; - private readonly _tags: string[]; - - constructor(json: any) { - this._id = json.id; - this._commentsEnabled = json.commentsEnabled; - this._title = json.title; - this._excerpt = json.excerpt; - this._content = json.content; - this._authorId = json.author; - this._published = new Date(json.published * 1000); - this._updated = (json.updated && new Date(json.updated * 1000)) || null; - this._url = new BlogUrl(json.url); - this._trimmed = json.trimmed; - this._identifier = json.identifier; - this._humanizedTimestamp = json.humanizedTimestamp; - this._formattedPublishDate = json.formattedPublishDate; - this._formattedUpdateDate = json.formattedUpdateDate; - this._tags = json.tags; - } - - get id(): string { - return this._id; - } - - get commentsEnabled(): boolean { - return this._commentsEnabled; - } - - get title(): string { - return this._title; - } - - get excerpt(): string { - return this._excerpt; - } - - get content(): string { - return this._content; - } - - get authorId(): string { - return this._authorId; - } - - get published(): Date { - return this._published; - } - - get updated(): Date { - return this._updated; - } - - get url(): BlogUrl { - return this._url; - } - - get tags(): string[] { - return this._tags; - } - - get trimmed(): boolean { - return this._trimmed; - } - - get identifier(): string { - return this._identifier; - } - - get humanizedTimestamp(): string { - return this._humanizedTimestamp; - } - - get formattedPublishDate(): string { - return this._formattedPublishDate; - } - - get formattedUpdateDate(): string { - return this._formattedUpdateDate; - } -} - -export default BlogPost; \ No newline at end of file diff --git a/src/ts/BlogUrl.ts b/src/ts/BlogUrl.ts deleted file mode 100644 index 351ad3c..0000000 --- a/src/ts/BlogUrl.ts +++ /dev/null @@ -1,32 +0,0 @@ -class BlogUrl { - private readonly _year: string; - private readonly _month: string; - private readonly _day: string; - private readonly _slug: string; - - constructor(json: any) { - this._year = json.year; - this._month = json.month; - this._day = json.day; - this._slug = json.slug; - } - - - get year(): string { - return this._year; - } - - get month(): string { - return this._month; - } - - get day(): string { - return this._day; - } - - get slug(): string { - return this._slug; - } -} - -export default BlogUrl; \ No newline at end of file diff --git a/src/ts/UI.ts b/src/ts/UI.ts index 3c9d9ed..554eb07 100644 --- a/src/ts/UI.ts +++ b/src/ts/UI.ts @@ -1,5 +1,3 @@ -import BlogPost from "./BlogPost"; -import Author from "./Author"; import TimeUtility from "./TimeUtility"; declare const bootstrap: any; @@ -7,18 +5,6 @@ declare const katex: any; declare const Prism: any; class UI { - public static get blogPost(): HTMLDivElement { - return document.querySelector("article[data-blog-post='true']"); - } - - public static get blogPostContainer(): HTMLDivElement { - return document.querySelector("#all-blog-posts"); - } - - public static get blogPostTemplate(): HTMLDivElement { - return document.querySelector("#blog-post-template"); - } - /** * Creates a