Compare commits

...

3 commits

Author SHA1 Message Date
D. Moonfire edf6289dee feat: added a Markdown heading level transformer
All checks were successful
deploy / deploy (push) Successful in 12m9s
2024-04-18 23:56:05 -05:00
D. Moonfire b3ea80c937 refactor: renaming ParseMarkdownHeadingOne to ExtractMarkdownHeadingOne 2024-04-18 23:29:01 -05:00
D. Moonfire 31d545321f chore: updating locks 2024-04-18 22:42:47 -05:00
8 changed files with 235 additions and 38 deletions

View file

@ -322,11 +322,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1710111843,
"narHash": "sha256-FbxiA5A8YVdVH8wE/JxjRQKvRGQj2OvoiQmd++xcvvE=",
"lastModified": 1710169003,
"narHash": "sha256-n5Xbw60+faIZsuhhwhRuY8f6p1Y+3DojFb+VR2vcfj8=",
"ref": "refs/heads/main",
"rev": "4631ae09b2b8dedbd8e454aef33658c2dc58040c",
"revCount": 10,
"rev": "dfcc08e78f3b68076d780b38cd29f40b0288ae53",
"revCount": 12,
"type": "git",
"url": "https://src.mfgames.com/mfgames-cli/mfgames-conventional-commit.git"
},
@ -362,11 +362,11 @@
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1709861835,
"narHash": "sha256-vXuOnCnpm3ii728s9KyNrAwH0wZ6RvQwmN53H55GtdE=",
"lastModified": 1713495963,
"narHash": "sha256-UDIqJjYr4/cnFZBVfJM6oxQADfNIoaXFcMTgbDEUIU4=",
"ref": "refs/heads/main",
"rev": "6ba1a8e9755ae03287457d5fe9bb3565f7421c4b",
"revCount": 17,
"rev": "afea19adb0d64d334c7c42cacea41b361574891e",
"revCount": 25,
"type": "git",
"url": "https://src.mfgames.com/nixos-contrib/mfgames-project-setup-flake.git"
},
@ -874,12 +874,12 @@
},
"nixpkgs_4": {
"locked": {
"lastModified": 1710021367,
"narHash": "sha256-FuMVdWqXMT38u1lcySYyv93A7B8wU0EGzUr4t4jQu8g=",
"rev": "b94a96839afcc56de3551aa7472b8d9a3e77e05d",
"revCount": 556575,
"lastModified": 1713145326,
"narHash": "sha256-m7+IWM6mkWOg22EC5kRUFCycXsXLSU7hWmHdmBfmC3s=",
"rev": "53a2c32bc66f5ae41a28d7a9a49d321172af621e",
"revCount": 557721,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2311.556575%2Brev-b94a96839afcc56de3551aa7472b8d9a3e77e05d/018e275e-a070-780b-ba43-d86b528adee8/source.tar.gz"
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.2311.557721%2Brev-53a2c32bc66f5ae41a28d7a9a49d321172af621e/018ee413-6e9c-72d4-be11-b9bef24c16bc/source.tar.gz"
},
"original": {
"type": "tarball",

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

@ -8,16 +8,16 @@ using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Markdown;
/// <summary>
/// An operation that parses the Markdown and converts the first heading one
/// An operation that parses the Markdown and pulls the first heading one
/// into a model to include in the component. The rest of the Markdown is put
/// back as the text content of the entity.
/// </summary>
[WithProperties]
public partial class ParseMarkdownHeadingOne : IOperation
public partial class ExtractMarkdownHeadingOne : IOperation
{
private readonly IValidator<ParseMarkdownHeadingOne> validator;
private readonly IValidator<ExtractMarkdownHeadingOne> validator;
public ParseMarkdownHeadingOne(IValidator<ParseMarkdownHeadingOne> validator)
public ExtractMarkdownHeadingOne(IValidator<ExtractMarkdownHeadingOne> validator)
{
this.validator = validator;
}

View file

@ -2,9 +2,9 @@ using FluentValidation;
namespace MfGames.Nitride.Markdown.Validators;
public class ParseMarkdownHeadingOneValidator : AbstractValidator<ParseMarkdownHeadingOne>
public class ExtractMarkdownHeadingOneValidator : AbstractValidator<ExtractMarkdownHeadingOne>
{
public ParseMarkdownHeadingOneValidator()
public ExtractMarkdownHeadingOneValidator()
{
this.RuleFor(x => x.AddModelCallback).NotNull();
}

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);
}
}

View file

@ -11,9 +11,9 @@ namespace MfGames.Nitride.Markdown.Tests;
/// <summary>
/// Tests the functionality of the ParseMarkdownHeadingOne operation.
/// </summary>
public class ParseMarkdownHeadingOneTests : TestBase<MarkdownTestContext>
public class ExtractMarkdownHeadingOneTests : TestBase<MarkdownTestContext>
{
public ParseMarkdownHeadingOneTests(ITestOutputHelper output)
public ExtractMarkdownHeadingOneTests(ITestOutputHelper output)
: base(output) { }
[Fact]
@ -29,8 +29,8 @@ public class ParseMarkdownHeadingOneTests : TestBase<MarkdownTestContext>
.SetTextContent("# Heading [One](/test)\n\nContent\nsecond\n\nline"),
};
ParseMarkdownHeadingOne op = context
.Resolve<ParseMarkdownHeadingOne>()
ExtractMarkdownHeadingOne op = context
.Resolve<ExtractMarkdownHeadingOne>()
.WithAddModelCallback((entity, heading) => entity.Set(heading));
IEnumerable<Entity> output = op.Run(input);
@ -49,8 +49,8 @@ public class ParseMarkdownHeadingOneTests : TestBase<MarkdownTestContext>
List<Entity> input =
new() { new Entity().Set(IsMarkdown.Instance).SetTextContent("#\nContent"), };
ParseMarkdownHeadingOne op = context
.Resolve<ParseMarkdownHeadingOne>()
ExtractMarkdownHeadingOne op = context
.Resolve<ExtractMarkdownHeadingOne>()
.WithAddModelCallback((entity, heading) => entity.Set(heading));
IEnumerable<Entity> output = op.Run(input);
@ -72,8 +72,8 @@ public class ParseMarkdownHeadingOneTests : TestBase<MarkdownTestContext>
new Entity().Set(IsMarkdown.Instance).SetTextContent("## Heading Two\nContent"),
};
ParseMarkdownHeadingOne op = context
.Resolve<ParseMarkdownHeadingOne>()
ExtractMarkdownHeadingOne op = context
.Resolve<ExtractMarkdownHeadingOne>()
.WithAddModelCallback((entity, heading) => entity.Set(heading));
IEnumerable<Entity> output = op.Run(input);
@ -97,8 +97,8 @@ public class ParseMarkdownHeadingOneTests : TestBase<MarkdownTestContext>
.SetTextContent("# Heading One\n\n# Heading Two\n Content"),
};
ParseMarkdownHeadingOne op = context
.Resolve<ParseMarkdownHeadingOne>()
ExtractMarkdownHeadingOne op = context
.Resolve<ExtractMarkdownHeadingOne>()
.WithAddModelCallback((entity, heading) => entity.Set(heading));
IEnumerable<Entity> output = op.Run(input);
@ -117,8 +117,8 @@ public class ParseMarkdownHeadingOneTests : TestBase<MarkdownTestContext>
List<Entity> input =
new() { new Entity().Set(IsMarkdown.Instance).SetTextContent("Content"), };
ParseMarkdownHeadingOne op = context
.Resolve<ParseMarkdownHeadingOne>()
ExtractMarkdownHeadingOne op = context
.Resolve<ExtractMarkdownHeadingOne>()
.WithAddModelCallback((entity, heading) => entity.Set(heading));
IEnumerable<Entity> output = op.Run(input);
@ -140,8 +140,8 @@ public class ParseMarkdownHeadingOneTests : TestBase<MarkdownTestContext>
new Entity().Set(IsMarkdown.Instance).SetTextContent("# Heading One\n\nContent"),
};
ParseMarkdownHeadingOne op = context
.Resolve<ParseMarkdownHeadingOne>()
ExtractMarkdownHeadingOne op = context
.Resolve<ExtractMarkdownHeadingOne>()
.WithAddModelCallback((entity, heading) => entity.Set(heading));
IEnumerable<Entity> output = op.Run(input);