diff --git a/src/MfGames.Markdown/RewriteLinkTransformer.cs b/src/MfGames.Markdown/RewriteLinkTransformer.cs
new file mode 100644
index 0000000..6e15044
--- /dev/null
+++ b/src/MfGames.Markdown/RewriteLinkTransformer.cs
@@ -0,0 +1,66 @@
+using Markdig.Renderers.Normalize;
+using Markdig.Syntax;
+using Markdig.Syntax.Inlines;
+
+namespace MfGames.Markdown;
+
+///
+/// A transformer that goes through a Markdown document and visits each link
+/// so it can be rewritten in a different format.
+///
+public class RewriteLinkTransformer
+{
+ private string output;
+
+ public RewriteLinkTransformer() { }
+
+ public RewriteLinkTransformer(Action onLink)
+ {
+ this.OnLink = onLink;
+ }
+
+ ///
+ /// A callback to trigger for every link found in the document.
+ ///
+ public Action? OnLink { get; set; }
+
+ ///
+ /// Parses the given input as Markdown, goes through and transforms all
+ /// the links, and then returns the modified Markdown.
+ ///
+ /// The input text as Markdown.
+ /// Modified Markdown text.
+ public string? Transform(string? input)
+ {
+ // If we get a null or blank string, we return it.
+ if (string.IsNullOrWhiteSpace(input) || this.OnLink == null)
+ {
+ return input;
+ }
+
+ // Parse the Markdown into an abstract syntax tree (AST). We need the
+ // trivia because we want to round-trip as much as possible and it
+ // contains things like extra whitespace or indention.
+ MarkdownDocument document = Markdig.Markdown.Parse(input, true);
+
+ // Go through all the links.
+ IEnumerable linkList = document.Descendants();
+
+ foreach (LinkInline oldLink in linkList)
+ {
+ this.OnLink.Invoke(oldLink);
+ }
+
+ // Convert the AST back into Markdown and return the results. The
+ // RoundtripRenderer doesn't work here, but NormalizeRenderer seems to
+ // allow us to modify the link above and get the results.
+ var writer = new StringWriter();
+ var renderer = new NormalizeRenderer(writer);
+
+ renderer.Write(document);
+
+ this.output = writer.ToString();
+
+ return this.output;
+ }
+}
diff --git a/src/MfGames.Nitride.Markdown/MfGames.Nitride.Markdown.csproj b/src/MfGames.Nitride.Markdown/MfGames.Nitride.Markdown.csproj
index 59be0fe..970cf0f 100644
--- a/src/MfGames.Nitride.Markdown/MfGames.Nitride.Markdown.csproj
+++ b/src/MfGames.Nitride.Markdown/MfGames.Nitride.Markdown.csproj
@@ -17,6 +17,7 @@
+
diff --git a/src/MfGames.Nitride.Markdown/ParseMarkdownHeadingOne.cs b/src/MfGames.Nitride.Markdown/ParseMarkdownHeadingOne.cs
index 73d6a14..5139511 100644
--- a/src/MfGames.Nitride.Markdown/ParseMarkdownHeadingOne.cs
+++ b/src/MfGames.Nitride.Markdown/ParseMarkdownHeadingOne.cs
@@ -41,7 +41,9 @@ public partial class ParseMarkdownHeadingOne : IOperation
private string? GetText(MarkdownObject? block)
{
if (block == null)
+ {
return null;
+ }
var writer = new StringWriter();
var renderer = new RoundtripRenderer(writer);
diff --git a/src/MfGames.Nitride.Markdown/RewriteLinkToIndexPath.cs b/src/MfGames.Nitride.Markdown/RewriteLinkToIndexPath.cs
new file mode 100644
index 0000000..2ab823f
--- /dev/null
+++ b/src/MfGames.Nitride.Markdown/RewriteLinkToIndexPath.cs
@@ -0,0 +1,55 @@
+using System.Text.RegularExpressions;
+using Markdig.Syntax.Inlines;
+using MfGames.Gallium;
+
+namespace MfGames.Nitride.Markdown;
+
+///
+/// An operation that rewrites link paths to be directory indexes.
+///
+public class RewriteLinkToIndexPath : IOperation
+{
+ private static readonly Regex IndexRegex = new(@"/index\.(markdown|md|html|htm)$");
+
+ private readonly RewriteMarkdownLink rewrite;
+
+ public RewriteLinkToIndexPath(RewriteMarkdownLink rewrite)
+ {
+ this.rewrite = rewrite;
+ this.rewrite.OnLinkCallback = this.OnLinkCallback;
+ }
+
+ ///
+ public IEnumerable Run(
+ IEnumerable input,
+ CancellationToken cancellationToken = default
+ )
+ {
+ return this.rewrite.Run(input, cancellationToken);
+ }
+
+ private void OnLinkCallback(LinkInline link)
+ {
+ // Ignore blank links (they could happen).
+ string? url = link.Url;
+
+ if (string.IsNullOrEmpty(url))
+ {
+ return;
+ }
+
+ // We only want links that start with a period or are relative to the
+ // current directory. Ideally, we don't want links that are remote URLs.
+ // This is a simplistic version.
+ if (url.Contains("://"))
+ {
+ return;
+ }
+
+ // Check to see what the path ends with something we can map.
+ if (IndexRegex.IsMatch(url))
+ {
+ link.Url = IndexRegex.Replace(url, "/");
+ }
+ }
+}
diff --git a/src/MfGames.Nitride.Markdown/RewriteMarkdownLink.cs b/src/MfGames.Nitride.Markdown/RewriteMarkdownLink.cs
new file mode 100644
index 0000000..86ed0b3
--- /dev/null
+++ b/src/MfGames.Nitride.Markdown/RewriteMarkdownLink.cs
@@ -0,0 +1,49 @@
+using Markdig.Syntax.Inlines;
+using MfGames.Gallium;
+using MfGames.Markdown;
+using MfGames.Nitride.Contents;
+using MfGames.Nitride.Generators;
+
+namespace MfGames.Nitride.Markdown;
+
+///
+/// An operation that turns rewrites Markdown links with a variable callback.
+///
+[WithProperties]
+public partial class RewriteMarkdownLink : IOperation
+{
+ private readonly RewriteLinkTransformer transformer;
+
+ public RewriteMarkdownLink()
+ {
+ this.transformer = new RewriteLinkTransformer();
+ }
+
+ public Action? OnLinkCallback
+ {
+ get => this.transformer.OnLink;
+ set => this.transformer.OnLink = value;
+ }
+
+ ///
+ public IEnumerable Run(
+ IEnumerable input,
+ CancellationToken cancellationToken = default
+ )
+ {
+ return input.SelectManyEntity(x => x.Select(this.Transform));
+ }
+
+ ///
+ /// This turns all links that start with a link into a single link while
+ /// removing all trailing links within the line. This is to simplify the
+ /// rendering of the link on page.
+ ///
+ private Entity Transform(Entity entity)
+ {
+ string input = entity.GetTextContentString()!;
+ string output = this.transformer.Transform(input)!;
+
+ return entity.SetTextContent(output);
+ }
+}
diff --git a/tests/MfGames.Markdown.Tests/RewriteLinkTransformer.cs b/tests/MfGames.Markdown.Tests/RewriteLinkTransformer.cs
new file mode 100644
index 0000000..8c9de69
--- /dev/null
+++ b/tests/MfGames.Markdown.Tests/RewriteLinkTransformer.cs
@@ -0,0 +1,79 @@
+using MfGames.TestSetup;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace MfGames.Markdown.Tests;
+
+///
+/// Tests the functionality of RewriteLinkTransformer.
+///
+public class RewriteLinkTransformerTests : TestBase
+{
+ public RewriteLinkTransformerTests(ITestOutputHelper output)
+ : base(output) { }
+
+ [Fact]
+ public void ComplexExample()
+ {
+ string input = string.Join(
+ "\n",
+ "# A [Heading](/heading/index.md)",
+ "",
+ "That includes many [not-changing](https://example.org) examples",
+ "and ones that [will change](/gary/index.md).",
+ "",
+ "No one knows how this [won't change](/favicon.ico).",
+ ""
+ );
+
+ string expected = string.Join(
+ "\n",
+ "# A [Heading](/heading/)",
+ "",
+ "That includes many [not-changing](https://example.org) examples",
+ "and ones that [will change](/gary/).",
+ "",
+ "",
+ "No one knows how this [won't change](/favicon.ico).",
+ ""
+ );
+
+ RewriteLinkTransformer transformer =
+ new((link) => link.Url = link.Url?.Replace("/index.md", "/"));
+
+ string? output = transformer.Transform(input);
+
+ Assert.Equal(expected, output);
+ }
+
+ [Fact]
+ public void HandleLink()
+ {
+ const string Input = "[Link Title](/link/index.md)";
+
+ RewriteLinkTransformer transformer =
+ new((link) => link.Url = link.Url?.Replace("/index.md", "/"));
+
+ string? output = transformer.Transform(Input);
+
+ Assert.Equal("[Link Title](/link/)", output);
+ }
+
+ [Fact]
+ public void HandleNulls()
+ {
+ RewriteLinkTransformer transformer = new();
+
+ Assert.Null(transformer.Transform(null));
+ }
+
+ [Fact]
+ public void HandleText()
+ {
+ const string Input = "Content";
+ RewriteLinkTransformer transformer = new();
+ string? output = transformer.Transform(Input);
+
+ Assert.Equal("Content", output);
+ }
+}
diff --git a/tests/MfGames.Nitride.Markdown.Tests/RewriteLinkToIndexPathTests.cs b/tests/MfGames.Nitride.Markdown.Tests/RewriteLinkToIndexPathTests.cs
new file mode 100644
index 0000000..f1ca587
--- /dev/null
+++ b/tests/MfGames.Nitride.Markdown.Tests/RewriteLinkToIndexPathTests.cs
@@ -0,0 +1,103 @@
+using System.Collections.Generic;
+using System.Linq;
+using MfGames.Gallium;
+using MfGames.Nitride.Contents;
+using MfGames.TestSetup;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace MfGames.Nitride.Markdown.Tests;
+
+///
+/// Tests the functionality of the RewriteLinkToIndexPath operation.
+///
+public class RewriteLinkToIndexPathTests : TestBase
+{
+ ///
+ public RewriteLinkToIndexPathTests(ITestOutputHelper output)
+ : base(output) { }
+
+ [Fact]
+ public void NoRewriteSimpleIndexPhp()
+ {
+ using MarkdownTestContext context = this.CreateContext();
+
+ List input =
+ new()
+ {
+ new Entity().Set(IsMarkdown.Instance).SetTextContent("[Link Title](/index.php)"),
+ };
+
+ var op = context.Resolve();
+
+ IEnumerable output = op.Run(input);
+ Entity first = output.First();
+ string content = first.GetTextContentString()!.Trim();
+
+ Assert.Equal("[Link Title](/index.php)", content);
+ }
+
+ [Fact]
+ public void RewritePeriodSlashRelativePath()
+ {
+ using MarkdownTestContext context = this.CreateContext();
+
+ List input =
+ new()
+ {
+ new Entity()
+ .Set(IsMarkdown.Instance)
+ .SetTextContent("[Link Title](./bob/index.md)"),
+ };
+
+ var op = context.Resolve();
+
+ IEnumerable output = op.Run(input);
+ Entity first = output.First();
+ string content = first.GetTextContentString()!.Trim();
+
+ Assert.Equal("[Link Title](./bob/)", content);
+ }
+
+ [Fact]
+ public void RewriteRemoteIndex()
+ {
+ using MarkdownTestContext context = this.CreateContext();
+
+ List input =
+ new()
+ {
+ new Entity()
+ .Set(IsMarkdown.Instance)
+ .SetTextContent("[Link Title](https://example.org/index.md)"),
+ };
+
+ var op = context.Resolve();
+
+ IEnumerable output = op.Run(input);
+ Entity first = output.First();
+ string content = first.GetTextContentString()!.Trim();
+
+ Assert.Equal("[Link Title](https://example.org/index.md)", content);
+ }
+
+ [Fact]
+ public void RewriteSimpleIndexMarkdown()
+ {
+ using MarkdownTestContext context = this.CreateContext();
+
+ List input =
+ new()
+ {
+ new Entity().Set(IsMarkdown.Instance).SetTextContent("[Link Title](/index.md)"),
+ };
+
+ var op = context.Resolve();
+
+ IEnumerable output = op.Run(input);
+ Entity first = output.First();
+ string content = first.GetTextContentString()!.Trim();
+
+ Assert.Equal("[Link Title](/)", content);
+ }
+}