Compare commits
No commits in common. "720b6364394bb896e496755e474b07aeaf82a960" and "ba09fa22df9c98635a6e0a5c8d46bb18b104f5c6" have entirely different histories.
720b636439
...
ba09fa22df
@ -1,14 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
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,12 +25,6 @@ internal sealed class WebContext : DbContext
|
||||
/// <value>The collection of books.</value>
|
||||
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>
|
||||
/// Gets the collection of blacklist entries in the database.
|
||||
/// </summary>
|
||||
@ -86,7 +80,6 @@ internal sealed class WebContext : DbContext
|
||||
{
|
||||
modelBuilder.ApplyConfiguration(new BlacklistEntryConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new BookConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new CodeSnippetConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new ProgrammingLanguageConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new ProjectConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new TemplateConfiguration());
|
||||
|
@ -1,139 +0,0 @@
|
||||
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)}}}}}";
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
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,12 +33,10 @@ builder.Services.AddSingleton(provider => new MarkdownPipelineBuilder()
|
||||
builder.Services.AddDbContextFactory<BlogContext>();
|
||||
builder.Services.AddDbContextFactory<WebContext>();
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddSingleton<ICodeSnippetService, CodeSnippetService>();
|
||||
builder.Services.AddSingleton<IContactService, ContactService>();
|
||||
builder.Services.AddSingleton<ITemplateService, TemplateService>();
|
||||
builder.Services.AddSingleton<IBlogPostService, BlogPostService>();
|
||||
builder.Services.AddSingleton<IBlogUserService, BlogUserService>();
|
||||
builder.Services.AddSingleton<IProgrammingLanguageService, ProgrammingLanguageService>();
|
||||
builder.Services.AddSingleton<IProjectService, ProjectService>();
|
||||
builder.Services.AddSingleton<IMastodonService, MastodonService>();
|
||||
builder.Services.AddSingleton<ITutorialService, TutorialService>();
|
||||
|
@ -1,48 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
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);
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
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,7 +1,5 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Markdig;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OliverBooth.Data.Web;
|
||||
using OliverBooth.Formatting;
|
||||
@ -16,51 +14,31 @@ namespace OliverBooth.Services;
|
||||
/// </summary>
|
||||
internal sealed class TemplateService : ITemplateService
|
||||
{
|
||||
private readonly Dictionary<string, CustomTemplateRenderer> _customTemplateRendererOverrides = new();
|
||||
private static readonly Random Random = new();
|
||||
private readonly ILogger<TemplateService> _logger;
|
||||
private readonly IDbContextFactory<WebContext> _webContextFactory;
|
||||
private readonly SmartFormatter _formatter;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TemplateService" /> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="serviceProvider">The <see cref="IServiceProvider" />.</param>
|
||||
/// <param name="webContextFactory">The <see cref="WebContext" /> factory.</param>
|
||||
public TemplateService(ILogger<TemplateService> logger,
|
||||
IServiceProvider serviceProvider,
|
||||
public TemplateService(IServiceProvider serviceProvider,
|
||||
IDbContextFactory<WebContext> webContextFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
_formatter = Smart.CreateDefaultSmartFormat();
|
||||
_formatter.AddExtensions(new DefaultSource());
|
||||
_formatter.AddExtensions(new ReflectionSource());
|
||||
_formatter.AddExtensions(new DateFormatter());
|
||||
_formatter.AddExtensions(new MarkdownFormatter(serviceProvider));
|
||||
|
||||
_logger.LogDebug("Registering template override Snippet to CodeSnippetTemplateRenderer");
|
||||
AddRendererOverride("Snippet", new CodeSnippetTemplateRenderer(serviceProvider));
|
||||
|
||||
_webContextFactory = webContextFactory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string RenderGlobalTemplate(TemplateInline 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);
|
||||
}
|
||||
if (templateInline is null) throw new ArgumentNullException(nameof(templateInline));
|
||||
|
||||
return TryGetTemplate(templateInline.Name, templateInline.Variant, out ITemplate? template)
|
||||
? RenderTemplate(templateInline, template)
|
||||
@ -111,12 +89,6 @@ internal sealed class TemplateService : ITemplateService
|
||||
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)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(templateInline.ArgumentString)
|
||||
|
28
src/ts/UI.ts
28
src/ts/UI.ts
@ -77,7 +77,6 @@ class UI {
|
||||
UI.addHighlighting(element);
|
||||
UI.addBootstrapTooltips(element);
|
||||
UI.renderSpoilers(element);
|
||||
UI.renderTabs(element);
|
||||
UI.renderTeX(element);
|
||||
UI.renderTimestamps(element);
|
||||
UI.updateProjectCards(element);
|
||||
@ -147,33 +146,6 @@ 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.
|
||||
* @param element The element to search for TeX in.
|
||||
|
Loading…
Reference in New Issue
Block a user