Skip to content

Commit

Permalink
Implemented Table of Contents (only on newly saved articles)
Browse files Browse the repository at this point in the history
  • Loading branch information
miawinter98 committed May 2, 2024
1 parent c98293c commit 51ace95
Show file tree
Hide file tree
Showing 12 changed files with 902 additions and 7 deletions.
36 changes: 35 additions & 1 deletion Wave/Components/ArticleComponent.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
@using Humanizer
@using Wave.Utilities

@inject NavigationManager Navigation
@inject IStringLocalizer<Pages.ArticleView> Localizer

<SectionContent SectionName="GlobalHeader">
<header class="bg-secondary text-secondary-content border-b-2 border-current py-6 px-4 md:px-12">
<header class="bg-secondary text-secondary-content border-b-2 border-current py-6 px-4 md:px-12" data-nosnippet>
<h1 class="text-3xl lg:text-5xl font-light">
@Article.Title
</h1>
Expand Down Expand Up @@ -46,6 +47,39 @@
</header>
</SectionContent>

@if (Article.Headings.Count > 0) {
<section class="mb-3 p-2 bg-base-200 rounded-box w-80 float-start mr-2 mb-2" data-nosnippet>
<h2 class="text-xl font-bold mb-3">@Localizer["TableOfContents"]</h2>
<ul class="menu p-0 [&_li>*]:rounded-none">
@{
int level = 1;
foreach (var heading in Article.Headings.OrderBy(h => h.Order)) {
int headingLevel = heading.Order % 10;

while (headingLevel < level) {
level--;
@(new MarkupString("</ul></li>"))
}

while (headingLevel > level) {
level++;
@(new MarkupString("<li><ul>"))
}

<li>
<a href="/@Navigation.ToBaseRelativePath(Navigation.Uri)#@heading.Anchor">@heading.Label</a>
</li>
}

while (level > 1) {
level--;
@(new MarkupString("<li><ul>"))
}
}
</ul>
</section>
}

<article class="mb-6">
<div class="prose prose-neutral max-w-none hyphens-auto text-justify">
@Content
Expand Down
2 changes: 1 addition & 1 deletion Wave/Components/Pages/ArticleView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
}
</AdditionalContent>
</ArticleComponent>


<div class="flex gap-2 mt-3 flex-wrap">
@if (article.AllowedToEdit(HttpContext.User)) {
Expand Down
3 changes: 2 additions & 1 deletion Wave/Components/Pages/Partials/ArticleEditorPartial.razor
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,6 @@
}

Article.LastModified = DateTimeOffset.UtcNow;
Article.UpdateBody();

await using var context = await ContextFactory.CreateDbContextAsync();

Expand All @@ -213,6 +212,8 @@
.Where(ac => ac.Article.Id == Article.Id).LoadAsync();

context.Update(Article);
context.RemoveRange(Article.Headings);
Article.UpdateBody();

var existingImages = await context.Set<Article>()
.IgnoreQueryFilters().Where(a => a.Id == Article.Id)
Expand Down
1 change: 1 addition & 0 deletions Wave/Data/ApplicationDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options
.IsRequired().OnDelete(DeleteBehavior.Cascade);
article.HasOne(a => a.Reviewer).WithMany()
.IsRequired(false).OnDelete(DeleteBehavior.SetNull);
article.OwnsMany(a => a.Headings);
article.Property(a => a.CreationDate)
.IsRequired().HasDefaultValueSql("now()")
Expand Down
31 changes: 29 additions & 2 deletions Wave/Data/Article.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,20 @@ public enum ArticleStatus {
Published = 2
}

public class ArticleHeading {
[Key]
public int Id { get; set; }
public required int Order { get; set; }
[MaxLength(128)]
public required string Label { get; set; }
[MaxLength(256)]
public required string Anchor { get; set; }
}

// TODO:: Add tags for MVP ?
// TODO:: Archive System (Notice / Redirect to new content?) (Deprecation date?)

public class Article : ISoftDelete {
public partial class Article : ISoftDelete {
[Key]
public Guid Id { get; set; }
public bool IsDeleted { get; set; }
Expand Down Expand Up @@ -46,6 +56,7 @@ public class Article : ISoftDelete {

public IList<Category> Categories { get; } = [];
public IList<ArticleImage> Images { get; } = [];
public IList<ArticleHeading> Headings { get; } = [];

public void UpdateSlug(string? potentialNewSlug = null) {
if (!string.IsNullOrWhiteSpace(potentialNewSlug) && Uri.IsWellFormedUriString(potentialNewSlug, UriKind.Relative)) {
Expand Down Expand Up @@ -74,11 +85,27 @@ public class Article : ISoftDelete {
}

Slug = slug[..Math.Min(slug.Length, 64 - escapeTrimOvershoot)];
if (Slug.EndsWith("%")) Slug = Slug[..^1];
}

public void UpdateBody() {
BodyHtml = MarkdownUtilities.Parse(Body).Trim();
BodyPlain = HtmlUtilities.GetPlainText(BodyHtml).Trim();

Headings.Clear();
var headings = HeadingsRegex().Matches(BodyHtml);
foreach(Match match in headings) {
string label = match.Groups["Label"].Value;
string anchor = match.Groups["Anchor"].Value;

var h = new ArticleHeading {
Order = match.Index * 10 + int.Parse(match.Groups["Level"].Value),
Label = label[..Math.Min(128, label.Length)],
Anchor = anchor[..Math.Min(256, anchor.Length)]
};
Headings.Add(h);
}
}

[GeneratedRegex("<h(?<Level>[1-6]).*id=\"(?<Anchor>.+)\".*>(?<Label>.+)</h[1-6]>")]
private static partial Regex HeadingsRegex();
}
Loading

0 comments on commit 51ace95

Please sign in to comment.