feat: added a Markdown heading level transformer
All checks were successful
deploy / deploy (push) Successful in 12m9s

This commit is contained in:
D. Moonfire 2024-04-18 23:56:05 -05:00
parent b3ea80c937
commit edf6289dee
4 changed files with 202 additions and 5 deletions

View file

@ -0,0 +1,36 @@
namespace MfGames.Markdown.Exceptions;
public class MarkdownHeaderOutOfRangeException : Exception
{
public MarkdownHeaderOutOfRangeException(int oldLevel, int newLevel)
: this(FormatMessage(oldLevel, newLevel))
{
this.OldLevel = oldLevel;
this.NewLevel = newLevel;
}
public MarkdownHeaderOutOfRangeException()
: base() { }
public MarkdownHeaderOutOfRangeException(string? message)
: base(message) { }
public MarkdownHeaderOutOfRangeException(int oldLevel, int newLevel, Exception? innerException)
: base(FormatMessage(oldLevel, newLevel), innerException) { }
public MarkdownHeaderOutOfRangeException(string? message, Exception? innerException)
: base(message, innerException) { }
public int NewLevel { get; }
public int OldLevel { get; }
private static string FormatMessage(int oldLevel, int newLevel)
{
return string.Format(
"Cannot change the Markdown heading level from {0} to {1}.",
oldLevel,
newLevel
);
}
}

View file

@ -0,0 +1,72 @@
using Markdig.Renderers.Normalize;
using Markdig.Syntax;
using MfGames.Markdown.Exceptions;
namespace MfGames.Markdown;
/// <summary>
/// A transformer that goes through a Markdown document alters the level of the
/// headings either higher or lower..
/// </summary>
public class HeadingLevelTransformer
{
public HeadingLevelTransformer(int offset = 1)
{
this.Offset = offset;
}
/// <summary>
/// Gets or sets the value to offset the heading. A positive number would
/// increase the heading by that amount, a negative would reduce it. If
/// this would produce a negative heading, an exception is thrown. The
/// default is 1 to increase the heading level by one (H1 -> H2).
/// </summary>
public int Offset { get; } = 1;
/// <summary>
/// Parses the given input as Markdown, goes through and transforms all
/// the links, and then returns the modified Markdown.
/// </summary>
/// <param name="input">The input text as Markdown.</param>
/// <returns>Modified Markdown text.</returns>
public string? Transform(string? input)
{
// If we get a null or blank string, we return it.
if (string.IsNullOrWhiteSpace(input) || this.Offset == 0)
{
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 headings.
IEnumerable<HeadingBlock> blockList = document.Descendants<HeadingBlock>();
foreach (HeadingBlock block in blockList)
{
// Make sure we have sane values.
int oldLevel = block.Level;
int newLevel = oldLevel + this.Offset;
if (newLevel is < 1 or > 7)
{
throw new MarkdownHeaderOutOfRangeException(oldLevel, newLevel);
}
block.Level = newLevel;
}
// 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);
return writer.ToString();
}
}

View file

@ -10,8 +10,6 @@ namespace MfGames.Markdown;
/// </summary>
public class RewriteLinkTransformer
{
private string output;
public RewriteLinkTransformer() { }
public RewriteLinkTransformer(Action<LinkInline> onLink)
@ -59,8 +57,6 @@ public class RewriteLinkTransformer
renderer.Write(document);
this.output = writer.ToString();
return this.output;
return writer.ToString();
}
}

View file

@ -0,0 +1,93 @@
using MfGames.Markdown.Exceptions;
using MfGames.TestSetup;
using Xunit;
using Xunit.Abstractions;
namespace MfGames.Markdown.Tests;
/// <summary>
/// Tests the functionality of HeadingLevelTransformer.
/// </summary>
public class HeadingLevelTransformerTests : TestBase<TestContext>
{
public HeadingLevelTransformerTests(ITestOutputHelper output)
: base(output) { }
[Fact]
public void MakeShallow()
{
string input = string.Join("\n", "### Heading 3", "", "Paragraph", "");
string expected = string.Join("\n", "# Heading 3", "", "Paragraph", "");
HeadingLevelTransformer transformer = new(-2);
string? output = transformer.Transform(input);
Assert.Equal(expected, output);
}
[Fact]
public void ThreeLevels()
{
string input = string.Join(
"\n",
"# Heading 1",
"",
"## Heading 2",
"",
"### Heading 3",
"",
"Paragraph",
""
);
string expected = string.Join(
"\n",
"## Heading 1",
"",
"### Heading 2",
"",
"#### Heading 3",
"",
"Paragraph",
""
);
HeadingLevelTransformer transformer = new(1);
string? output = transformer.Transform(input);
Assert.Equal(expected, output);
}
[Fact]
public void TooDeep()
{
string input = string.Join("\n", "###### Heading 1", "", "Paragraph", "");
HeadingLevelTransformer transformer = new(5);
var exception = Assert.Throws<MarkdownHeaderOutOfRangeException>(
() => transformer.Transform(input)
);
Assert.Equal(6, exception.OldLevel);
Assert.Equal(11, exception.NewLevel);
}
[Fact]
public void TooShallow()
{
string input = string.Join("\n", "# Heading 1", "", "Paragraph", "");
HeadingLevelTransformer transformer = new(-1);
var exception = Assert.Throws<MarkdownHeaderOutOfRangeException>(
() => transformer.Transform(input)
);
Assert.Equal(1, exception.OldLevel);
Assert.Equal(0, exception.NewLevel);
}
}