From 2a72fa7552c466a433badb1a6c4e344a2191de3d Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Tue, 6 Feb 2024 13:33:46 +0800 Subject: [PATCH] Upgrade to v8.1 --- MyApp.ServiceInterface/EmailServices.cs | 31 +++----- MyApp.ServiceModel/Hello.cs | 5 +- MyApp/Components/Pages/Posts/Author.razor | 2 +- .../Pages/Posts/{All.razor => Index.razor} | 2 +- MyApp/Components/Pages/Posts/Tagged.razor | 2 +- MyApp/Components/Pages/Posts/Year.razor | 2 +- MyApp/Configure.Auth.cs | 2 +- MyApp/Configure.AutoQuery.cs | 5 +- MyApp/Configure.Markdown.cs | 3 - MyApp/Markdown.Blog.cs | 45 +++++------ MyApp/Markdown.Meta.cs | 60 ++++++++++++++ MyApp/Markdown.Pages.cs | 25 +++--- MyApp/Markdown.Videos.cs | 13 ++-- MyApp/Markdown.WhatsNew.cs | 72 +++++++++++++++++ MyApp/MarkdownPagesBase.cs | 39 ++++------ MyApp/MarkdownTagHelper.cs | 78 +++++++++++++++++++ MyApp/MyApp.csproj | 3 + MyApp/Program.cs | 18 ++++- MyApp/Properties/launchSettings.json | 6 +- 19 files changed, 299 insertions(+), 114 deletions(-) rename MyApp/Components/Pages/Posts/{All.razor => Index.razor} (98%) create mode 100644 MyApp/Markdown.Meta.cs create mode 100644 MyApp/Markdown.WhatsNew.cs create mode 100644 MyApp/MarkdownTagHelper.cs diff --git a/MyApp.ServiceInterface/EmailServices.cs b/MyApp.ServiceInterface/EmailServices.cs index 5bc52b2..5e3974a 100644 --- a/MyApp.ServiceInterface/EmailServices.cs +++ b/MyApp.ServiceInterface/EmailServices.cs @@ -48,32 +48,24 @@ public class SmtpConfig /// /// Uses a configured SMTP client to send emails /// -public class EmailServices : Service +public class EmailServices(SmtpConfig config, ILogger log) + // TODO: Uncomment to enable sending emails with SMTP + // : Service { - public EmailServices(SmtpConfig config, ILogger log) - { - Config = config; - Log = log; - } - - public SmtpConfig Config { get; } - public ILogger Log { get; } - - /* Uncomment to enable sending emails with SMTP public object Any(SendEmail request) { - Log.LogInformation("Sending email to {Email} with subject {Subject}", request.To, request.Subject); + log.LogInformation("Sending email to {Email} with subject {Subject}", request.To, request.Subject); - using var client = new SmtpClient(Config.Host, Config.Port); - client.Credentials = new System.Net.NetworkCredential(Config.Username, Config.Password); + using var client = new SmtpClient(config.Host, config.Port); + client.Credentials = new System.Net.NetworkCredential(config.Username, config.Password); client.EnableSsl = true; // If DevToEmail is set, send all emails to that address instead - var emailTo = Config.DevToEmail != null - ? new MailAddress(Config.DevToEmail) + var emailTo = config.DevToEmail != null + ? new MailAddress(config.DevToEmail) : new MailAddress(request.To, request.ToName); - var emailFrom = new MailAddress(Config.FromEmail, Config.FromName); + var emailFrom = new MailAddress(config.FromEmail, config.FromName); var msg = new MailMessage(emailFrom, emailTo) { @@ -82,14 +74,13 @@ public object Any(SendEmail request) IsBodyHtml = request.BodyHtml != null, }; - if (Config.Bcc != null) + if (config.Bcc != null) { - msg.Bcc.Add(new MailAddress(Config.Bcc)); + msg.Bcc.Add(new MailAddress(config.Bcc)); } client.Send(msg); return new EmptyResponse(); } - */ } diff --git a/MyApp.ServiceModel/Hello.cs b/MyApp.ServiceModel/Hello.cs index ac63beb..5b0a24c 100644 --- a/MyApp.ServiceModel/Hello.cs +++ b/MyApp.ServiceModel/Hello.cs @@ -2,14 +2,13 @@ namespace MyApp.ServiceModel; -[Route("/hello")] [Route("/hello/{Name}")] -public class Hello : IReturn +public class Hello : IGet, IReturn { public string? Name { get; set; } } public class HelloResponse { - public string Result { get; set; } = default!; + public required string Result { get; set; } } \ No newline at end of file diff --git a/MyApp/Components/Pages/Posts/Author.razor b/MyApp/Components/Pages/Posts/Author.razor index 0b6412a..a8c1afd 100644 --- a/MyApp/Components/Pages/Posts/Author.razor +++ b/MyApp/Components/Pages/Posts/Author.razor @@ -17,7 +17,7 @@ diff --git a/MyApp/Components/Pages/Posts/All.razor b/MyApp/Components/Pages/Posts/Index.razor similarity index 98% rename from MyApp/Components/Pages/Posts/All.razor rename to MyApp/Components/Pages/Posts/Index.razor index 08a41f5..884ed8c 100644 --- a/MyApp/Components/Pages/Posts/All.razor +++ b/MyApp/Components/Pages/Posts/Index.razor @@ -1,4 +1,4 @@ -@page "/posts/all" +@page "/posts/" @inject MarkdownBlog Blog @inject AppConfig AppConfig diff --git a/MyApp/Components/Pages/Posts/Tagged.razor b/MyApp/Components/Pages/Posts/Tagged.razor index 1378c3a..fb335e9 100644 --- a/MyApp/Components/Pages/Posts/Tagged.razor +++ b/MyApp/Components/Pages/Posts/Tagged.razor @@ -32,7 +32,7 @@ diff --git a/MyApp/Components/Pages/Posts/Year.razor b/MyApp/Components/Pages/Posts/Year.razor index 276a194..2ce5b67 100644 --- a/MyApp/Components/Pages/Posts/Year.razor +++ b/MyApp/Components/Pages/Posts/Year.razor @@ -32,7 +32,7 @@ diff --git a/MyApp/Configure.Auth.cs b/MyApp/Configure.Auth.cs index c913e11..a8ac18d 100644 --- a/MyApp/Configure.Auth.cs +++ b/MyApp/Configure.Auth.cs @@ -11,8 +11,8 @@ public void Configure(IWebHostBuilder builder) => builder .ConfigureAppHost(appHost => { appHost.Plugins.Add(new AuthFeature(IdentityAuth.For(options => { - options.EnableCredentialsAuth = true; options.SessionFactory = () => new CustomUserSession(); + options.CredentialsAuth(); }))); }); } diff --git a/MyApp/Configure.AutoQuery.cs b/MyApp/Configure.AutoQuery.cs index e3837a8..76077d6 100644 --- a/MyApp/Configure.AutoQuery.cs +++ b/MyApp/Configure.AutoQuery.cs @@ -1,5 +1,4 @@ -using ServiceStack; -using ServiceStack.Data; +using ServiceStack.Data; [assembly: HostingStartup(typeof(MyApp.ConfigureAutoQuery))] @@ -11,7 +10,7 @@ public void Configure(IWebHostBuilder builder) => builder .ConfigureServices(services => { // Enable Audit History services.AddSingleton(c => - new OrmLiteCrudEvents(c.Resolve())); + new OrmLiteCrudEvents(c.GetRequiredService())); }) .ConfigureAppHost(appHost => { diff --git a/MyApp/Configure.Markdown.cs b/MyApp/Configure.Markdown.cs index e0502b7..743b2ee 100644 --- a/MyApp/Configure.Markdown.cs +++ b/MyApp/Configure.Markdown.cs @@ -25,9 +25,6 @@ public void Configure(IWebHostBuilder builder) => builder var blogPosts = appHost.Resolve(); blogPosts.Authors = AppConfig.Instance.Authors; - new IMarkdownPages[] { pages, videos, blogPosts } - .Each(x => x.VirtualFiles = appHost.VirtualFiles); - pages.LoadFrom("_pages"); videos.LoadFrom("_videos"); blogPosts.LoadFrom("_posts"); diff --git a/MyApp/Markdown.Blog.cs b/MyApp/Markdown.Blog.cs index cbb8034..a8b77fd 100644 --- a/MyApp/Markdown.Blog.cs +++ b/MyApp/Markdown.Blog.cs @@ -2,7 +2,6 @@ using System.Globalization; using Markdig; using ServiceStack.IO; -using ServiceStack.Logging; namespace MyApp; @@ -18,18 +17,18 @@ public class AuthorInfo public string? MastodonUrl { get; set; } } -public class MarkdownBlog : MarkdownPagesBase +public class MarkdownBlog(ILogger log, IWebHostEnvironment env, IVirtualFiles fs) + : MarkdownPagesBase(log, env, fs) { public override string Id => "posts"; - public MarkdownBlog(ILogger log, IWebHostEnvironment env) : base(log,env) {} - List Posts { get; set; } = new(); + List Posts { get; set; } = []; public List VisiblePosts => Posts.Where(IsVisible).ToList(); public string FallbackProfileUrl { get; set; } = Svg.ToDataUri(Svg.Create(Svg.Body.User, stroke:"none").Replace("fill='currentColor'","fill='#0891b2'")); public string FallbackSplashUrl { get; set; } = "https://source.unsplash.com/random/2000x1000/?stationary"; - public List Authors { get; set; } = new(); + public List Authors { get; set; } = []; public Dictionary AuthorSlugMap { get; } = new(); public Dictionary TagSlugMap { get; } = new(); @@ -71,7 +70,7 @@ public List GetPosts(string? author = null, string? tag = null public string GetPostLink(MarkdownFileInfo post) => $"posts/{post.Slug}"; - public string GetAllPostsLink() => "posts/all"; + public string GetPostsLink() => "posts/"; public string? GetAuthorLink(string? author) => author != null && Authors.Any(x => x.Name.Equals(author, StringComparison.OrdinalIgnoreCase)) ? $"posts/author/{author.GenerateSlug()}" : null; @@ -81,19 +80,13 @@ public List GetPosts(string? author = null, string? tag = null public string GetDateLabel(DateTime? date) => X.Map(date ?? DateTime.UtcNow, d => d.ToString("MMMM d, yyyy"))!; public string GetDateTimestamp(DateTime? date) => X.Map(date ?? DateTime.UtcNow, d => d.ToString("O"))!; - public AuthorInfo? GetAuthorBySlug(string? slug) - { - return AuthorSlugMap.TryGetValue(slug, out var author) - ? author - : null; - } + public AuthorInfo? GetAuthorBySlug(string? slug) => slug != null && AuthorSlugMap.TryGetValue(slug, out var author) + ? author + : null; - public string? GetTagBySlug(string? slug) - { - return TagSlugMap.TryGetValue(slug, out var tag) - ? tag - : null; - } + public string? GetTagBySlug(string? slug) => slug != null && TagSlugMap.TryGetValue(slug, out var tag) + ? tag + : null; public string GetSplashImage(MarkdownFileInfo post) { @@ -108,14 +101,14 @@ public string GetSplashImage(MarkdownFileInfo post) public override MarkdownFileInfo? Load(string path, MarkdownPipeline? pipeline = null) { var file = VirtualFiles.GetFile(path) - ?? throw new FileNotFoundException(path.LastRightPart('/')); + ?? throw new FileNotFoundException(path.LastRightPart('/')); var content = file.ReadAllText(); var writer = new StringWriter(); var doc = CreateMarkdownFile(content, writer, pipeline); - if (doc?.Title == null) + if (doc.Title == null) { - Log.LogWarning("No frontmatter found for {0}, ignoring...", file.VirtualPath); + log.LogWarning("No frontmatter found for {VirtualPath}, ignoring...", file.VirtualPath); return null; } @@ -127,7 +120,7 @@ public string GetSplashImage(MarkdownFileInfo post) if (!DateTime.TryParseExact(datePart, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var date)) { - Log.LogWarning("Could not parse date '{0}', ignoring...", datePart); + log.LogWarning("Could not parse date '{DatePart}', ignoring...", datePart); return null; } @@ -143,10 +136,8 @@ public string GetSplashImage(MarkdownFileInfo post) public void LoadFrom(string fromDirectory) { Posts.Clear(); - var fs = AssertVirtualFiles(); - var files = fs.GetDirectory(fromDirectory).GetAllFiles().ToList(); - var log = LogManager.GetLogger(GetType()); - log.InfoFormat("Found {0} posts", files.Count); + var files = VirtualFiles.GetDirectory(fromDirectory).GetAllFiles().ToList(); + log.LogInformation("Found {Count} posts", files.Count); var pipeline = CreatePipeline(); @@ -162,7 +153,7 @@ public void LoadFrom(string fromDirectory) } catch (Exception e) { - log.Error(e, "Couldn't load {0}: {1}", file.VirtualPath, e.Message); + log.LogError(e, "Couldn't load {VirtualPath}: {Message}", file.VirtualPath, e.Message); } } diff --git a/MyApp/Markdown.Meta.cs b/MyApp/Markdown.Meta.cs new file mode 100644 index 0000000..bcfceaa --- /dev/null +++ b/MyApp/Markdown.Meta.cs @@ -0,0 +1,60 @@ +using ServiceStack.IO; +using ServiceStack.Text; + +namespace MyApp; + +public class MarkdownMeta +{ + public List Features { get; set; } = []; + + public async Task RenderToAsync(string metaDir, string baseUrl) + { + FileSystemVirtualFiles.RecreateDirectory(metaDir); + using var scope = JsConfig.With(new Config { ExcludeTypeInfo = true }); + var featureDocs = new Dictionary>(); + var allYears = new HashSet(); + var index = new Dictionary(); + foreach (var feature in Features.Safe()) + { + var allDocs = feature.GetAll() + .OrderByDescending(x => x.Date!.Value) + .ThenBy(x => x.Order) + .ThenBy(x => x.FileName) + .ToList(); + allDocs.ForEach(x => { + if (x.Url?.StartsWith("/") == true) + x.Url = baseUrl.CombineWith(x.Url); + if (x.Image?.StartsWith("/") == true) + x.Image = baseUrl.CombineWith(x.Image); + }); + featureDocs[feature.Id] = allDocs; + var featureYears = allDocs.Select(x => x.Date!.Value.Year).Distinct().OrderBy(x => x).ToList(); + featureYears.ForEach(x => allYears.Add(x)); + + index[feature.Id] = featureYears.Map(x => baseUrl.CombineWith($"/meta/{x}/{feature.Id}.json")); + foreach (var year in featureYears) + { + var yearDocs = allDocs + .Where(x => x.Date!.Value.Year == year) + .ToList(); + var yearDir = metaDir.CombineWith(year).AssertDir(); + var metaPath = yearDir.CombineWith($"{feature.Id}.json"); + await File.WriteAllTextAsync(metaPath, yearDocs.ToJson()); + } + } + await File.WriteAllTextAsync(metaDir.CombineWith("index.json"), JSON.stringify(index)); + + await File.WriteAllTextAsync(metaDir.CombineWith("all.json"), JSON.stringify(featureDocs)); + foreach (var year in allYears.OrderBy(x => x)) + { + var yearDocs = new Dictionary>(); + foreach (var entry in featureDocs) + { + yearDocs[entry.Key] = entry.Value + .Where(x => x.Date!.Value.Year == year) + .ToList(); + } + await File.WriteAllTextAsync(metaDir.CombineWith($"{year}/all.json"), JSON.stringify(yearDocs)); + } + } +} \ No newline at end of file diff --git a/MyApp/Markdown.Pages.cs b/MyApp/Markdown.Pages.cs index 9ab1f61..12f7f34 100644 --- a/MyApp/Markdown.Pages.cs +++ b/MyApp/Markdown.Pages.cs @@ -1,17 +1,16 @@ // run node postinstall.js to update to latest version using ServiceStack.IO; -using ServiceStack.Logging; namespace MyApp; -public class MarkdownPages : MarkdownPagesBase +public class MarkdownPages(ILogger log, IWebHostEnvironment env, IVirtualFiles fs) + : MarkdownPagesBase(log, env, fs) { public override string Id => "pages"; public virtual string? DefaultMenuIcon { get; set; } = ""; - - public MarkdownPages(ILogger log, IWebHostEnvironment env) : base(log,env) {} + List Pages { get; set; } = new(); public List GetVisiblePages(string? prefix=null, bool allDirectories=false) => prefix == null ? Pages.Where(x => IsVisible(x) && !x.Slug!.Contains('/')).OrderBy(x => x.Order).ThenBy(x => x.Path).ToList() @@ -31,12 +30,10 @@ public void LoadFrom(string fromDirectory) { Sidebars.Clear(); Pages.Clear(); - var fs = AssertVirtualFiles(); - var files = fs.GetDirectory(fromDirectory).GetAllFiles() + var files = VirtualFiles.GetDirectory(fromDirectory).GetAllFiles() .OrderBy(x => x.VirtualPath) .ToList(); - var log = LogManager.GetLogger(GetType()); - log.InfoFormat("Found {0} pages", files.Count); + log.LogInformation("Found {Count} pages", files.Count); var pipeline = CreatePipeline(); @@ -50,7 +47,7 @@ public void LoadFrom(string fromDirectory) if (doc == null) continue; - var relativePath = file.VirtualPath.Substring(fromDirectory.Length + 1); + var relativePath = file.VirtualPath[(fromDirectory.Length + 1)..]; if (relativePath.IndexOf('/') >= 0) { doc.Slug = relativePath.LastLeftPart('/') + '/' + doc.Slug; @@ -61,7 +58,7 @@ public void LoadFrom(string fromDirectory) else if (file.Name == "sidebar.json") { var virtualPath = file.VirtualPath.Substring(fromDirectory.Length); - var folder = virtualPath.Substring(0, virtualPath.Length - "sidebar.json".Length).Trim('/'); + var folder = virtualPath[..^"sidebar.json".Length].Trim('/'); var sidebarJson = file.ReadAllText(); var sidebar = sidebarJson.FromJson>(); @@ -76,12 +73,12 @@ public void LoadFrom(string fromDirectory) } catch (Exception e) { - log.Error(e, "Couldn't load {0}: {1}", file.VirtualPath, e.Message); + log.LogError(e, "Couldn't load {VirtualPath}: {Message}", file.VirtualPath, e.Message); } } if (Sidebars.Count > 0) { - log.Info($"Loaded {Sidebars.Count} sidebars: {Sidebars.Keys.Join(", ")}"); + log.LogInformation("Loaded {Count} sidebars: {Sidebars}", Sidebars.Count, Sidebars.Keys.Join(", ")); } } @@ -105,7 +102,7 @@ public virtual List GetSidebar(string folder, MarkdownMenu? defaul if (group == null) { menuItem = defaultMenu ?? new MarkdownMenu { - Children = new(), + Children = [], }; } else @@ -118,7 +115,7 @@ public virtual List GetSidebar(string folder, MarkdownMenu? defaul foreach (var page in allPages.Where(x => x.Group == group).OrderBy(x => x.Order)) { - menuItem.Children ??= new(); + menuItem.Children ??= []; var link = page.Slug!; if (link.EndsWith("/index")) { diff --git a/MyApp/Markdown.Videos.cs b/MyApp/Markdown.Videos.cs index ad6e43f..e910403 100644 --- a/MyApp/Markdown.Videos.cs +++ b/MyApp/Markdown.Videos.cs @@ -4,25 +4,24 @@ namespace MyApp; -public class MarkdownVideos : MarkdownPagesBase +public class MarkdownVideos(ILogger log, IWebHostEnvironment env, IVirtualFiles fs) + : MarkdownPagesBase(log, env, fs) { public override string Id => "videos"; - public MarkdownVideos(ILogger log, IWebHostEnvironment env) : base(log,env) {} public Dictionary> Groups { get; set; } = new(); public List GetVideos(string group) { return Groups.TryGetValue(group, out var docs) ? Fresh(docs.Where(IsVisible).OrderBy(x => x.Order).ThenBy(x => x.FileName).ToList()) - : new List(); + : []; } public void LoadFrom(string fromDirectory) { Groups.Clear(); - var fs = AssertVirtualFiles(); - var dirs = fs.GetDirectory(fromDirectory).GetDirectories().ToList(); - Log.LogInformation("Found {0} video directories", dirs.Count); + var dirs = VirtualFiles.GetDirectory(fromDirectory).GetDirectories().ToList(); + log.LogInformation("Found {Count} video directories", dirs.Count); var pipeline = CreatePipeline(); @@ -44,7 +43,7 @@ public void LoadFrom(string fromDirectory) } catch (Exception e) { - Log.LogError(e, "Couldn't load {0}: {1}", file.VirtualPath, e.Message); + log.LogError(e, "Couldn't load {VirtualPath}: {Message}", file.VirtualPath, e.Message); } } } diff --git a/MyApp/Markdown.WhatsNew.cs b/MyApp/Markdown.WhatsNew.cs new file mode 100644 index 0000000..11241ce --- /dev/null +++ b/MyApp/Markdown.WhatsNew.cs @@ -0,0 +1,72 @@ +// run node postinstall.js to update to latest version +using System.Globalization; +using ServiceStack.IO; + +namespace MyApp; + +public class MarkdownWhatsNew(ILogger log, IWebHostEnvironment env, IVirtualFiles fs) + : MarkdownPagesBase(log, env, fs) +{ + public override string Id => "whatsnew"; + public Dictionary> Features { get; set; } = new(); + + public List GetFeatures(string release) + { + return Features.TryGetValue(release, out var docs) + ? Fresh(docs.Where(IsVisible).OrderBy(x => x.Order).ThenBy(x => x.FileName).ToList()) + : []; + } + + public void LoadFrom(string fromDirectory) + { + Features.Clear(); + var dirs = VirtualFiles.GetDirectory(fromDirectory).GetDirectories().ToList(); + log.LogInformation("Found {Count} whatsnew directories", dirs.Count); + + var pipeline = CreatePipeline(); + + foreach (var dir in dirs) + { + var datePart = dir.Name.LeftPart('_'); + if (!DateTime.TryParseExact(datePart, "yyyy-MM-dd", CultureInfo.InvariantCulture, + DateTimeStyles.AdjustToUniversal, out var date)) + { + log.LogWarning("Could not parse date '{DatePart}', ignoring...", datePart); + continue; + } + + var releaseVersion = dir.Name.RightPart('_'); + var releaseDate = date; + + foreach (var file in dir.GetFiles().OrderBy(x => x.Name)) + { + try + { + var doc = Load(file.VirtualPath, pipeline); + if (doc == null) + continue; + + doc.Date = releaseDate; + doc.Group = releaseVersion; + + var releaseFeatures = Features.GetOrAdd(dir.Name, v => new List()); + releaseFeatures.Add(doc); + } + catch (Exception e) + { + log.LogError(e, "Couldn't load {VirtualPath}: {Message}", file.VirtualPath, e.Message); + } + } + } + } + + public override List GetAll() + { + var to = new List(); + foreach (var entry in Features) + { + to.AddRange(entry.Value.Where(IsVisible).Map(doc => ToMetaDoc(doc, x => x.Content = StripFrontmatter(doc.Content)))); + } + return to; + } +} \ No newline at end of file diff --git a/MyApp/MarkdownPagesBase.cs b/MyApp/MarkdownPagesBase.cs index 70aeb8e..d418ba8 100644 --- a/MyApp/MarkdownPagesBase.cs +++ b/MyApp/MarkdownPagesBase.cs @@ -88,22 +88,14 @@ public virtual void Update(MarkdownFileBase newDoc) public interface IMarkdownPages { string Id { get; } - IVirtualFiles VirtualFiles { get; set; } List GetAll(); } -public abstract class MarkdownPagesBase : IMarkdownPages where T : MarkdownFileBase + +public abstract class MarkdownPagesBase(ILogger log, IWebHostEnvironment env, IVirtualFiles fs) : IMarkdownPages + where T : MarkdownFileBase { public abstract string Id { get; } - protected ILogger Log { get; } - protected IWebHostEnvironment Environment { get; } - - public MarkdownPagesBase(ILogger log, IWebHostEnvironment env) - { - this.Log = log; - this.Environment = env; - } - - public IVirtualFiles VirtualFiles { get; set; } = default!; + public IVirtualFiles VirtualFiles => fs; public virtual MarkdownPipeline CreatePipeline() { @@ -133,7 +125,7 @@ public virtual List Fresh(List docs) public virtual T? Fresh(T? doc) { // Ignore reloading source .md if run in production or as AppTask - if (doc == null || !Environment.IsDevelopment() || AppTasks.IsRunAsAppTask()) + if (doc == null || !env.IsDevelopment() || AppTasks.IsRunAsAppTask()) return doc; var newDoc = Load(doc.Path); doc.Update(newDoc); @@ -175,7 +167,7 @@ public virtual T CreateMarkdownFile(string content, TextWriter writer, MarkdownP public virtual T? Load(string path, MarkdownPipeline? pipeline = null) { - var file = VirtualFiles.GetFile(path) + var file = fs.GetFile(path) ?? throw new FileNotFoundException(path.LastRightPart('/')); var content = file.ReadAllText(); @@ -197,7 +189,7 @@ public virtual T CreateMarkdownFile(string content, TextWriter writer, MarkdownP return doc; } - public virtual bool IsVisible(T doc) => Environment.IsDevelopment() || + public virtual bool IsVisible(T doc) => env.IsDevelopment() || !doc.Draft && (doc.Date == null || doc.Date.Value <= DateTime.UtcNow); public int WordsPerMin { get; set; } = 225; @@ -206,9 +198,6 @@ public virtual bool IsVisible(T doc) => Environment.IsDevelopment() || public virtual int LineCount(string str) => str.CountOccurrencesOf('\n'); public virtual int MinutesToRead(int? words) => (int)Math.Ceiling((words ?? 1) / (double)WordsPerMin); - protected IVirtualFiles AssertVirtualFiles() => - VirtualFiles ?? throw new NullReferenceException($"{nameof(VirtualFiles)} is not populated"); - public virtual List GetAll() => new(); public virtual string? StripFrontmatter(string? content) @@ -496,11 +485,10 @@ protected override void Write(HtmlRenderer renderer, CustomContainerInline obj) } } -public class CustomContainerRenderers : HtmlObjectRenderer +public class CustomContainerRenderers(ContainerExtensions extensions) : HtmlObjectRenderer { - public CustomContainerRenderers(ContainerExtensions extensions) => Extensions = extensions; - public ContainerExtensions Extensions { get; } - + public ContainerExtensions Extensions { get; } = extensions; + protected override void Write(HtmlRenderer renderer, CustomContainer obj) { var useRenderer = obj.Info != null && Extensions.BlockContainers.TryGetValue(obj.Info, out var customRenderer) @@ -510,11 +498,10 @@ protected override void Write(HtmlRenderer renderer, CustomContainer obj) } } -public class CustomContainerInlineRenderers : HtmlObjectRenderer +public class CustomContainerInlineRenderers(ContainerExtensions extensions) : HtmlObjectRenderer { - public CustomContainerInlineRenderers(ContainerExtensions extensions) => Extensions = extensions; - public ContainerExtensions Extensions { get; } - + public ContainerExtensions Extensions { get; } = extensions; + protected override void Write(HtmlRenderer renderer, CustomContainerInline obj) { var firstWord = obj.FirstChild is LiteralInline literalInline diff --git a/MyApp/MarkdownTagHelper.cs b/MyApp/MarkdownTagHelper.cs new file mode 100644 index 0000000..bc71037 --- /dev/null +++ b/MyApp/MarkdownTagHelper.cs @@ -0,0 +1,78 @@ +// run node postinstall.js to update to latest version +using Markdig; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace MyApp; + +[HtmlTargetElement("markdown", TagStructure = TagStructure.NormalOrSelfClosing)] +[HtmlTargetElement(Attributes = "markdown")] +public class MarkdownTagHelper : TagHelper +{ + public ModelExpression? Content { get; set; } + + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + if (output.TagName == "markdown") + { + output.TagName = null; + } + output.Attributes.RemoveAll("markdown"); + + var content = await GetContent(output); + + var pipeline = new MarkdownPipelineBuilder() + .UseAdvancedExtensions() + .Build(); + + var writer = new StringWriter(); + var cls = output.Attributes["class"]?.Value ?? "prose"; + await writer.WriteAsync($"
"); + var renderer = new Markdig.Renderers.HtmlRenderer(writer); + pipeline.Setup(renderer); + + var include = output.Attributes["include"]?.Value?.ToString() ?? ""; + if (!string.IsNullOrEmpty(include)) + { + if (include.EndsWith(".md")) + { + var markdown = HostContext.Resolve(); + // default relative path to _includes/ + include = include[0] != '/' + ? "_includes/" + include + : include.TrimStart('/'); + var prefix = include.LeftPart('/'); + var slug = include.LeftPart('.'); + var allIncludes = markdown.GetVisiblePages(prefix, allDirectories: true); + var doc = allIncludes.FirstOrDefault(x => x.Slug == slug); + if (doc != null) + { + renderer.WriteLine(doc.Preview!); + } + } + else + { + renderer.WriteLine($"Could not find: {include}"); + } + } + else + { + var document = Markdown.Parse(content ?? "", pipeline); + renderer.Render(document); + } + + await writer.WriteAsync("
"); + await writer.FlushAsync(); + var html = writer.ToString(); + + output.Content.SetHtmlContent(html ?? ""); + } + + private async Task GetContent(TagHelperOutput output) + { + if (Content == null) + return (await output.GetChildContentAsync()).GetContent(); + + return Content.Model?.ToString(); + } +} \ No newline at end of file diff --git a/MyApp/MyApp.csproj b/MyApp/MyApp.csproj index 32292f9..a0c83fe 100644 --- a/MyApp/MyApp.csproj +++ b/MyApp/MyApp.csproj @@ -21,6 +21,8 @@ + + @@ -30,6 +32,7 @@ + diff --git a/MyApp/Program.cs b/MyApp/Program.cs index 5f77614..672a7a1 100644 --- a/MyApp/Program.cs +++ b/MyApp/Program.cs @@ -3,12 +3,12 @@ using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.EntityFrameworkCore; using ServiceStack.Blazor; using MyApp.Components; using MyApp.Data; using MyApp.Components.Account; +using MyApp.ServiceInterface; var builder = WebApplication.CreateBuilder(args); @@ -29,6 +29,7 @@ options.DefaultScheme = IdentityConstants.ApplicationScheme; options.DefaultSignInScheme = IdentityConstants.ExternalScheme; }) + .AddBasicAuth() .AddIdentityCookies(); services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo("App_Data")); @@ -57,12 +58,23 @@ services.AddBlazorServerIdentityApiClient(baseUrl); services.AddLocalStorage(); +services.AddEndpointsApiExplorer(); +services.AddSwaggerGen(); + +services.AddServiceStack(typeof(MyServices).Assembly, c => { + c.AddSwagger(o => { + o.AddBasicAuth(); + }); +}); + var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseMigrationsEndPoint(); + app.UseSwagger(); + app.UseSwaggerUI(); } else { @@ -81,7 +93,9 @@ // Add additional endpoints required by the Identity /Account Razor components. app.MapAdditionalIdentityEndpoints(); -app.UseServiceStack(new AppHost()); +app.UseServiceStack(new AppHost(), options => { + options.MapEndpoints(); +}); BlazorConfig.Set(new() { diff --git a/MyApp/Properties/launchSettings.json b/MyApp/Properties/launchSettings.json index 9d9434c..884f48c 100644 --- a/MyApp/Properties/launchSettings.json +++ b/MyApp/Properties/launchSettings.json @@ -12,16 +12,14 @@ "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" + "ASPNETCORE_ENVIRONMENT": "Development" } }, "MyApp": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" + "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "https://localhost:5001/;http://localhost:5000/" },