Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ public static MarkdownPipelineBuilder UseLazyLoadImages(this MarkdownPipelineBui
pipeline.Extensions.Add(new LazyLoadImageExtension());
return pipeline;
}

public static MarkdownPipelineBuilder UseExternalLinks(this MarkdownPipelineBuilder pipeline)
{
pipeline.Extensions.Add(new ExternalLinkExtension());
return pipeline;
}

public static MarkdownPipelineBuilder UseResponsiveTables(this MarkdownPipelineBuilder pipeline)
{
pipeline.Extensions.Add(new ResponsiveTableExtension());
return pipeline;
}
}

internal sealed class CopyCodeBlockToClipboardExtension : IMarkdownExtension
Expand Down Expand Up @@ -55,6 +67,10 @@ internal sealed class CustomCodeBlockRenderer : CodeBlockRenderer
protected override void Write(HtmlRenderer renderer, CodeBlock obj)
{
renderer.Write("""<div class="position-relative">""");
if (obj is FencedCodeBlock { Info.Length: > 0 } fenced)
{
renderer.Write($"""<span class="badge bg-secondary position-absolute top-0 start-0 m-2 code-lang-badge">{fenced.Info}</span>""");
}
renderer.Write("""
<button class="btn btn-sm position-absolute top-0 end-0 m-2 border border-primary text-primary copy-btn"
type="button"
Expand Down
40 changes: 40 additions & 0 deletions src/LinkDotNet.Blog.Web/Features/ExternalLinkExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System;
using Markdig;
using Markdig.Renderers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;

namespace LinkDotNet.Blog.Web.Features;

internal sealed class ExternalLinkExtension : IMarkdownExtension
{
public void Setup(MarkdownPipelineBuilder pipeline)
{
pipeline.DocumentProcessed -= MarkExternalLinks;
pipeline.DocumentProcessed += MarkExternalLinks;
}

public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
}

private static void MarkExternalLinks(MarkdownDocument document)
{
foreach (var link in document.Descendants<LinkInline>())
{
if (link.IsImage
|| !Uri.TryCreate(link.Url, UriKind.Absolute, out var uri)
|| (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps))
{
continue;
}

var attributes = link.GetAttributes();
attributes.AddPropertyIfNotExist("target", "_blank");
attributes.AddPropertyIfNotExist("rel", "noopener noreferrer");
attributes.AddClass("external-link");
link.AppendChild(new HtmlInline("""<i class="bi bi-box-arrow-up-right ms-1" aria-hidden="true"></i>"""));
}
}
}
4 changes: 4 additions & 0 deletions src/LinkDotNet.Blog.Web/Features/MarkdownConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public static class MarkdownConverter
.UseCopyCodeBlock()
.UseCallouts()
.UseLazyLoadImages()
.UseExternalLinks()
.UseResponsiveTables()
.Build();

public static MarkupString ToMarkupString(string markdown)
Expand Down Expand Up @@ -123,6 +125,8 @@ static void ProcessInlineDelegate(Inline? inline, StringBuilder stringBuilder)
case HtmlEntityInline htmlEntity:
stringBuilder.Append(htmlEntity.Transcoded);
break;
case HtmlInline:
break;
default:
stringBuilder.Append(current);
break;
Expand Down
41 changes: 41 additions & 0 deletions src/LinkDotNet.Blog.Web/Features/ResponsiveTableExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Markdig;
using Markdig.Extensions.Tables;
using Markdig.Renderers;
using Markdig.Renderers.Html;

namespace LinkDotNet.Blog.Web.Features;

internal sealed class ResponsiveTableExtension : IMarkdownExtension
{
public void Setup(MarkdownPipelineBuilder pipeline)
{
}

public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is not HtmlRenderer htmlRenderer)
{
return;
}

var originalTableRenderer = htmlRenderer.ObjectRenderers.FindExact<HtmlTableRenderer>();
if (originalTableRenderer is null)
{
return;
}

htmlRenderer.ObjectRenderers.Remove(originalTableRenderer);
htmlRenderer.ObjectRenderers.Add(new ResponsiveHtmlTableRenderer());
}
}

internal sealed class ResponsiveHtmlTableRenderer : HtmlTableRenderer
{
protected override void Write(HtmlRenderer renderer, Table table)
{
renderer.Write("""<div class="table-container">""");
base.Write(renderer, table);
renderer.EnsureLine();
renderer.Write("</div>");
}
}
20 changes: 20 additions & 0 deletions src/LinkDotNet.Blog.Web/wwwroot/css/basic.css
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ code {
}
}

.code-lang-badge {
font-size: 0.7rem;
opacity: 0.85;
}

/* Template defined css */
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
Expand Down Expand Up @@ -666,6 +671,21 @@ code {
object-fit: cover;
}

.blogpost-content .contains-task-list {
list-style: none;
padding-left: 0.5rem;
}

.blogpost-content .task-list-item input[type="checkbox"] {
accent-color: var(--bs-primary);
margin-right: 0.4rem;
}

.blogpost-content .task-list-item:has(input:checked) {
color: var(--bs-secondary-color);
text-decoration: line-through;
}

@media only screen and (max-width: 700px) {
.blog-outer-box .blog-container {
width: 90%;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ public void ShouldNotAddHeadingAnchorToTableOfContents()
toc.ShouldHaveSingleItem().Text.ShouldBe("Hello World");
}

[Fact]
public void ShouldGenerateTocTextForHeadingWithExternalLink()
{
var toc = MarkdownConverter.GenerateToc("## [Markdig](https://github.com/xoofx/markdig)");

toc.ShouldHaveSingleItem().Text.ShouldBe("Markdig");
}

[Fact]
public void ShouldReturnEmptyMarkupForEmptyContentWithHeadingAnchors()
{
Expand All @@ -99,4 +107,87 @@ public void ShouldNotAddLazyLoadingToLinks()

html.ShouldNotContain("loading=");
}

[Fact]
public void ShouldMarkAbsoluteLinksAsExternal()
{
var html = MarkdownConverter.ToMarkupString("[link](https://localhost)").Value;

html.ShouldContain("target=\"_blank\"");
html.ShouldContain("rel=\"noopener noreferrer\"");
html.ShouldContain("class=\"external-link\"");
html.ShouldContain("""<i class="bi bi-box-arrow-up-right ms-1" aria-hidden="true"></i>""");
}

[Fact]
public void ShouldNotMarkRelativeLinksAsExternal()
{
var html = MarkdownConverter.ToMarkupString("[post](/blogPost/1)").Value;

html.ShouldNotContain("external-link");
html.ShouldNotContain("target=");
}

[Fact]
public void ShouldNotMarkFragmentLinksAsExternal()
{
var html = MarkdownConverter.ToMarkupString("[jump](#section)").Value;

html.ShouldNotContain("external-link");
html.ShouldNotContain("target=");
}

[Fact]
public void ShouldNotMarkExternalImagesAsExternalLinks()
{
var html = MarkdownConverter.ToMarkupString("![alt](https://localhost/image.png)").Value;

html.ShouldNotContain("external-link");
html.ShouldNotContain("bi-box-arrow-up-right");
}

[Fact]
public void ShouldRenderLanguageBadgeForFencedCodeBlock()
{
var html = MarkdownConverter.ToMarkupString("```csharp\nvar x = 1;\n```").Value;

html.ShouldContain("""<span class="badge bg-secondary position-absolute top-0 start-0 m-2 code-lang-badge">csharp</span>""");
}

[Fact]
public void ShouldNotRenderLanguageBadgeWithoutLanguage()
{
var html = MarkdownConverter.ToMarkupString("```\nvar x = 1;\n```").Value;

html.ShouldNotContain("code-lang-badge");
}

[Fact]
public void ShouldNotRenderLanguageBadgeForIndentedCodeBlock()
{
var html = MarkdownConverter.ToMarkupString(" var x = 1;").Value;

html.ShouldNotContain("code-lang-badge");
}

[Fact]
public void ShouldRenderTaskListCheckboxes()
{
var html = MarkdownConverter.ToMarkupString("- [ ] Todo item\n- [x] Done item\n- Regular item").Value;

html.ShouldContain("contains-task-list");
html.ShouldContain("""<li class="task-list-item"><input disabled="disabled" type="checkbox" /> Todo item</li>""");
html.ShouldContain("""<li class="task-list-item"><input disabled="disabled" type="checkbox" checked="checked" /> Done item</li>""");
html.ShouldContain("<li>Regular item</li>");
}

[Fact]
public void ShouldWrapTablesInResponsiveContainer()
{
var html = MarkdownConverter.ToMarkupString("| A | B |\n|---|---|\n| 1 | 2 |").Value;

html.ShouldContain("""<div class="table-container">""");
html.ShouldContain("""<table class="table">""");
html.ShouldContain("</table>\n</div>");
}
}