Compare commits
No commits in common. "96e63a30888ebbdf69ae4dfe5a54c5c26cba5d8d" and "b119861eeebbab7628792f41b6a2f3bb94f385f7" have entirely different histories.
96e63a3088
...
b119861eee
@ -16,9 +16,6 @@ internal sealed class BlogPost : IBlogPost
|
||||
/// <inheritdoc />
|
||||
public bool EnableComments { get; internal set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? Excerpt { get; internal set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; private set; } = Guid.NewGuid();
|
||||
|
||||
|
@ -20,7 +20,6 @@ internal sealed class BlogPostConfiguration : IEntityTypeConfiguration<BlogPost>
|
||||
builder.Property(e => e.Updated).IsRequired(false);
|
||||
builder.Property(e => e.Title).HasMaxLength(255).IsRequired();
|
||||
builder.Property(e => e.Body).IsRequired();
|
||||
builder.Property(e => e.Excerpt).HasMaxLength(512).IsRequired(false);
|
||||
builder.Property(e => e.IsRedirect).IsRequired();
|
||||
builder.Property(e => e.RedirectUrl).HasConversion<UriToStringConverter>().HasMaxLength(255).IsRequired(false);
|
||||
builder.Property(e => e.EnableComments).IsRequired();
|
||||
|
@ -25,12 +25,6 @@ public interface IBlogPost
|
||||
/// </value>
|
||||
bool EnableComments { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the excerpt of this post, if it has one.
|
||||
/// </summary>
|
||||
/// <value>The excerpt, or <see langword="null" /> if this post has no excerpt.</value>
|
||||
string? Excerpt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ID of the post.
|
||||
/// </summary>
|
||||
|
@ -16,7 +16,6 @@ internal sealed class TutorialArticleConfiguration : IEntityTypeConfiguration<Tu
|
||||
|
||||
builder.Property(e => e.Id).IsRequired();
|
||||
builder.Property(e => e.Folder).IsRequired();
|
||||
builder.Property(e => e.Excerpt).HasMaxLength(512).IsRequired(false);
|
||||
builder.Property(e => e.Published).IsRequired();
|
||||
builder.Property(e => e.Updated);
|
||||
builder.Property(e => e.Slug).IsRequired();
|
||||
@ -25,6 +24,5 @@ internal sealed class TutorialArticleConfiguration : IEntityTypeConfiguration<Tu
|
||||
builder.Property(e => e.NextPart);
|
||||
builder.Property(e => e.PreviousPart);
|
||||
builder.Property(e => e.Visibility).HasConversion<EnumToStringConverter<Visibility>>();
|
||||
builder.Property(e => e.EnableComments).IsRequired();
|
||||
}
|
||||
}
|
||||
|
@ -11,20 +11,6 @@ public interface ITutorialArticle
|
||||
/// <value>The body.</value>
|
||||
string Body { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether comments are enabled for the article.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <see langword="true" /> if comments are enabled for the article; otherwise, <see langword="false" />.
|
||||
/// </value>
|
||||
bool EnableComments { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the excerpt of this article, if it has one.
|
||||
/// </summary>
|
||||
/// <value>The excerpt, or <see langword="null" /> if this article has no excerpt.</value>
|
||||
string? Excerpt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ID of the folder this article is contained within.
|
||||
/// </summary>
|
||||
|
@ -8,12 +8,6 @@ internal sealed class TutorialArticle : IEquatable<TutorialArticle>, ITutorialAr
|
||||
/// <inheritdoc />
|
||||
public string Body { get; private set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool EnableComments { get; internal set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? Excerpt { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Folder { get; private set; }
|
||||
|
||||
@ -21,7 +15,7 @@ internal sealed class TutorialArticle : IEquatable<TutorialArticle>, ITutorialAr
|
||||
public int Id { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int? NextPart { get; private set; }
|
||||
public int? NextPart { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Uri? PreviewImageUrl { get; private set; }
|
||||
|
@ -1,153 +0,0 @@
|
||||
using System.Web;
|
||||
using Cysharp.Text;
|
||||
using OliverBooth.Data.Blog;
|
||||
using OliverBooth.Data.Web;
|
||||
using OliverBooth.Services;
|
||||
|
||||
namespace OliverBooth.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides helper methods for generating HTML tags
|
||||
/// </summary>
|
||||
public static class HtmlUtility
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates <c><meta></c> embed tags by pulling data from the specified blog post.
|
||||
/// </summary>
|
||||
/// <param name="post">The blog post whose metadata should be retrieved.</param>
|
||||
/// <param name="blogPostService">The <see cref="IBlogPostService" /> injected by the page.</param>
|
||||
/// <returns>A string containing a collection of <c><meta></c> embed tags.</returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// <para><paramref name="post" /> is <see langword="null" />.</para>
|
||||
/// -or-
|
||||
/// <para><paramref name="blogPostService" /> is <see langword="null" />.</para>
|
||||
/// </exception>
|
||||
public static string CreateMetaTagsFromPost(IBlogPost post, IBlogPostService blogPostService)
|
||||
{
|
||||
if (post is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(post));
|
||||
}
|
||||
|
||||
if (blogPostService is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(blogPostService));
|
||||
}
|
||||
|
||||
|
||||
string excerpt = blogPostService.RenderExcerpt(post, out _);
|
||||
var tags = new Dictionary<string, string>
|
||||
{
|
||||
["title"] = post.Title,
|
||||
["description"] = excerpt,
|
||||
["author"] = post.Author.DisplayName
|
||||
};
|
||||
return CreateMetaTags(tags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates <c><meta></c> embed tags by pulling data from the specified article.
|
||||
/// </summary>
|
||||
/// <param name="article">The article whose metadata should be retrieved.</param>
|
||||
/// <param name="tutorialService">The <see cref="ITutorialService" /> injected by the page.</param>
|
||||
/// <returns>A string containing a collection of <c><meta></c> embed tags.</returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// <para><paramref name="article" /> is <see langword="null" />.</para>
|
||||
/// -or-
|
||||
/// <para><paramref name="tutorialService" /> is <see langword="null" />.</para>
|
||||
/// </exception>
|
||||
public static string CreateMetaTagsFromTutorialArticle(ITutorialArticle article, ITutorialService tutorialService)
|
||||
{
|
||||
if (article is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(article));
|
||||
}
|
||||
|
||||
if (tutorialService is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tutorialService));
|
||||
}
|
||||
|
||||
|
||||
string excerpt = tutorialService.RenderExcerpt(article, out _);
|
||||
var tags = new Dictionary<string, string>
|
||||
{
|
||||
["title"] = article.Title,
|
||||
["description"] = excerpt,
|
||||
["author"] = "Oliver Booth" // TODO add article author support?
|
||||
};
|
||||
return CreateMetaTags(tags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates <c><meta></c> embed tags by pulling data from the specified dictionary.
|
||||
/// </summary>
|
||||
/// <param name="tags">
|
||||
/// A dictionary containing the tag values. This dictionary should be in the form:
|
||||
///
|
||||
/// <list type="table">
|
||||
/// <listheader>
|
||||
/// <term>Key</term>
|
||||
/// <description>Description</description>
|
||||
/// </listheader>
|
||||
///
|
||||
/// <item>
|
||||
/// <term>description</term>
|
||||
/// <description>
|
||||
/// The value to apply to the <c>description</c>, <c>og:description</c>, and <c>twitter:description</c>, tags.
|
||||
/// </description>
|
||||
/// </item>
|
||||
///
|
||||
/// <item>
|
||||
/// <term>author</term>
|
||||
/// <description>The value to apply to the <c>og:site_name</c>, and <c>twitter:creator</c>, tags.</description>
|
||||
/// </item>
|
||||
///
|
||||
/// <item>
|
||||
/// <term>title</term>
|
||||
/// <description>
|
||||
/// The value to apply to the <c>title</c>, <c>og:title</c>, and <c>twitter:title</c>, tags.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
///
|
||||
/// Any other values contained with the dictionary are ignored.
|
||||
/// </param>
|
||||
/// <returns>A string containing a collection of <c><meta></c> embed tags.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="tags" /> is <see langword="null" />.</exception>
|
||||
public static string CreateMetaTags(IReadOnlyDictionary<string, string> tags)
|
||||
{
|
||||
if (tags is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tags));
|
||||
}
|
||||
|
||||
using Utf8ValueStringBuilder builder = ZString.CreateUtf8StringBuilder();
|
||||
builder.AppendLine("""<meta property="og:type" content="article">""");
|
||||
|
||||
if (tags.TryGetValue("description", out string? description))
|
||||
{
|
||||
description = HttpUtility.HtmlEncode(description);
|
||||
builder.AppendLine($"""<meta name="description" content="{description}">""");
|
||||
builder.AppendLine($"""<meta property="og:description" content="{description}">""");
|
||||
builder.AppendLine($"""<meta property="twitter:description" content="{description}">""");
|
||||
}
|
||||
|
||||
if (tags.TryGetValue("author", out string? author))
|
||||
{
|
||||
author = HttpUtility.HtmlEncode(author);
|
||||
builder.AppendLine($"""<meta property="og:site_name" content="{author}">""");
|
||||
builder.AppendLine($"""<meta property="twitter:creator" content="{author}">""");
|
||||
}
|
||||
|
||||
if (tags.TryGetValue("title", out string? title))
|
||||
{
|
||||
title = HttpUtility.HtmlEncode(title);
|
||||
builder.AppendLine($"""<meta name="title" content="{title}">""");
|
||||
builder.AppendLine($"""<meta property="og:title" content="{title}">""");
|
||||
builder.AppendLine($"""<meta property="twitter:title" content="{title}">""");
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
@ -132,25 +132,31 @@
|
||||
|
||||
@if (post.EnableComments)
|
||||
{
|
||||
<div class="giscus"></div>
|
||||
@section Scripts
|
||||
{
|
||||
<script src="https://giscus.app/client.js"
|
||||
data-repo="oliverbooth/oliverbooth.dev"
|
||||
data-repo-id="MDEwOlJlcG9zaXRvcnkyNDUxODEyNDI="
|
||||
data-category="Comments"
|
||||
data-category-id="DIC_kwDODp0rOs4Ce_Nj"
|
||||
data-mapping="pathname"
|
||||
data-strict="0"
|
||||
data-reactions-enabled="1"
|
||||
data-emit-metadata="0"
|
||||
data-input-position="bottom"
|
||||
data-theme="preferred_color_scheme"
|
||||
data-lang="en"
|
||||
crossorigin="anonymous"
|
||||
async>
|
||||
</script>
|
||||
}
|
||||
<div id="disqus_thread"></div>
|
||||
<script>
|
||||
var disqus_config = function () {
|
||||
this.page.url = "@post.GetDisqusUrl()";
|
||||
this.page.identifier = "@post.GetDisqusIdentifier()";
|
||||
this.page.title = "@post.Title";
|
||||
this.page.postId = "@post.GetDisqusPostId()";
|
||||
};
|
||||
|
||||
(function() {
|
||||
const d = document, s = d.createElement("script");
|
||||
s.async = true;
|
||||
s.type = "text/javascript";
|
||||
s.src = "https://oliverbooth-dev.disqus.com/embed.js";
|
||||
s.setAttribute("data-timestamp", (+ new Date()).toString());
|
||||
(d.head || d.body).appendChild(s);
|
||||
})();
|
||||
</script>
|
||||
<script id="dsq-count-scr" src="https://oliverbooth-dev.disqus.com/count.js" async></script>
|
||||
<noscript>
|
||||
Please enable JavaScript to view the
|
||||
<a href="https://disqus.com/?ref_noscript" rel="nofollow">
|
||||
comments powered by Disqus.
|
||||
</a>
|
||||
</noscript>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -8,9 +8,22 @@
|
||||
@{
|
||||
ViewData["Title"] = "Blog";
|
||||
MastodonStatus latestStatus = MastodonService.GetLatestStatus();
|
||||
bool doAprilFools = DateOnly.FromDateTime(DateTime.UtcNow) == new DateOnly(2024, 04, 01) || Environment.GetEnvironmentVariable("DO_AF") == "1";
|
||||
}
|
||||
|
||||
<div class="card text-center mastodon-update-card">
|
||||
@if (doAprilFools)
|
||||
{
|
||||
<h1>UNDER CONSTRUCTION</h1>
|
||||
<div style="text-align: center">
|
||||
<img src="~/img/construction_90x85.gif">
|
||||
<img src="~/img/underconstruction_323x118.gif">
|
||||
<img src="~/img/construction_90x85.gif">
|
||||
<p>Coming soon WATCH THIS SPACE</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card text-center mastodon-update-card">
|
||||
<div class="card-body">
|
||||
@Html.Raw(latestStatus.Content)
|
||||
@foreach (MediaAttachment attachment in latestStatus.MediaAttachments)
|
||||
@ -37,13 +50,13 @@
|
||||
•
|
||||
<a href="@latestStatus.OriginalUri" target="_blank">View on Mastodon</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="all-blog-posts">
|
||||
<div id="all-blog-posts">
|
||||
@await Html.PartialAsync("_LoadingSpinner")
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id="blog-post-template" type="text/x-handlebars-template">
|
||||
<script id="blog-post-template" type="text/x-handlebars-template">
|
||||
<div class="card-header">
|
||||
<span class="text-muted">
|
||||
<img class="blog-author-icon" src="{{author.avatar}}" alt="{{author.name}}">
|
||||
@ -78,4 +91,5 @@
|
||||
<a href="?tag={{urlEncode this}}" class="badge text-bg-dark">{{this}}</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
</script>
|
||||
</script>
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
|
||||
<main class="container">
|
||||
<h1 class="display-4">@ViewData["Title"]</h1>
|
||||
<p class="lead">Last Updated: 27 April 2024</p>
|
||||
<p class="lead">Last Updated: 26 May 2023</p>
|
||||
<div class="alert alert-primary">
|
||||
This Privacy Policy differs from the policy that applies to my applications published to Google Play. For my
|
||||
applications' privacy policy, please <a asp-page="/Privacy/GooglePlay">click here</a>.
|
||||
@ -32,13 +32,10 @@
|
||||
</p>
|
||||
<p>I do not use any cookies or similar tracking technologies that can identify individual users.</p>
|
||||
<p>
|
||||
Please note that my website includes a GitHub integration for commenting on blog posts. GitHub is a third-party
|
||||
Please note that my website includes a Disqus integration for commenting on blog posts. Disqus is a third-party
|
||||
service, and their use of cookies and collection of personal information are governed by their own privacy
|
||||
policies. I have no control over the information collected by GitHub, and I encourage you to review
|
||||
<a href="https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement">
|
||||
their privacy policy
|
||||
</a>
|
||||
to understand how your information may be used by them.
|
||||
policies. I have no control over the information collected by Disqus, and I encourage you to review their
|
||||
privacy policy to understand how your information may be used by them.
|
||||
</p>
|
||||
|
||||
<h2>Use of Information</h2>
|
||||
|
@ -1,12 +1,10 @@
|
||||
@using OliverBooth.Data.Blog
|
||||
@using OliverBooth.Data.Web
|
||||
@using OliverBooth.Extensions
|
||||
@using OliverBooth.Services
|
||||
@inject IBlogPostService BlogPostService
|
||||
@inject ITutorialService TutorialService
|
||||
@{
|
||||
HttpRequest request = Context.Request;
|
||||
var url = new Uri($"{request.Scheme}://{request.Host}{request.Path}{request.QueryString}");
|
||||
bool doAprilFools = DateOnly.FromDateTime(DateTime.UtcNow) == new DateOnly(2024, 04, 01) || Environment.GetEnvironmentVariable("DO_AF") == "1";
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
@ -30,11 +28,15 @@
|
||||
}
|
||||
@if (ViewData["Post"] is IBlogPost post)
|
||||
{
|
||||
@Html.Raw(HtmlUtility.CreateMetaTagsFromPost(post, BlogPostService))
|
||||
}
|
||||
else if (ViewData["Post"] is ITutorialArticle article)
|
||||
{
|
||||
@Html.Raw(HtmlUtility.CreateMetaTagsFromTutorialArticle(article, TutorialService))
|
||||
string excerpt = BlogPostService.RenderExcerpt(post, out bool trimmed);
|
||||
<meta name="title" content="@post.Title">
|
||||
<meta name="description" content="@excerpt">
|
||||
<meta property="og:title" content="@post.Title">
|
||||
<meta property="og:description" content="@excerpt">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="twitter:title" content="@post.Title">
|
||||
<meta property="twitter:creator" content="@post.Author.DisplayName">
|
||||
<meta property="twitter:description" content="@excerpt">
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -60,12 +62,28 @@
|
||||
<link rel="stylesheet" href="~/css/prism.vs.min.css" asp-append-version="true">
|
||||
<link rel="stylesheet" href="~/css/app.min.css" asp-append-version="true">
|
||||
<link rel="stylesheet" href="~/css/ribbon.min.css" asp-append-version="true">
|
||||
@if (doAprilFools)
|
||||
{
|
||||
<link rel="stylesheet" href="~/css/af-app.min.css" asp-append-version="true">
|
||||
}
|
||||
</head>
|
||||
<body>
|
||||
<header class="container" style="margin-top: 20px;">
|
||||
<div id="site-title" class="text-center">
|
||||
<h1>
|
||||
@if (doAprilFools)
|
||||
{
|
||||
<marquee>
|
||||
<a href="/">
|
||||
<img src="~/img/ob-af-256x256.png" alt="Oliver Booth" height="128">
|
||||
<img src="~/img/af-oliverbooth-1236x293.png" alt="Oliver Booth" height="128">
|
||||
</a>
|
||||
</marquee>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a href="/"><img src="~/img/ob-256x256.png" alt="Oliver Booth" height="128"> Oliver Booth</a>
|
||||
}
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
@ -95,6 +113,13 @@
|
||||
<div style="margin:50px 0;"></div>
|
||||
|
||||
<div class="container">
|
||||
@if (doAprilFools)
|
||||
{
|
||||
<h1 style="text-decoration: underline; color: #0f0 !important; margin: 20px 0;">
|
||||
<img src="~/img/af-homepage_500x383.jpg" alt="WELCOME TO MY HOMEPAGE!!!!111SHIFT+1">
|
||||
</h1>
|
||||
}
|
||||
|
||||
<main role="main" class="pb-3">
|
||||
@RenderBody()
|
||||
</main>
|
||||
@ -112,7 +137,7 @@
|
||||
</ul>
|
||||
|
||||
<ul class="footer-nav" style="margin-top: 20px;">
|
||||
<li>© @DateTime.UtcNow.Year</li>
|
||||
<li>@(doAprilFools ? "(C) 2003" : Html.Raw($"© {DateTime.UtcNow.Year}"))</li>
|
||||
<li><a asp-page="/privacy/index">Privacy</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -90,32 +90,3 @@
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
|
||||
@if (article.EnableComments)
|
||||
{
|
||||
<div class="giscus"></div>
|
||||
@section Scripts
|
||||
{
|
||||
<script src="https://giscus.app/client.js"
|
||||
data-repo="oliverbooth/oliverbooth.dev"
|
||||
data-repo-id="MDEwOlJlcG9zaXRvcnkyNDUxODEyNDI="
|
||||
data-category="Comments"
|
||||
data-category-id="DIC_kwDODp0rOs4Ce_Nj"
|
||||
data-mapping="pathname"
|
||||
data-strict="0"
|
||||
data-reactions-enabled="1"
|
||||
data-emit-metadata="0"
|
||||
data-input-position="bottom"
|
||||
data-theme="preferred_color_scheme"
|
||||
data-lang="en"
|
||||
crossorigin="anonymous"
|
||||
async>
|
||||
</script>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-center text-muted">Comments are not enabled for this post.</p>
|
||||
}
|
@ -90,12 +90,6 @@ internal sealed class BlogPostService : IBlogPostService
|
||||
/// <inheritdoc />
|
||||
public string RenderExcerpt(IBlogPost post, out bool wasTrimmed)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(post.Excerpt))
|
||||
{
|
||||
wasTrimmed = false;
|
||||
return Markdig.Markdown.ToHtml(post.Excerpt, _markdownPipeline);
|
||||
}
|
||||
|
||||
string body = post.Body;
|
||||
int moreIndex = body.IndexOf("<!--more-->", StringComparison.Ordinal);
|
||||
|
||||
|
@ -67,17 +67,6 @@ public interface ITutorialService
|
||||
/// <returns>The rendered HTML of the article.</returns>
|
||||
string RenderArticle(ITutorialArticle article);
|
||||
|
||||
/// <summary>
|
||||
/// Renders the excerpt of the specified article.
|
||||
/// </summary>
|
||||
/// <param name="article">The article whose excerpt to render.</param>
|
||||
/// <param name="wasTrimmed">
|
||||
/// When this method returns, contains <see langword="true" /> if the excerpt was trimmed; otherwise,
|
||||
/// <see langword="false" />.
|
||||
/// </param>
|
||||
/// <returns>The rendered HTML of the article's excerpt.</returns>
|
||||
string RenderExcerpt(ITutorialArticle article, out bool wasTrimmed);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to find an article by its ID.
|
||||
/// </summary>
|
||||
|
@ -1,6 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Cysharp.Text;
|
||||
using Humanizer;
|
||||
using Markdig;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OliverBooth.Data;
|
||||
@ -109,29 +108,6 @@ internal sealed class TutorialService : ITutorialService
|
||||
return Markdig.Markdown.ToHtml(article.Body, _markdownPipeline);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string RenderExcerpt(ITutorialArticle article, out bool wasTrimmed)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(article.Excerpt))
|
||||
{
|
||||
wasTrimmed = false;
|
||||
return Markdig.Markdown.ToHtml(article.Excerpt, _markdownPipeline);
|
||||
}
|
||||
|
||||
string body = article.Body;
|
||||
int moreIndex = body.IndexOf("<!--more-->", StringComparison.Ordinal);
|
||||
|
||||
if (moreIndex == -1)
|
||||
{
|
||||
string excerpt = body.Truncate(255, "...");
|
||||
wasTrimmed = body.Length > 255;
|
||||
return Markdig.Markdown.ToHtml(excerpt, _markdownPipeline);
|
||||
}
|
||||
|
||||
wasTrimmed = true;
|
||||
return Markdig.Markdown.ToHtml(body[..moreIndex], _markdownPipeline);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetArticle(int id, [NotNullWhen(true)] out ITutorialArticle? article)
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user