Use completely standalone Razor template and compiler
This commit is contained in:
parent
4a040985ee
commit
d2ddfa4be4
@ -17,6 +17,7 @@ public record ThreadPost
|
|||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool IsChapterAnnounce { get; set; } = false;
|
public bool IsChapterAnnounce { get; set; } = false;
|
||||||
public ChapterMetadata? Chapter { get; set; }
|
public ChapterMetadata? Chapter { get; set; }
|
||||||
|
public List<int>? RepliesTo { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public record Metadata
|
public record Metadata
|
||||||
@ -25,6 +26,7 @@ public record Metadata
|
|||||||
public string Author { get; set; }
|
public string Author { get; set; }
|
||||||
public Uri AuthorPage { get; set; }
|
public Uri AuthorPage { get; set; }
|
||||||
public string? AuthorTwitter { get; set; }
|
public string? AuthorTwitter { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
public Uri AssetsBaseUrl { get; set; }
|
public Uri AssetsBaseUrl { get; set; }
|
||||||
public string SocialPreview { get; set; }
|
public string SocialPreview { get; set; }
|
||||||
public List<int> Threads { get; set; }
|
public List<int> Threads { get; set; }
|
||||||
@ -62,6 +64,7 @@ public class TemplateModel
|
|||||||
public Metadata Metadata { get; set; }
|
public Metadata Metadata { get; set; }
|
||||||
public DateTime Now { get; set; }
|
public DateTime Now { get; set; }
|
||||||
public List<ThreadPost> Posts { get; set; }
|
public List<ThreadPost> Posts { get; set; }
|
||||||
|
public List<ThreadPost> AllPosts { get; set; }
|
||||||
public string BaseUrl { get; set; }
|
public string BaseUrl { get; set; }
|
||||||
public string ToolVersion { get; set; }
|
public string ToolVersion { get; set; }
|
||||||
}
|
}
|
@ -1,42 +0,0 @@
|
|||||||
using RazorEngineCore;
|
|
||||||
|
|
||||||
namespace QuestReader
|
|
||||||
{
|
|
||||||
public class HtmlSafeTemplate<TModel> : RazorEngineTemplateBase
|
|
||||||
{
|
|
||||||
public new TModel Model { get; set; }
|
|
||||||
|
|
||||||
class RawContent
|
|
||||||
{
|
|
||||||
public object Value { get; set; }
|
|
||||||
|
|
||||||
public RawContent(object value)
|
|
||||||
{
|
|
||||||
Value = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static object Raw(object value)
|
|
||||||
{
|
|
||||||
return new RawContent(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Task WriteAsync(object? obj = null)
|
|
||||||
{
|
|
||||||
var value = obj is not null and RawContent rawContent
|
|
||||||
? rawContent.Value
|
|
||||||
: System.Web.HttpUtility.HtmlEncode(obj);
|
|
||||||
|
|
||||||
return base.WriteAsync(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Task WriteAttributeValueAsync(string prefix, int prefixOffset, object? value, int valueOffset, int valueLength, bool isLiteral)
|
|
||||||
{
|
|
||||||
value = value is RawContent rawContent
|
|
||||||
? rawContent.Value
|
|
||||||
: System.Web.HttpUtility.HtmlAttributeEncode(value?.ToString());
|
|
||||||
|
|
||||||
return base.WriteAttributeValueAsync(prefix, prefixOffset, value, valueOffset, valueLength, isLiteral);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
using QuestReader.Services;
|
using QuestReader.Services;
|
||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
|
|
||||||
|
214
RazorStandalone.cs
Normal file
214
RazorStandalone.cs
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Razor.Language;
|
||||||
|
using Microsoft.AspNetCore.Razor.Language.Extensions;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
using Microsoft.CodeAnalysis.Emit;
|
||||||
|
|
||||||
|
namespace QuestReader;
|
||||||
|
|
||||||
|
public class RazorStandalone<TTemplate>
|
||||||
|
{
|
||||||
|
RazorProjectEngine Engine { get; set; }
|
||||||
|
public RazorStandalone(string @namespace)
|
||||||
|
{
|
||||||
|
var fs = RazorProjectFileSystem.Create(".");
|
||||||
|
|
||||||
|
Engine = RazorProjectEngine.Create(RazorConfiguration.Default, fs, (builder) =>
|
||||||
|
{
|
||||||
|
builder.SetNamespace(@namespace);
|
||||||
|
builder.SetBaseType(typeof(TTemplate).FullName);
|
||||||
|
builder.AddTargetExtension(new TemplateTargetExtension()
|
||||||
|
{
|
||||||
|
TemplateTypeName = "RazorStandaloneHelperResult",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public TTemplate? Compile(string filename)
|
||||||
|
{
|
||||||
|
var doc = RazorSourceDocument.Create(File.ReadAllText("page_template.cshtml"), Path.GetFileName(filename));
|
||||||
|
|
||||||
|
var codeDocument = Engine.Process(doc, null, new List<RazorSourceDocument>(), new List<TagHelperDescriptor>());
|
||||||
|
var cs = codeDocument.GetCSharpDocument();
|
||||||
|
var tree = CSharpSyntaxTree.ParseText(cs.GeneratedCode, new CSharpParseOptions(LanguageVersion.Latest));
|
||||||
|
|
||||||
|
var dllName = Path.GetFileNameWithoutExtension(filename) + ".dll";
|
||||||
|
var assemblies = new[]
|
||||||
|
{
|
||||||
|
Assembly.Load(new AssemblyName("netstandard")),
|
||||||
|
typeof(object).Assembly,
|
||||||
|
typeof(Uri).Assembly,
|
||||||
|
Assembly.Load(new AssemblyName("System.Runtime")),
|
||||||
|
Assembly.Load(new AssemblyName("System.Collections")),
|
||||||
|
Assembly.Load(new AssemblyName("System.Linq")),
|
||||||
|
Assembly.Load(new AssemblyName("System.Linq.Expressions")),
|
||||||
|
Assembly.Load(new AssemblyName("Microsoft.CSharp")),
|
||||||
|
Assembly.GetExecutingAssembly(),
|
||||||
|
};
|
||||||
|
|
||||||
|
var compilation = CSharpCompilation.Create(dllName, new[] { tree },
|
||||||
|
assemblies.Select(assembly => MetadataReference.CreateFromFile(assembly.Location)).ToArray(),
|
||||||
|
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
|
||||||
|
);
|
||||||
|
|
||||||
|
var memoryStream = new MemoryStream();
|
||||||
|
|
||||||
|
var result = compilation.Emit(memoryStream, options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb));
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
Console.WriteLine(string.Join(Environment.NewLine, result.Diagnostics));
|
||||||
|
throw new Exception("Welp");
|
||||||
|
}
|
||||||
|
|
||||||
|
var asm = Assembly.Load(memoryStream.ToArray());
|
||||||
|
var templateInstance = (TTemplate?) Activator.CreateInstance(asm.GetType("QuestReader.Template"));
|
||||||
|
if (templateInstance is null)
|
||||||
|
throw new Exception("Template is null");
|
||||||
|
|
||||||
|
return templateInstance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IHelper
|
||||||
|
{
|
||||||
|
public void WriteTo(TextWriter writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RazorStandaloneHelperResult : IHelper
|
||||||
|
{
|
||||||
|
public Action<TextWriter> WriteAction { get; }
|
||||||
|
|
||||||
|
public RazorStandaloneHelperResult(Action<TextWriter> action)
|
||||||
|
{
|
||||||
|
WriteAction = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteTo(TextWriter writer)
|
||||||
|
{
|
||||||
|
WriteAction(writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
using var buffer = new MemoryStream();
|
||||||
|
using var writer = new StreamWriter(buffer, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true), 4096, leaveOpen: true);
|
||||||
|
WriteTo(writer);
|
||||||
|
return Encoding.UTF8.GetString(buffer.ToArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RawContent : IHelper
|
||||||
|
{
|
||||||
|
public object Value { get; set; }
|
||||||
|
|
||||||
|
public RawContent(object value)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteTo(TextWriter writer)
|
||||||
|
{
|
||||||
|
writer.Write(Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => Value?.ToString() ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class StandaloneTemplate<TModel>
|
||||||
|
{
|
||||||
|
public TModel Model { get; set; }
|
||||||
|
|
||||||
|
protected StreamWriter Output { get; set; }
|
||||||
|
|
||||||
|
public async Task WriteLiteralAsync(string literal)
|
||||||
|
{
|
||||||
|
await Output.WriteAsync(literal);
|
||||||
|
}
|
||||||
|
|
||||||
|
string? Suffix {get;set;}
|
||||||
|
|
||||||
|
public async Task BeginWriteAttributeAsync(
|
||||||
|
string name,
|
||||||
|
string prefix, int prefixOffset,
|
||||||
|
string suffix, int suffixOffset,
|
||||||
|
int attributeValuesCount
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Suffix = suffix;
|
||||||
|
await WriteLiteralAsync(prefix);
|
||||||
|
}
|
||||||
|
public async Task WriteAttributeValueAsync(string prefix, int prefixOffset, object? value, int valueOffset, int valueLength, bool isLiteral)
|
||||||
|
{
|
||||||
|
await WriteLiteralAsync(prefix);
|
||||||
|
await WriteAsync(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EndWriteAttributeAsync() {
|
||||||
|
await WriteLiteralAsync(Suffix!);
|
||||||
|
Suffix = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task WriteAsync(object? obj)
|
||||||
|
{
|
||||||
|
if (obj is not null and IHelper helper)
|
||||||
|
helper.WriteTo(Output);
|
||||||
|
else
|
||||||
|
await Output.WriteAsync(System.Web.HttpUtility.HtmlEncode(obj));
|
||||||
|
Output.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ExecuteAsync(Stream stream)
|
||||||
|
{
|
||||||
|
// We technically don't need this intermediate buffer if this method accepts a memory stream.
|
||||||
|
//var buffer = new MemoryStream();
|
||||||
|
Output = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true), 4096, leaveOpen: true);
|
||||||
|
await ExecuteAsync();
|
||||||
|
await Output.FlushAsync();
|
||||||
|
await Output.DisposeAsync();
|
||||||
|
//buffer.Seek(0, SeekOrigin.Begin);
|
||||||
|
//await buffer.CopyToAsync(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PushWriter(TextWriter writer)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamWriter PopWriter()
|
||||||
|
{
|
||||||
|
return Output;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual Task ExecuteAsync()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Write(object? obj)
|
||||||
|
{
|
||||||
|
WriteAsync(obj).Wait();
|
||||||
|
}
|
||||||
|
public void WriteLiteral(string literal)
|
||||||
|
{
|
||||||
|
WriteLiteralAsync(literal).Wait();
|
||||||
|
}
|
||||||
|
public void BeginWriteAttribute(string name, string prefix, int prefixOffset, string suffix, int suffixOffset, int attributeValuesCount)
|
||||||
|
{
|
||||||
|
BeginWriteAttributeAsync(name, prefix, prefixOffset, suffix, suffixOffset, attributeValuesCount).Wait();
|
||||||
|
}
|
||||||
|
public void WriteAttributeValue(string prefix, int prefixOffset, object value, int valueOffset, int valueLength, bool isLiteral)
|
||||||
|
{
|
||||||
|
WriteAttributeValueAsync(prefix, prefixOffset, value, valueOffset, valueLength, isLiteral).Wait();
|
||||||
|
}
|
||||||
|
public void EndWriteAttribute()
|
||||||
|
{
|
||||||
|
EndWriteAttributeAsync().Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static object Raw(object value)
|
||||||
|
{
|
||||||
|
return new RawContent(value);
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,10 @@
|
|||||||
namespace QuestReader.Services;
|
namespace QuestReader.Services;
|
||||||
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using RazorEngineCore;
|
|
||||||
|
|
||||||
public class Generator
|
public class Generator
|
||||||
{
|
{
|
||||||
public IRazorEngineCompiledTemplate<HtmlSafeTemplate<TemplateModel>> RazorTemplate { get; set; }
|
public StandaloneTemplate<TemplateModel> RazorTemplate { get; set; }
|
||||||
|
|
||||||
public string QuestName { get; set; }
|
public string QuestName { get; set; }
|
||||||
|
|
||||||
@ -27,38 +26,34 @@ public class Generator
|
|||||||
p.Chapter = PostsSource.Metadata.Chapters.Single(c => (c.Announce ?? c.Start) == p.Id);
|
p.Chapter = PostsSource.Metadata.Chapters.Single(c => (c.Announce ?? c.Start) == p.Id);
|
||||||
});
|
});
|
||||||
|
|
||||||
var razorEngine = new RazorEngine();
|
var razorEngine = new RazorStandalone<StandaloneTemplate<TemplateModel>>("QuestReader");
|
||||||
var templateFile = "page_template.cshtml";
|
var templateFile = "page_template.cshtml";
|
||||||
var baseUrl = "";
|
var baseUrl = "";
|
||||||
RazorTemplate = razorEngine.Compile<HtmlSafeTemplate<TemplateModel>>(
|
RazorTemplate = razorEngine.Compile(
|
||||||
File.ReadAllText("page_template.cshtml"),
|
"page_template.cshtml"
|
||||||
option => {
|
) ?? throw new Exception("No template");
|
||||||
option.AddAssemblyReferenceByName("System.Collections");
|
|
||||||
option.AddAssemblyReferenceByName("System.Private.Uri");
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Console.WriteLine($"Using \"{templateFile}\" with base URL {baseUrl}");
|
Console.WriteLine($"Using \"{templateFile}\" with base URL {baseUrl}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Run()
|
public string Run()
|
||||||
{
|
{
|
||||||
string result = RazorTemplate.Run(instance =>
|
RazorTemplate.Model = new TemplateModel
|
||||||
{
|
|
||||||
instance.Model = new TemplateModel
|
|
||||||
{
|
{
|
||||||
Metadata = PostsSource.Metadata,
|
Metadata = PostsSource.Metadata,
|
||||||
Posts = PostsSource.Accepted,
|
Posts = PostsSource.Accepted,
|
||||||
|
AllPosts = PostsSource.Posts,
|
||||||
Now = @DateTime.UtcNow,
|
Now = @DateTime.UtcNow,
|
||||||
//BaseUrl = "assets"
|
|
||||||
BaseUrl = $"/static/{QuestName}",
|
BaseUrl = $"/static/{QuestName}",
|
||||||
ToolVersion = Assembly.GetEntryAssembly()?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "unknown"
|
ToolVersion = Assembly.GetEntryAssembly()?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "unknown"
|
||||||
};
|
};
|
||||||
});
|
|
||||||
|
|
||||||
Console.WriteLine($"Template output {result.Length} bytes");
|
var outputStream = new MemoryStream();
|
||||||
|
RazorTemplate.ExecuteAsync(outputStream).Wait();
|
||||||
|
|
||||||
var outputPath = Path.Join(QuestPath, "output.html");
|
var outputPath = Path.Join(QuestPath, "output.html");
|
||||||
File.WriteAllText(outputPath, result);
|
Console.WriteLine($"Template output {outputStream.Length} bytes");
|
||||||
|
File.WriteAllBytes(outputPath, outputStream.ToArray());
|
||||||
Console.WriteLine($"Wrote output to {outputPath}");
|
Console.WriteLine($"Wrote output to {outputPath}");
|
||||||
return outputPath;
|
return outputPath;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ namespace QuestReader.Services;
|
|||||||
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
public class PostsSource
|
public class PostsSource
|
||||||
{
|
{
|
||||||
@ -16,7 +17,8 @@ public class PostsSource
|
|||||||
var options = new JsonSerializerOptions
|
var options = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
|
WriteIndented = true
|
||||||
};
|
};
|
||||||
|
|
||||||
using var fileStream = File.OpenRead(Path.Combine(basePath, "metadata.json"));
|
using var fileStream = File.OpenRead(Path.Combine(basePath, "metadata.json"));
|
||||||
@ -39,5 +41,24 @@ public class PostsSource
|
|||||||
?? throw new InvalidDataException("Empty deserialisation result for quest metadata");
|
?? throw new InvalidDataException("Empty deserialisation result for quest metadata");
|
||||||
Accepted = Posts.Where(p => ids.Contains(p.Id)).ToList();
|
Accepted = Posts.Where(p => ids.Contains(p.Id)).ToList();
|
||||||
Console.Out.WriteLine($"Loaded a list of {Accepted.Count} posts, referencing {Accepted.Where(a => a.File is not null).Count()} files");
|
Console.Out.WriteLine($"Loaded a list of {Accepted.Count} posts, referencing {Accepted.Where(a => a.File is not null).Count()} files");
|
||||||
|
|
||||||
|
var rx = new Regex(@"data-post-ref=""(\d+)""",
|
||||||
|
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
foreach (var post in Posts)
|
||||||
|
{
|
||||||
|
var matches = rx.Matches(post.RawHtml);
|
||||||
|
if (!matches.Any())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
post.RepliesTo = new List<int>();
|
||||||
|
foreach (Match match in matches)
|
||||||
|
{
|
||||||
|
var replyId = int.Parse(match.Groups[1].Value);
|
||||||
|
var found = Posts.FirstOrDefault(p => p.Id == replyId);
|
||||||
|
if (found is null)
|
||||||
|
continue;
|
||||||
|
post.RepliesTo.Add(replyId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
@namespace QuestReader
|
@namespace QuestReader
|
||||||
@inherits HtmlSafeTemplate<TemplateModel>
|
@using System
|
||||||
|
@using System.Linq
|
||||||
|
@inherits StandaloneTemplate<TemplateModel>
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@ -11,8 +12,9 @@
|
|||||||
|
|
||||||
@{
|
@{
|
||||||
var title = $"\"{Model.Metadata.Name}\" by {Model.Metadata.Author}";
|
var title = $"\"{Model.Metadata.Name}\" by {Model.Metadata.Author}";
|
||||||
var description = $"Quest single-page archive. Generated {Model.Now} (UTC), {Model.Posts.Count} posts, {Model.Metadata.Chapters.Count} chapters";
|
var autoDescription = $"Quest single-page archive. Generated {Model.Now} (UTC), {Model.Posts.Count} posts, {Model.Metadata.Chapters.Count} chapters";
|
||||||
// A hack, tbh, should be something better instead..
|
// A hack, tbh, should be something better instead..
|
||||||
|
var description = Model.Metadata.Description ?? autoDescription;
|
||||||
var preview = $"https://media.lunar.exchange{Model.BaseUrl}/{Model.Metadata.SocialPreview}";
|
var preview = $"https://media.lunar.exchange{Model.BaseUrl}/{Model.Metadata.SocialPreview}";
|
||||||
}
|
}
|
||||||
<title>@title</title>
|
<title>@title</title>
|
||||||
@ -44,10 +46,33 @@
|
|||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h1><span class="quest-title">@Model.Metadata.Name</span> by <a href="@Model.Metadata.AuthorPage" class="quest-author">@Model.Metadata.Author</a></h1>
|
<h1><span class="quest-title">@Model.Metadata.Name</span> by <a href="@Model.Metadata.AuthorPage" class="quest-author">@Model.Metadata.Author</a></h1>
|
||||||
<p>@description</p>
|
@if (Model.Metadata.Description is not null)
|
||||||
<p>Posts from @Model.Posts[0].Date - @Model.Posts[Model.Posts.Count -1].Date</p>
|
{
|
||||||
|
<p>@Model.Metadata.Description</p>
|
||||||
|
}
|
||||||
|
<p>@autoDescription</p>
|
||||||
|
<p>Posts from @Model.Posts.First().Date - @Model.Posts.Last().Date</p>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
|
@{
|
||||||
|
Func<(ThreadPost, bool), object> makePost =
|
||||||
|
@<article id="post-@item.Item1.Id" class="post@(item.Item1 is not null ? " image-post" : "")@(item.Item2 ? "" : " suggestion-post")">
|
||||||
|
@if (item.Item1.Title is not null) {
|
||||||
|
<h2 class="post-self-title">@item.Item1.Title</h2>
|
||||||
|
}
|
||||||
|
<h3 class="post-header"><a class="post-anchor" href="#post-@item.Item1.Id"><span class="post-anchor-mark">#</span>@item.Item1.Id</a> <span class="author">@item.Item1.Author</span> <time>@item.Item1.Date</time></h3>
|
||||||
|
<div class="post-content">
|
||||||
|
@if (item.Item1.File is not null) {
|
||||||
|
<figure class="post-image">
|
||||||
|
<img src="@Model.BaseUrl/@item.Item1.File" alt="@item.Item1.Filename">
|
||||||
|
</figure>
|
||||||
|
}
|
||||||
|
@if (item.Item1.RawHtml.Trim().Length > 0) {
|
||||||
|
<div class="post-text">@Raw(item.Item1.RawHtml)</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</article>;
|
||||||
|
}
|
||||||
@foreach (var item in Model.Posts)
|
@foreach (var item in Model.Posts)
|
||||||
{
|
{
|
||||||
@if (item.IsChapterAnnounce) {
|
@if (item.IsChapterAnnounce) {
|
||||||
@ -55,26 +80,19 @@
|
|||||||
<a class="chapter-anchor" href="#chapter-@item.Chapter.Id">#</a> <span class="chapter-name">@item.Chapter.Name</span> - <span class="chapter-subtitle">@item.Chapter.Subtitle</span>
|
<a class="chapter-anchor" href="#chapter-@item.Chapter.Id">#</a> <span class="chapter-name">@item.Chapter.Name</span> - <span class="chapter-subtitle">@item.Chapter.Subtitle</span>
|
||||||
</h2>
|
</h2>
|
||||||
}
|
}
|
||||||
<article id="post-@item.Id" class="post@(item.File is not null ? " image-post" : "")">
|
if (item.RepliesTo is not null && item.RepliesTo.Count > 0)
|
||||||
@if (item.Title is not null) {
|
{
|
||||||
<h2 class="post-self-title">@item.Title</h2>
|
@foreach (var replyId in item.RepliesTo)
|
||||||
|
{
|
||||||
|
@makePost((Model.AllPosts.First(p => p.Id == replyId), false))
|
||||||
}
|
}
|
||||||
<h3 class="post-header"><a class="post-anchor" href="#post-@item.Id"><span class="post-anchor-mark">#</span>@item.Id</a> <span class="author">@item.Author</span> <time>@item.Date</time></h3>
|
|
||||||
<div class="post-content">
|
|
||||||
@if (item.File is not null) {
|
|
||||||
<figure class="post-image">
|
|
||||||
<img src="@Model.BaseUrl/@item.File" alt="@item.Filename">
|
|
||||||
</figure>
|
|
||||||
}
|
}
|
||||||
@if (item.RawHtml.Trim().Length > 0) {
|
|
||||||
<blockquote>@Raw(item.RawHtml)</blockquote>
|
@makePost((item, true));
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
}
|
}
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
<p>Ⓒ @Model.Metadata.Author @Model.Posts[0].Date.Year - @Model.Posts[Model.Posts.Count -1].Date.Year, page generated with <span><a href="https://source.lunar.exchange/Saphire/quest-reader">quest-reader</a> v.<span class="commit-hash">@Model.ToolVersion</span></span> by <a href="https://saphi.re">Saphire</a></p>
|
<p>Ⓒ @Model.Metadata.Author @Model.Posts.First().Date.Year - @Model.Posts.Last().Date.Year, page generated with <span><a href="https://source.lunar.exchange/Saphire/quest-reader">quest-reader</a> v.<span class="commit-hash">@Model.ToolVersion</span></span> by <a href="https://saphi.re">Saphire</a></p>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -11,9 +11,10 @@
|
|||||||
<NoWarn>$(NoWarn);1591;8618</NoWarn>
|
<NoWarn>$(NoWarn);1591;8618</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.42" />
|
|
||||||
<PackageReference Include="RazorEngineCore" Version="2022.1.2" />
|
|
||||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta3.22111.2" />
|
<PackageReference Include="System.CommandLine" Version="2.0.0-beta3.22111.2" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Razor.Language" Version="6.0.2" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.0.1" />
|
||||||
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.42" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Target Name="SetSourceRevisionId" BeforeTargets="InitializeSourceControlInformation">
|
<Target Name="SetSourceRevisionId" BeforeTargets="InitializeSourceControlInformation">
|
||||||
<Exec
|
<Exec
|
||||||
|
Loading…
Reference in New Issue
Block a user