feat: add support for MediaWiki-style templates
This commit is contained in:
parent
da5fe30c7a
commit
6af41cba5a
82
OliverBooth/Data/Web/ArticleTemplate.cs
Normal file
82
OliverBooth/Data/Web/ArticleTemplate.cs
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
using SmartFormat;
|
||||||
|
using SmartFormat.Core.Extensions;
|
||||||
|
|
||||||
|
namespace OliverBooth.Data.Web;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a MediaWiki-style template.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ArticleTemplate : IEquatable<ArticleTemplate>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the format string.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The format string.</value>
|
||||||
|
public string FormatString { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the template.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; private set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a value indicating whether two instances of <see cref="ArticleTemplate" /> are equal.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="left">The first instance of <see cref="ArticleTemplate" /> to compare.</param>
|
||||||
|
/// <param name="right">The second instance of <see cref="ArticleTemplate" /> to compare.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// <see langword="true" /> if <paramref name="left" /> and <paramref name="right" /> are equal; otherwise,
|
||||||
|
/// <see langword="false" />.
|
||||||
|
/// </returns>
|
||||||
|
public static bool operator ==(ArticleTemplate? left, ArticleTemplate? right) => Equals(left, right);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a value indicating whether two instances of <see cref="ArticleTemplate" /> are not equal.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="left">The first instance of <see cref="ArticleTemplate" /> to compare.</param>
|
||||||
|
/// <param name="right">The second instance of <see cref="ArticleTemplate" /> to compare.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// <see langword="true" /> if <paramref name="left" /> and <paramref name="right" /> are not equal; otherwise,
|
||||||
|
/// <see langword="false" />.
|
||||||
|
/// </returns>
|
||||||
|
public static bool operator !=(ArticleTemplate? left, ArticleTemplate? right) => !(left == right);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a value indicating whether this instance of <see cref="ArticleTemplate" /> is equal to another
|
||||||
|
/// instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="other">An instance to compare with this instance.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// <see langword="true" /> if <paramref name="other" /> is equal to this instance; otherwise,
|
||||||
|
/// <see langword="false" />.
|
||||||
|
/// </returns>
|
||||||
|
public bool Equals(ArticleTemplate? other)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(null, other)) return false;
|
||||||
|
if (ReferenceEquals(this, other)) return true;
|
||||||
|
return Name == other.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a value indicating whether this instance is equal to a specified object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="obj">An object to compare with this instance.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// <see langword="true" /> if <paramref name="obj" /> is an instance of <see cref="ArticleTemplate" /> and
|
||||||
|
/// equals the value of this instance; otherwise, <see langword="false" />.
|
||||||
|
/// </returns>
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
{
|
||||||
|
return ReferenceEquals(this, obj) || obj is ArticleTemplate other && Equals(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the hash code for this instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The hash code.</returns>
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
// ReSharper disable once NonReadonlyMemberInGetHashCode
|
||||||
|
return Name.GetHashCode();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace OliverBooth.Data.Web.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the configuration for the <see cref="ArticleTemplate" /> entity.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class ArticleTemplateConfiguration : IEntityTypeConfiguration<ArticleTemplate>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<ArticleTemplate> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("ArticleTemplate");
|
||||||
|
builder.HasKey(e => e.Name);
|
||||||
|
|
||||||
|
builder.Property(e => e.Name).IsRequired();
|
||||||
|
builder.Property(e => e.FormatString).IsRequired();
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using OliverBooth.Data.Web;
|
||||||
|
using OliverBooth.Data.Web.Configuration;
|
||||||
|
|
||||||
namespace OliverBooth.Data;
|
namespace OliverBooth.Data;
|
||||||
|
|
||||||
@ -9,11 +11,21 @@ public sealed class WebContext : DbContext
|
|||||||
{
|
{
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="WebContext" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configuration">The configuration.</param>
|
||||||
public WebContext(IConfiguration configuration)
|
public WebContext(IConfiguration configuration)
|
||||||
{
|
{
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the set of article templates.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The set of article templates.</value>
|
||||||
|
public DbSet<ArticleTemplate> ArticleTemplates { get; private set; } = null!;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
@ -24,5 +36,6 @@ public sealed class WebContext : DbContext
|
|||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
|
modelBuilder.ApplyConfiguration(new ArticleTemplateConfiguration());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
33
OliverBooth/DateFormatter.cs
Normal file
33
OliverBooth/DateFormatter.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using SmartFormat.Core.Extensions;
|
||||||
|
|
||||||
|
namespace OliverBooth;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a SmartFormat formatter that formats a date.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class DateFormatter : IFormatter
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool CanAutoDetect { get; set; } = true;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Name { get; set; } = "date";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool TryEvaluateFormat(IFormattingInfo formattingInfo)
|
||||||
|
{
|
||||||
|
if (formattingInfo.CurrentValue is not string value)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!DateTime.TryParseExact(value, "yyyy-MM-dd",
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
DateTimeStyles.None,
|
||||||
|
out DateTime date))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
|
||||||
|
formattingInfo.Write(date.ToString(formattingInfo.Format?.ToString()));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
33
OliverBooth/Markdown/TemplateExtension.cs
Normal file
33
OliverBooth/Markdown/TemplateExtension.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using Markdig;
|
||||||
|
using Markdig.Renderers;
|
||||||
|
using OliverBooth.Services;
|
||||||
|
|
||||||
|
namespace OliverBooth.Markdown;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a Markdown extension that adds support for MediaWiki-style templates.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class TemplateExtension : IMarkdownExtension
|
||||||
|
{
|
||||||
|
private readonly TemplateService _templateService;
|
||||||
|
|
||||||
|
public TemplateExtension(TemplateService templateService)
|
||||||
|
{
|
||||||
|
_templateService = templateService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||||
|
{
|
||||||
|
pipeline.InlineParsers.AddIfNotAlready<TemplateInlineParser>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||||
|
{
|
||||||
|
if (renderer is HtmlRenderer htmlRenderer)
|
||||||
|
{
|
||||||
|
htmlRenderer.ObjectRenderers.Add(new TemplateRenderer(_templateService));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
OliverBooth/Markdown/TemplateInline.cs
Normal file
33
OliverBooth/Markdown/TemplateInline.cs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
using Markdig.Syntax.Inlines;
|
||||||
|
|
||||||
|
namespace OliverBooth.Markdown;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a Markdown inline element that represents a MediaWiki-style template.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TemplateInline : Inline
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the raw argument string.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The raw argument string.</value>
|
||||||
|
public string ArgumentString { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the argument list.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The argument list.</value>
|
||||||
|
public IReadOnlyList<string> ArgumentList { get; set; } = ArraySegment<string>.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name of the template.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The name of the template.</value>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the template parameters.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The template parameters.</value>
|
||||||
|
public Dictionary<string, string> Params { get; set; } = new();
|
||||||
|
}
|
167
OliverBooth/Markdown/TemplateInlineParser.cs
Normal file
167
OliverBooth/Markdown/TemplateInlineParser.cs
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
using Cysharp.Text;
|
||||||
|
using Markdig.Helpers;
|
||||||
|
using Markdig.Parsers;
|
||||||
|
|
||||||
|
namespace OliverBooth.Markdown;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a Markdown inline parser that handles MediaWiki-style templates.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TemplateInlineParser : InlineParser
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
||||||
|
{
|
||||||
|
ReadOnlySpan<char> span = slice.Text.AsSpan();
|
||||||
|
ReadOnlySpan<char> template = span[slice.Start..];
|
||||||
|
|
||||||
|
if (!template.StartsWith("{{"))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int endIndex = template.IndexOf("}}");
|
||||||
|
if (endIndex == -1)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
template = template[2..endIndex];
|
||||||
|
ReadOnlySpan<char> templateName = template;
|
||||||
|
int pipeIndex = template.IndexOf('|');
|
||||||
|
var templateArgs = new Dictionary<string, string>();
|
||||||
|
var rawArgumentString = string.Empty;
|
||||||
|
var argumentList = new List<string>();
|
||||||
|
|
||||||
|
if (pipeIndex != -1)
|
||||||
|
{
|
||||||
|
templateName = templateName[..pipeIndex];
|
||||||
|
rawArgumentString = template[(pipeIndex + 1)..].ToString();
|
||||||
|
|
||||||
|
ReadOnlySpan<char> args = template[(pipeIndex + 1)..];
|
||||||
|
|
||||||
|
using Utf8ValueStringBuilder keyBuilder = ZString.CreateUtf8StringBuilder();
|
||||||
|
using Utf8ValueStringBuilder valueBuilder = ZString.CreateUtf8StringBuilder();
|
||||||
|
|
||||||
|
var isKey = true;
|
||||||
|
var isEscape = false;
|
||||||
|
var nestLevel = 0;
|
||||||
|
|
||||||
|
for (var index = 0; index < args.Length; index++)
|
||||||
|
{
|
||||||
|
char current = args[index];
|
||||||
|
var key = keyBuilder.ToString();
|
||||||
|
var value = valueBuilder.ToString();
|
||||||
|
|
||||||
|
if (current == '=' && isKey && !isEscape)
|
||||||
|
{
|
||||||
|
isKey = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current == '{' && index < args.Length - 1 && args[index + 1] == '{')
|
||||||
|
{
|
||||||
|
nestLevel++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current == '}' && index < args.Length - 1 && args[index + 1] == '}')
|
||||||
|
{
|
||||||
|
if (nestLevel == 0)
|
||||||
|
{
|
||||||
|
template = template[..(pipeIndex + 1 + index)];
|
||||||
|
args = args[..index];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
nestLevel--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isKey)
|
||||||
|
{
|
||||||
|
if (current == '\'')
|
||||||
|
{
|
||||||
|
if (isEscape)
|
||||||
|
{
|
||||||
|
keyBuilder.Append('\'');
|
||||||
|
isEscape = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
isEscape = !isEscape;
|
||||||
|
|
||||||
|
if (index == args.Length - 1)
|
||||||
|
{
|
||||||
|
argumentList.Add(isKey ? key : $"{key}={value}");
|
||||||
|
templateArgs.Add(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current == '|' && !isEscape)
|
||||||
|
{
|
||||||
|
argumentList.Add(key);
|
||||||
|
templateArgs.Add(key, string.Empty);
|
||||||
|
keyBuilder.Clear();
|
||||||
|
|
||||||
|
if (index == args.Length - 1)
|
||||||
|
{
|
||||||
|
argumentList.Add(isKey ? key : $"{key}={value}");
|
||||||
|
templateArgs.Add(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
keyBuilder.Append(current);
|
||||||
|
|
||||||
|
if (index == args.Length - 1)
|
||||||
|
{
|
||||||
|
argumentList.Add(isKey ? key : $"{key}={value}");
|
||||||
|
templateArgs.Add(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current == '\'')
|
||||||
|
{
|
||||||
|
if (isEscape)
|
||||||
|
{
|
||||||
|
valueBuilder.Append('\'');
|
||||||
|
isEscape = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
isEscape = !isEscape;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current == '|' && !isEscape)
|
||||||
|
{
|
||||||
|
argumentList.Add($"{key}={value}");
|
||||||
|
templateArgs.Add(key, value);
|
||||||
|
keyBuilder.Clear();
|
||||||
|
valueBuilder.Clear();
|
||||||
|
isKey = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
valueBuilder.Append(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processor.Inline = new TemplateInline
|
||||||
|
{
|
||||||
|
Name = templateName.ToString(),
|
||||||
|
Params = templateArgs,
|
||||||
|
ArgumentString = rawArgumentString,
|
||||||
|
ArgumentList = argumentList.ToArray()
|
||||||
|
};
|
||||||
|
|
||||||
|
slice.End = slice.Start;
|
||||||
|
slice.Start += template.Length + 4;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
28
OliverBooth/Markdown/TemplateRenderer.cs
Normal file
28
OliverBooth/Markdown/TemplateRenderer.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using Markdig.Renderers;
|
||||||
|
using Markdig.Renderers.Html;
|
||||||
|
using OliverBooth.Services;
|
||||||
|
|
||||||
|
namespace OliverBooth.Markdown;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a Markdown object renderer that handles <see cref="TemplateInline" /> elements.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class TemplateRenderer : HtmlObjectRenderer<TemplateInline>
|
||||||
|
{
|
||||||
|
private readonly TemplateService _templateService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="TemplateRenderer" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="templateService">The <see cref="TemplateService" />.</param>
|
||||||
|
public TemplateRenderer(TemplateService templateService)
|
||||||
|
{
|
||||||
|
_templateService = templateService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Write(HtmlRenderer renderer, TemplateInline template)
|
||||||
|
{
|
||||||
|
renderer.Write(_templateService.RenderTemplate(template));
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,7 @@
|
|||||||
<PackageReference Include="SmartFormat.NET" Version="3.2.2"/>
|
<PackageReference Include="SmartFormat.NET" Version="3.2.2"/>
|
||||||
<PackageReference Include="X10D" Version="3.2.2"/>
|
<PackageReference Include="X10D" Version="3.2.2"/>
|
||||||
<PackageReference Include="X10D.Hosting" Version="3.2.2"/>
|
<PackageReference Include="X10D.Hosting" Version="3.2.2"/>
|
||||||
|
<PackageReference Include="ZString" Version="2.5.0"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using Markdig;
|
using Markdig;
|
||||||
using NLog.Extensions.Logging;
|
using NLog.Extensions.Logging;
|
||||||
using OliverBooth.Data;
|
using OliverBooth.Data;
|
||||||
|
using OliverBooth.Markdown;
|
||||||
using OliverBooth.Services;
|
using OliverBooth.Services;
|
||||||
using X10D.Hosting.DependencyInjection;
|
using X10D.Hosting.DependencyInjection;
|
||||||
|
|
||||||
@ -10,7 +11,9 @@ builder.Configuration.AddTomlFile("data/config.toml", true, true);
|
|||||||
builder.Logging.ClearProviders();
|
builder.Logging.ClearProviders();
|
||||||
builder.Logging.AddNLog();
|
builder.Logging.AddNLog();
|
||||||
builder.Services.AddHostedSingleton<LoggingService>();
|
builder.Services.AddHostedSingleton<LoggingService>();
|
||||||
builder.Services.AddSingleton(new MarkdownPipelineBuilder()
|
builder.Services.AddSingleton<TemplateService>();
|
||||||
|
|
||||||
|
builder.Services.AddSingleton(provider => new MarkdownPipelineBuilder()
|
||||||
.UseAbbreviations()
|
.UseAbbreviations()
|
||||||
.UseAdvancedExtensions()
|
.UseAdvancedExtensions()
|
||||||
.UseBootstrap()
|
.UseBootstrap()
|
||||||
@ -23,6 +26,7 @@ builder.Services.AddSingleton(new MarkdownPipelineBuilder()
|
|||||||
.UseMathematics()
|
.UseMathematics()
|
||||||
.UseAutoIdentifiers()
|
.UseAutoIdentifiers()
|
||||||
.UseAutoLinks()
|
.UseAutoLinks()
|
||||||
|
.Use(new TemplateExtension(provider.GetRequiredService<TemplateService>()))
|
||||||
.Build());
|
.Build());
|
||||||
|
|
||||||
builder.Services.AddDbContextFactory<BlogContext>();
|
builder.Services.AddDbContextFactory<BlogContext>();
|
||||||
|
@ -43,7 +43,7 @@ public sealed class BlogService
|
|||||||
/// <returns>The processed content of the blog post.</returns>
|
/// <returns>The processed content of the blog post.</returns>
|
||||||
public string GetContent(BlogPost post)
|
public string GetContent(BlogPost post)
|
||||||
{
|
{
|
||||||
return ProcessContent(post.Body);
|
return RenderContent(post.Body);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -61,7 +61,7 @@ public sealed class BlogService
|
|||||||
int moreIndex = span.IndexOf("<!--more-->", StringComparison.Ordinal);
|
int moreIndex = span.IndexOf("<!--more-->", StringComparison.Ordinal);
|
||||||
trimmed = moreIndex != -1 || span.Length > 256;
|
trimmed = moreIndex != -1 || span.Length > 256;
|
||||||
string result = moreIndex != -1 ? span[..moreIndex].Trim().ToString() : post.Body.Truncate(256);
|
string result = moreIndex != -1 ? span[..moreIndex].Trim().ToString() : post.Body.Truncate(256);
|
||||||
return ProcessContent(result);
|
return RenderContent(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -139,8 +139,8 @@ public sealed class BlogService
|
|||||||
post = context.BlogPosts.FirstOrDefault(p => p.WordPressId == postId);
|
post = context.BlogPosts.FirstOrDefault(p => p.WordPressId == postId);
|
||||||
return post is not null;
|
return post is not null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ProcessContent(string content)
|
private string RenderContent(string content)
|
||||||
{
|
{
|
||||||
content = content.Replace("<!--more-->", string.Empty);
|
content = content.Replace("<!--more-->", string.Empty);
|
||||||
|
|
||||||
@ -149,6 +149,6 @@ public sealed class BlogService
|
|||||||
content = content.Replace("\n\n", "\n");
|
content = content.Replace("\n\n", "\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Markdown.ToHtml(content.Trim(), _markdownPipeline);
|
return Markdig.Markdown.ToHtml(content.Trim(), _markdownPipeline);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
69
OliverBooth/Services/TemplateService.cs
Normal file
69
OliverBooth/Services/TemplateService.cs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using OliverBooth.Data;
|
||||||
|
using OliverBooth.Data.Web;
|
||||||
|
using OliverBooth.Markdown;
|
||||||
|
using SmartFormat;
|
||||||
|
using SmartFormat.Extensions;
|
||||||
|
|
||||||
|
namespace OliverBooth.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a service that renders MediaWiki-style templates.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class TemplateService
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<WebContext> _webContextFactory;
|
||||||
|
private readonly SmartFormatter _formatter;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="TemplateService" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="webContextFactory">The <see cref="WebContext" /> factory.</param>
|
||||||
|
public TemplateService(IDbContextFactory<WebContext> webContextFactory)
|
||||||
|
{
|
||||||
|
_formatter = Smart.CreateDefaultSmartFormat();
|
||||||
|
_formatter.AddExtensions(new DefaultSource());
|
||||||
|
_formatter.AddExtensions(new ReflectionSource());
|
||||||
|
_formatter.AddExtensions(new DateFormatter());
|
||||||
|
|
||||||
|
_webContextFactory = webContextFactory;
|
||||||
|
Current = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TemplateService Current { get; private set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Renders the specified template with the specified arguments.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="templateInline">The template to render.</param>
|
||||||
|
/// <returns>The rendered template.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// <paramref name="templateInline" /> is <see langword="null" />.
|
||||||
|
/// </exception>
|
||||||
|
public string RenderTemplate(TemplateInline templateInline)
|
||||||
|
{
|
||||||
|
if (templateInline is null) throw new ArgumentNullException(nameof(templateInline));
|
||||||
|
using WebContext webContext = _webContextFactory.CreateDbContext();
|
||||||
|
ArticleTemplate? template = webContext.ArticleTemplates.Find(templateInline.Name);
|
||||||
|
if (template is null)
|
||||||
|
{
|
||||||
|
return $"{{{{{templateInline.Name}}}}}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var formatted = new
|
||||||
|
{
|
||||||
|
templateInline.ArgumentList,
|
||||||
|
templateInline.ArgumentString,
|
||||||
|
templateInline.Params,
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Markdig.Markdown.ToHtml(_formatter.Format(template.FormatString, formatted));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return $"{{{{{templateInline.Name}|{templateInline.ArgumentString}}}}}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user