Merge branch 'feature/code_snippets'
This commit is contained in:
commit
720b636439
14
OliverBooth/Data/Web/CodeSnippet.cs
Normal file
14
OliverBooth/Data/Web/CodeSnippet.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
namespace OliverBooth.Data.Web;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal sealed class CodeSnippet : ICodeSnippet
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Content { get; } = string.Empty;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int Id { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string Language { get; } = string.Empty;
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace OliverBooth.Data.Web.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the configuration for the <see cref="Book" /> entity.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class CodeSnippetConfiguration : IEntityTypeConfiguration<CodeSnippet>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Configure(EntityTypeBuilder<CodeSnippet> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("CodeSnippet");
|
||||||
|
builder.HasKey(e => new { e.Id, e.Language });
|
||||||
|
|
||||||
|
builder.Property(e => e.Id);
|
||||||
|
builder.Property(e => e.Language);
|
||||||
|
builder.Property(e => e.Content);
|
||||||
|
}
|
||||||
|
}
|
25
OliverBooth/Data/Web/ICodeSnippet.cs
Normal file
25
OliverBooth/Data/Web/ICodeSnippet.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
namespace OliverBooth.Data.Web;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a code snippet.
|
||||||
|
/// </summary>
|
||||||
|
public interface ICodeSnippet
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the content for this snippet.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The content for this snippet</value>
|
||||||
|
string Content { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the ID for this snippet.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The ID for this snippet</value>
|
||||||
|
int Id { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the language for this snippet.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The language for this snippet</value>
|
||||||
|
string Language { get; }
|
||||||
|
}
|
@ -25,6 +25,12 @@ internal sealed class WebContext : DbContext
|
|||||||
/// <value>The collection of books.</value>
|
/// <value>The collection of books.</value>
|
||||||
public DbSet<Book> Books { get; private set; } = null!;
|
public DbSet<Book> Books { get; private set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the collection of code snippets in the database.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>The collection of code snippets.</value>
|
||||||
|
public DbSet<CodeSnippet> CodeSnippets { get; private set; } = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the collection of blacklist entries in the database.
|
/// Gets the collection of blacklist entries in the database.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -80,6 +86,7 @@ internal sealed class WebContext : DbContext
|
|||||||
{
|
{
|
||||||
modelBuilder.ApplyConfiguration(new BlacklistEntryConfiguration());
|
modelBuilder.ApplyConfiguration(new BlacklistEntryConfiguration());
|
||||||
modelBuilder.ApplyConfiguration(new BookConfiguration());
|
modelBuilder.ApplyConfiguration(new BookConfiguration());
|
||||||
|
modelBuilder.ApplyConfiguration(new CodeSnippetConfiguration());
|
||||||
modelBuilder.ApplyConfiguration(new ProgrammingLanguageConfiguration());
|
modelBuilder.ApplyConfiguration(new ProgrammingLanguageConfiguration());
|
||||||
modelBuilder.ApplyConfiguration(new ProjectConfiguration());
|
modelBuilder.ApplyConfiguration(new ProjectConfiguration());
|
||||||
modelBuilder.ApplyConfiguration(new TemplateConfiguration());
|
modelBuilder.ApplyConfiguration(new TemplateConfiguration());
|
||||||
|
139
OliverBooth/Markdown/Template/CodeSnippetTemplateRenderer.cs
Normal file
139
OliverBooth/Markdown/Template/CodeSnippetTemplateRenderer.cs
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
using Markdig;
|
||||||
|
using OliverBooth.Data.Web;
|
||||||
|
using OliverBooth.Services;
|
||||||
|
|
||||||
|
namespace OliverBooth.Markdown.Template;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a custom template renderer which renders the <c>{{Snippet}}</c> template.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class CodeSnippetTemplateRenderer : CustomTemplateRenderer
|
||||||
|
{
|
||||||
|
private readonly ICodeSnippetService _codeSnippetService;
|
||||||
|
private readonly Lazy<MarkdownPipeline> _markdownPipeline;
|
||||||
|
private readonly IProgrammingLanguageService _programmingLanguageService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="CodeSnippetTemplateRenderer" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serviceProvider">The service provider.</param>
|
||||||
|
public CodeSnippetTemplateRenderer(IServiceProvider serviceProvider) : base(serviceProvider)
|
||||||
|
{
|
||||||
|
// lazily evaluate to avoid circular dependency problem causing tremendous stack overflow
|
||||||
|
_markdownPipeline = new Lazy<MarkdownPipeline>(serviceProvider.GetRequiredService<MarkdownPipeline>);
|
||||||
|
_codeSnippetService = serviceProvider.GetRequiredService<ICodeSnippetService>();
|
||||||
|
_programmingLanguageService = serviceProvider.GetRequiredService<IProgrammingLanguageService>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string Render(TemplateInline template)
|
||||||
|
{
|
||||||
|
Debug.Assert(template.Name == "Snippet");
|
||||||
|
Trace.Assert(template.Name == "Snippet");
|
||||||
|
|
||||||
|
IReadOnlyList<string> argumentList = template.ArgumentList;
|
||||||
|
|
||||||
|
if (argumentList.Count < 1)
|
||||||
|
{
|
||||||
|
return DefaultRender(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!int.TryParse(argumentList[0], out int snippetId))
|
||||||
|
{
|
||||||
|
return DefaultRender(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
var identifier = Guid.NewGuid();
|
||||||
|
var snippets = new List<ICodeSnippet>();
|
||||||
|
|
||||||
|
IReadOnlyList<string> languages = argumentList.Count > 1
|
||||||
|
? argumentList[1].Split(';')
|
||||||
|
: _codeSnippetService.GetLanguagesForSnippet(snippetId);
|
||||||
|
|
||||||
|
foreach (string language in languages)
|
||||||
|
{
|
||||||
|
if (_codeSnippetService.TryGetCodeSnippetForLanguage(snippetId, language, out ICodeSnippet? snippet))
|
||||||
|
{
|
||||||
|
snippets.Add(snippet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snippets.Count == 1)
|
||||||
|
{
|
||||||
|
ICodeSnippet snippet = snippets[0];
|
||||||
|
return RenderHtml(snippet);
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.AppendLine($"""
|
||||||
|
<ul class="nav nav-tabs mb-3" id="snp-{identifier:N}" data-identifier="{identifier:N}" role="tablist"
|
||||||
|
style="margin-bottom: -0.5em !important;">
|
||||||
|
""");
|
||||||
|
|
||||||
|
for (var index = 0; index < languages.Count; index++)
|
||||||
|
{
|
||||||
|
var language = languages[index];
|
||||||
|
string classList = "";
|
||||||
|
if (index == 0)
|
||||||
|
{
|
||||||
|
classList = " active";
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine("""<li class="nav-item" role="presentation">""");
|
||||||
|
builder.AppendLine($"""
|
||||||
|
<a
|
||||||
|
data-tab-init
|
||||||
|
class="nav-link{classList}"
|
||||||
|
id="snp-{snippetId}-{identifier:N}-{language}-l"
|
||||||
|
href="#snp-{snippetId}-{identifier:N}-{language}"
|
||||||
|
role="tab"
|
||||||
|
data-tabs="snp-{snippetId}-{identifier:N}"
|
||||||
|
aria-controls="snp-{snippetId}-{identifier:N}-{language}"
|
||||||
|
aria-selected="true"
|
||||||
|
>{_programmingLanguageService.GetLanguageName(language)}</a
|
||||||
|
>
|
||||||
|
""");
|
||||||
|
builder.AppendLine("</li>");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine("</ul>");
|
||||||
|
|
||||||
|
builder.AppendLine($"""<div class="tab-content" id="snp-{snippetId}-{identifier:N}">""");
|
||||||
|
|
||||||
|
for (var index = 0; index < snippets.Count; index++)
|
||||||
|
{
|
||||||
|
string classList = "";
|
||||||
|
if (index == 0)
|
||||||
|
{
|
||||||
|
classList = " show active";
|
||||||
|
}
|
||||||
|
|
||||||
|
var snippet = snippets[index];
|
||||||
|
string html = RenderHtml(snippet);
|
||||||
|
builder.AppendLine($"""
|
||||||
|
<div class="tab-pane fade{classList}" id="snp-{snippetId}-{identifier:N}-{snippet.Language}" data-identifier="{identifier:N}" role="tabpanel"
|
||||||
|
aria-labelledby="snp-{snippetId}-{identifier:N}-{snippet.Language}">
|
||||||
|
""");
|
||||||
|
builder.AppendLine(html);
|
||||||
|
builder.AppendLine("</div>");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.AppendLine("</div>");
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string RenderHtml(ICodeSnippet snippet)
|
||||||
|
{
|
||||||
|
return Markdig.Markdown.ToHtml($"```{snippet.Language}\n{snippet.Content}\n```", _markdownPipeline.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DefaultRender(TemplateInline template)
|
||||||
|
{
|
||||||
|
return template.ArgumentList.Count == 0
|
||||||
|
? $"{{{{{template.Name}}}}}"
|
||||||
|
: $"{{{{{template.Name}|{string.Join('|', template.ArgumentList)}}}}}";
|
||||||
|
}
|
||||||
|
}
|
32
OliverBooth/Markdown/Template/CustomTemplateRenderer.cs
Normal file
32
OliverBooth/Markdown/Template/CustomTemplateRenderer.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using OliverBooth.Data.Web;
|
||||||
|
|
||||||
|
namespace OliverBooth.Markdown.Template;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a custom renderer which overrides the default behaviour of the template engine.
|
||||||
|
/// </summary>
|
||||||
|
internal abstract class CustomTemplateRenderer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="CustomTemplateRenderer" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serviceProvider">The service provider.</param>
|
||||||
|
protected CustomTemplateRenderer(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
DbContextFactory = serviceProvider.GetRequiredService<IDbContextFactory<WebContext>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="WebContext" /> factory that was injected into this instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <value>An <see cref="IDbContextFactory{TContext}" /> for <see cref="WebContext" />.</value>
|
||||||
|
protected IDbContextFactory<WebContext> DbContextFactory { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Renders the specified template.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="template">The template to render.</param>
|
||||||
|
/// <returns>The rendered result of the template.</returns>
|
||||||
|
public abstract string Render(TemplateInline template);
|
||||||
|
}
|
@ -33,10 +33,12 @@ builder.Services.AddSingleton(provider => new MarkdownPipelineBuilder()
|
|||||||
builder.Services.AddDbContextFactory<BlogContext>();
|
builder.Services.AddDbContextFactory<BlogContext>();
|
||||||
builder.Services.AddDbContextFactory<WebContext>();
|
builder.Services.AddDbContextFactory<WebContext>();
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
|
builder.Services.AddSingleton<ICodeSnippetService, CodeSnippetService>();
|
||||||
builder.Services.AddSingleton<IContactService, ContactService>();
|
builder.Services.AddSingleton<IContactService, ContactService>();
|
||||||
builder.Services.AddSingleton<ITemplateService, TemplateService>();
|
builder.Services.AddSingleton<ITemplateService, TemplateService>();
|
||||||
builder.Services.AddSingleton<IBlogPostService, BlogPostService>();
|
builder.Services.AddSingleton<IBlogPostService, BlogPostService>();
|
||||||
builder.Services.AddSingleton<IBlogUserService, BlogUserService>();
|
builder.Services.AddSingleton<IBlogUserService, BlogUserService>();
|
||||||
|
builder.Services.AddSingleton<IProgrammingLanguageService, ProgrammingLanguageService>();
|
||||||
builder.Services.AddSingleton<IProjectService, ProjectService>();
|
builder.Services.AddSingleton<IProjectService, ProjectService>();
|
||||||
builder.Services.AddSingleton<IMastodonService, MastodonService>();
|
builder.Services.AddSingleton<IMastodonService, MastodonService>();
|
||||||
builder.Services.AddSingleton<ITutorialService, TutorialService>();
|
builder.Services.AddSingleton<ITutorialService, TutorialService>();
|
||||||
|
48
OliverBooth/Services/CodeSnippetService.cs
Normal file
48
OliverBooth/Services/CodeSnippetService.cs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using OliverBooth.Data.Web;
|
||||||
|
|
||||||
|
namespace OliverBooth.Services;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal sealed class CodeSnippetService : ICodeSnippetService
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<WebContext> _dbContextFactory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="CodeSnippetService" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dbContextFactory">The <see cref="WebContext" /> factory.</param>
|
||||||
|
public CodeSnippetService(IDbContextFactory<WebContext> dbContextFactory)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyList<string> GetLanguagesForSnippet(int id)
|
||||||
|
{
|
||||||
|
var languages = new HashSet<string>();
|
||||||
|
using WebContext context = _dbContextFactory.CreateDbContext();
|
||||||
|
|
||||||
|
foreach (CodeSnippet snippet in context.CodeSnippets.Where(s => s.Id == id))
|
||||||
|
{
|
||||||
|
languages.Add(snippet.Language);
|
||||||
|
}
|
||||||
|
|
||||||
|
return languages.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool TryGetCodeSnippetForLanguage(int id, string language, [NotNullWhen(true)] out ICodeSnippet? snippet)
|
||||||
|
{
|
||||||
|
if (language is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(language));
|
||||||
|
}
|
||||||
|
|
||||||
|
using WebContext context = _dbContextFactory.CreateDbContext();
|
||||||
|
IQueryable<CodeSnippet> snippets = context.CodeSnippets.Where(s => s.Id == id);
|
||||||
|
snippet = snippets.FirstOrDefault(s => s.Language == language);
|
||||||
|
return snippet is not null;
|
||||||
|
}
|
||||||
|
}
|
32
OliverBooth/Services/ICodeSnippetService.cs
Normal file
32
OliverBooth/Services/ICodeSnippetService.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using OliverBooth.Data.Web;
|
||||||
|
|
||||||
|
namespace OliverBooth.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a service which can fetch multi-language code snippets.
|
||||||
|
/// </summary>
|
||||||
|
public interface ICodeSnippetService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all the languages which apply to the specified snippet.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The ID of the snippet whose languages should be returned.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A read-only view of the languages that apply to the snippet. This list may be empty if the snippet ID is invalid.
|
||||||
|
/// </returns>
|
||||||
|
IReadOnlyList<string> GetLanguagesForSnippet(int id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to find a code snippet by the specified ID, in the specified language.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The ID of the snippet to search for.</param>
|
||||||
|
/// <param name="language">The language to search for.</param>
|
||||||
|
/// <param name="snippet">
|
||||||
|
/// When this method returns, contains the code snippet matching the specified criteria, if such a snippet was found;
|
||||||
|
/// otherwise, <see langword="null" />.
|
||||||
|
/// </param>
|
||||||
|
/// <returns><see langword="true" /> if the snippet was found; otherwise, <see langword="false" />.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException"><paramref name="language" /> is <see langword="null" />.</exception>
|
||||||
|
bool TryGetCodeSnippetForLanguage(int id, string language, [NotNullWhen(true)] out ICodeSnippet? snippet);
|
||||||
|
}
|
40
OliverBooth/Services/ProgrammingLanguageService.cs
Normal file
40
OliverBooth/Services/ProgrammingLanguageService.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using OliverBooth.Data.Web;
|
||||||
|
|
||||||
|
namespace OliverBooth.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a service which can perform programming language lookup.
|
||||||
|
/// </summary>
|
||||||
|
public interface IProgrammingLanguageService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the human-readable name of a language.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="alias">The alias of the language.</param>
|
||||||
|
/// <returns>The human-readable name, or <paramref name="alias" /> if the name could not be found.</returns>
|
||||||
|
string GetLanguageName(string alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal sealed class ProgrammingLanguageService : IProgrammingLanguageService
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<WebContext> _dbContextFactory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ProgrammingLanguageService" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dbContextFactory">The <see cref="WebContext" /> factory.</param>
|
||||||
|
public ProgrammingLanguageService(IDbContextFactory<WebContext> dbContextFactory)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string GetLanguageName(string alias)
|
||||||
|
{
|
||||||
|
using WebContext context = _dbContextFactory.CreateDbContext();
|
||||||
|
ProgrammingLanguage? language = context.ProgrammingLanguages.FirstOrDefault(l => l.Key == alias);
|
||||||
|
return language?.Name ?? alias;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Markdig;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using OliverBooth.Data.Web;
|
using OliverBooth.Data.Web;
|
||||||
using OliverBooth.Formatting;
|
using OliverBooth.Formatting;
|
||||||
@ -14,31 +16,51 @@ namespace OliverBooth.Services;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class TemplateService : ITemplateService
|
internal sealed class TemplateService : ITemplateService
|
||||||
{
|
{
|
||||||
|
private readonly Dictionary<string, CustomTemplateRenderer> _customTemplateRendererOverrides = new();
|
||||||
private static readonly Random Random = new();
|
private static readonly Random Random = new();
|
||||||
|
private readonly ILogger<TemplateService> _logger;
|
||||||
private readonly IDbContextFactory<WebContext> _webContextFactory;
|
private readonly IDbContextFactory<WebContext> _webContextFactory;
|
||||||
private readonly SmartFormatter _formatter;
|
private readonly SmartFormatter _formatter;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="TemplateService" /> class.
|
/// Initializes a new instance of the <see cref="TemplateService" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="logger">The logger.</param>
|
||||||
/// <param name="serviceProvider">The <see cref="IServiceProvider" />.</param>
|
/// <param name="serviceProvider">The <see cref="IServiceProvider" />.</param>
|
||||||
/// <param name="webContextFactory">The <see cref="WebContext" /> factory.</param>
|
/// <param name="webContextFactory">The <see cref="WebContext" /> factory.</param>
|
||||||
public TemplateService(IServiceProvider serviceProvider,
|
public TemplateService(ILogger<TemplateService> logger,
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
IDbContextFactory<WebContext> webContextFactory)
|
IDbContextFactory<WebContext> webContextFactory)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
_formatter = Smart.CreateDefaultSmartFormat();
|
_formatter = Smart.CreateDefaultSmartFormat();
|
||||||
_formatter.AddExtensions(new DefaultSource());
|
_formatter.AddExtensions(new DefaultSource());
|
||||||
_formatter.AddExtensions(new ReflectionSource());
|
_formatter.AddExtensions(new ReflectionSource());
|
||||||
_formatter.AddExtensions(new DateFormatter());
|
_formatter.AddExtensions(new DateFormatter());
|
||||||
_formatter.AddExtensions(new MarkdownFormatter(serviceProvider));
|
_formatter.AddExtensions(new MarkdownFormatter(serviceProvider));
|
||||||
|
|
||||||
|
_logger.LogDebug("Registering template override Snippet to CodeSnippetTemplateRenderer");
|
||||||
|
AddRendererOverride("Snippet", new CodeSnippetTemplateRenderer(serviceProvider));
|
||||||
|
|
||||||
_webContextFactory = webContextFactory;
|
_webContextFactory = webContextFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string RenderGlobalTemplate(TemplateInline templateInline)
|
public string RenderGlobalTemplate(TemplateInline templateInline)
|
||||||
{
|
{
|
||||||
if (templateInline is null) throw new ArgumentNullException(nameof(templateInline));
|
if (templateInline is null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Attempting to render null inline template!");
|
||||||
|
throw new ArgumentNullException(nameof(templateInline));
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Inline name is {Name}", templateInline.Name);
|
||||||
|
if (_customTemplateRendererOverrides.TryGetValue(templateInline.Name, out CustomTemplateRenderer? renderer))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("This matches renderer {Name}", renderer.GetType().Name);
|
||||||
|
return renderer.Render(templateInline);
|
||||||
|
}
|
||||||
|
|
||||||
return TryGetTemplate(templateInline.Name, templateInline.Variant, out ITemplate? template)
|
return TryGetTemplate(templateInline.Name, templateInline.Variant, out ITemplate? template)
|
||||||
? RenderTemplate(templateInline, template)
|
? RenderTemplate(templateInline, template)
|
||||||
@ -89,6 +111,12 @@ internal sealed class TemplateService : ITemplateService
|
|||||||
return template is not null;
|
return template is not null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AddRendererOverride(string templateName, CustomTemplateRenderer renderer)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Registering template override {Name} to {Renderer}", templateName, renderer.GetType().Name);
|
||||||
|
_customTemplateRendererOverrides[templateName] = renderer;
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetDefaultRender(TemplateInline templateInline)
|
private static string GetDefaultRender(TemplateInline templateInline)
|
||||||
{
|
{
|
||||||
return string.IsNullOrWhiteSpace(templateInline.ArgumentString)
|
return string.IsNullOrWhiteSpace(templateInline.ArgumentString)
|
||||||
|
28
src/ts/UI.ts
28
src/ts/UI.ts
@ -77,6 +77,7 @@ class UI {
|
|||||||
UI.addHighlighting(element);
|
UI.addHighlighting(element);
|
||||||
UI.addBootstrapTooltips(element);
|
UI.addBootstrapTooltips(element);
|
||||||
UI.renderSpoilers(element);
|
UI.renderSpoilers(element);
|
||||||
|
UI.renderTabs(element);
|
||||||
UI.renderTeX(element);
|
UI.renderTeX(element);
|
||||||
UI.renderTimestamps(element);
|
UI.renderTimestamps(element);
|
||||||
UI.updateProjectCards(element);
|
UI.updateProjectCards(element);
|
||||||
@ -146,6 +147,33 @@ class UI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders tabs in the document.
|
||||||
|
* @param element The element to search for tabs in.
|
||||||
|
*/
|
||||||
|
public static renderTabs(element?: Element) {
|
||||||
|
element = element || document.body;
|
||||||
|
element.querySelectorAll("[role=\"tablist\"]").forEach(function (tabList: HTMLElement) {
|
||||||
|
const identifier = tabList.dataset.identifier;
|
||||||
|
const tabLinks = tabList.querySelectorAll(".nav-link");
|
||||||
|
const tabPanes = element.querySelectorAll(`.tab-pane[data-identifier="${identifier}"]`);
|
||||||
|
|
||||||
|
tabLinks.forEach(function (tabLink: Element) {
|
||||||
|
tabLink.addEventListener("click", () => {
|
||||||
|
const controls = document.getElementById(tabLink.getAttribute("aria-controls"));
|
||||||
|
|
||||||
|
// switch "active" tab link
|
||||||
|
tabLinks.forEach(e => e.classList.remove("active"));
|
||||||
|
tabLink.classList.add("active");
|
||||||
|
|
||||||
|
// switch active tab itself
|
||||||
|
tabPanes.forEach(e => e.classList.remove("show", "active"));
|
||||||
|
controls.classList.add("show", "active");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders all TeX in the document.
|
* Renders all TeX in the document.
|
||||||
* @param element The element to search for TeX in.
|
* @param element The element to search for TeX in.
|
||||||
|
Loading…
Reference in New Issue
Block a user