feat: added a Markdown heading level transformer
All checks were successful
deploy / deploy (push) Successful in 12m9s
All checks were successful
deploy / deploy (push) Successful in 12m9s
This commit is contained in:
parent
b3ea80c937
commit
edf6289dee
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
72
src/MfGames.Markdown/HeadingLevelTransformer.cs
Normal file
72
src/MfGames.Markdown/HeadingLevelTransformer.cs
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
93
tests/MfGames.Markdown.Tests/HeadingLevelTransformerTests.cs
Normal file
93
tests/MfGames.Markdown.Tests/HeadingLevelTransformerTests.cs
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue