Compare commits
3 commits
23a65c8674
...
edf6289dee
Author | SHA1 | Date | |
---|---|---|---|
D. Moonfire | edf6289dee | ||
D. Moonfire | b3ea80c937 | ||
D. Moonfire | 31d545321f |
26
flake.lock
26
flake.lock
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
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);
|
||||
}
|
||||
}
|
|
@ -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);
|
Loading…
Reference in a new issue