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"
|
"systems": "systems"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1710111843,
|
"lastModified": 1710169003,
|
||||||
"narHash": "sha256-FbxiA5A8YVdVH8wE/JxjRQKvRGQj2OvoiQmd++xcvvE=",
|
"narHash": "sha256-n5Xbw60+faIZsuhhwhRuY8f6p1Y+3DojFb+VR2vcfj8=",
|
||||||
"ref": "refs/heads/main",
|
"ref": "refs/heads/main",
|
||||||
"rev": "4631ae09b2b8dedbd8e454aef33658c2dc58040c",
|
"rev": "dfcc08e78f3b68076d780b38cd29f40b0288ae53",
|
||||||
"revCount": 10,
|
"revCount": 12,
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://src.mfgames.com/mfgames-cli/mfgames-conventional-commit.git"
|
"url": "https://src.mfgames.com/mfgames-cli/mfgames-conventional-commit.git"
|
||||||
},
|
},
|
||||||
|
@ -362,11 +362,11 @@
|
||||||
"nixpkgs": "nixpkgs_3"
|
"nixpkgs": "nixpkgs_3"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1709861835,
|
"lastModified": 1713495963,
|
||||||
"narHash": "sha256-vXuOnCnpm3ii728s9KyNrAwH0wZ6RvQwmN53H55GtdE=",
|
"narHash": "sha256-UDIqJjYr4/cnFZBVfJM6oxQADfNIoaXFcMTgbDEUIU4=",
|
||||||
"ref": "refs/heads/main",
|
"ref": "refs/heads/main",
|
||||||
"rev": "6ba1a8e9755ae03287457d5fe9bb3565f7421c4b",
|
"rev": "afea19adb0d64d334c7c42cacea41b361574891e",
|
||||||
"revCount": 17,
|
"revCount": 25,
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://src.mfgames.com/nixos-contrib/mfgames-project-setup-flake.git"
|
"url": "https://src.mfgames.com/nixos-contrib/mfgames-project-setup-flake.git"
|
||||||
},
|
},
|
||||||
|
@ -874,12 +874,12 @@
|
||||||
},
|
},
|
||||||
"nixpkgs_4": {
|
"nixpkgs_4": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1710021367,
|
"lastModified": 1713145326,
|
||||||
"narHash": "sha256-FuMVdWqXMT38u1lcySYyv93A7B8wU0EGzUr4t4jQu8g=",
|
"narHash": "sha256-m7+IWM6mkWOg22EC5kRUFCycXsXLSU7hWmHdmBfmC3s=",
|
||||||
"rev": "b94a96839afcc56de3551aa7472b8d9a3e77e05d",
|
"rev": "53a2c32bc66f5ae41a28d7a9a49d321172af621e",
|
||||||
"revCount": 556575,
|
"revCount": 557721,
|
||||||
"type": "tarball",
|
"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": {
|
"original": {
|
||||||
"type": "tarball",
|
"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>
|
/// </summary>
|
||||||
public class RewriteLinkTransformer
|
public class RewriteLinkTransformer
|
||||||
{
|
{
|
||||||
private string output;
|
|
||||||
|
|
||||||
public RewriteLinkTransformer() { }
|
public RewriteLinkTransformer() { }
|
||||||
|
|
||||||
public RewriteLinkTransformer(Action<LinkInline> onLink)
|
public RewriteLinkTransformer(Action<LinkInline> onLink)
|
||||||
|
@ -59,8 +57,6 @@ public class RewriteLinkTransformer
|
||||||
|
|
||||||
renderer.Write(document);
|
renderer.Write(document);
|
||||||
|
|
||||||
this.output = writer.ToString();
|
return writer.ToString();
|
||||||
|
|
||||||
return this.output;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,16 +8,16 @@ using MfGames.Nitride.Generators;
|
||||||
namespace MfGames.Nitride.Markdown;
|
namespace MfGames.Nitride.Markdown;
|
||||||
|
|
||||||
/// <summary>
|
/// <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
|
/// into a model to include in the component. The rest of the Markdown is put
|
||||||
/// back as the text content of the entity.
|
/// back as the text content of the entity.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[WithProperties]
|
[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;
|
this.validator = validator;
|
||||||
}
|
}
|
|
@ -2,9 +2,9 @@ using FluentValidation;
|
||||||
|
|
||||||
namespace MfGames.Nitride.Markdown.Validators;
|
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();
|
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>
|
/// <summary>
|
||||||
/// Tests the functionality of the ParseMarkdownHeadingOne operation.
|
/// Tests the functionality of the ParseMarkdownHeadingOne operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ParseMarkdownHeadingOneTests : TestBase<MarkdownTestContext>
|
public class ExtractMarkdownHeadingOneTests : TestBase<MarkdownTestContext>
|
||||||
{
|
{
|
||||||
public ParseMarkdownHeadingOneTests(ITestOutputHelper output)
|
public ExtractMarkdownHeadingOneTests(ITestOutputHelper output)
|
||||||
: base(output) { }
|
: base(output) { }
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
@ -29,8 +29,8 @@ public class ParseMarkdownHeadingOneTests : TestBase<MarkdownTestContext>
|
||||||
.SetTextContent("# Heading [One](/test)\n\nContent\nsecond\n\nline"),
|
.SetTextContent("# Heading [One](/test)\n\nContent\nsecond\n\nline"),
|
||||||
};
|
};
|
||||||
|
|
||||||
ParseMarkdownHeadingOne op = context
|
ExtractMarkdownHeadingOne op = context
|
||||||
.Resolve<ParseMarkdownHeadingOne>()
|
.Resolve<ExtractMarkdownHeadingOne>()
|
||||||
.WithAddModelCallback((entity, heading) => entity.Set(heading));
|
.WithAddModelCallback((entity, heading) => entity.Set(heading));
|
||||||
|
|
||||||
IEnumerable<Entity> output = op.Run(input);
|
IEnumerable<Entity> output = op.Run(input);
|
||||||
|
@ -49,8 +49,8 @@ public class ParseMarkdownHeadingOneTests : TestBase<MarkdownTestContext>
|
||||||
List<Entity> input =
|
List<Entity> input =
|
||||||
new() { new Entity().Set(IsMarkdown.Instance).SetTextContent("#\nContent"), };
|
new() { new Entity().Set(IsMarkdown.Instance).SetTextContent("#\nContent"), };
|
||||||
|
|
||||||
ParseMarkdownHeadingOne op = context
|
ExtractMarkdownHeadingOne op = context
|
||||||
.Resolve<ParseMarkdownHeadingOne>()
|
.Resolve<ExtractMarkdownHeadingOne>()
|
||||||
.WithAddModelCallback((entity, heading) => entity.Set(heading));
|
.WithAddModelCallback((entity, heading) => entity.Set(heading));
|
||||||
|
|
||||||
IEnumerable<Entity> output = op.Run(input);
|
IEnumerable<Entity> output = op.Run(input);
|
||||||
|
@ -72,8 +72,8 @@ public class ParseMarkdownHeadingOneTests : TestBase<MarkdownTestContext>
|
||||||
new Entity().Set(IsMarkdown.Instance).SetTextContent("## Heading Two\nContent"),
|
new Entity().Set(IsMarkdown.Instance).SetTextContent("## Heading Two\nContent"),
|
||||||
};
|
};
|
||||||
|
|
||||||
ParseMarkdownHeadingOne op = context
|
ExtractMarkdownHeadingOne op = context
|
||||||
.Resolve<ParseMarkdownHeadingOne>()
|
.Resolve<ExtractMarkdownHeadingOne>()
|
||||||
.WithAddModelCallback((entity, heading) => entity.Set(heading));
|
.WithAddModelCallback((entity, heading) => entity.Set(heading));
|
||||||
|
|
||||||
IEnumerable<Entity> output = op.Run(input);
|
IEnumerable<Entity> output = op.Run(input);
|
||||||
|
@ -97,8 +97,8 @@ public class ParseMarkdownHeadingOneTests : TestBase<MarkdownTestContext>
|
||||||
.SetTextContent("# Heading One\n\n# Heading Two\n Content"),
|
.SetTextContent("# Heading One\n\n# Heading Two\n Content"),
|
||||||
};
|
};
|
||||||
|
|
||||||
ParseMarkdownHeadingOne op = context
|
ExtractMarkdownHeadingOne op = context
|
||||||
.Resolve<ParseMarkdownHeadingOne>()
|
.Resolve<ExtractMarkdownHeadingOne>()
|
||||||
.WithAddModelCallback((entity, heading) => entity.Set(heading));
|
.WithAddModelCallback((entity, heading) => entity.Set(heading));
|
||||||
|
|
||||||
IEnumerable<Entity> output = op.Run(input);
|
IEnumerable<Entity> output = op.Run(input);
|
||||||
|
@ -117,8 +117,8 @@ public class ParseMarkdownHeadingOneTests : TestBase<MarkdownTestContext>
|
||||||
List<Entity> input =
|
List<Entity> input =
|
||||||
new() { new Entity().Set(IsMarkdown.Instance).SetTextContent("Content"), };
|
new() { new Entity().Set(IsMarkdown.Instance).SetTextContent("Content"), };
|
||||||
|
|
||||||
ParseMarkdownHeadingOne op = context
|
ExtractMarkdownHeadingOne op = context
|
||||||
.Resolve<ParseMarkdownHeadingOne>()
|
.Resolve<ExtractMarkdownHeadingOne>()
|
||||||
.WithAddModelCallback((entity, heading) => entity.Set(heading));
|
.WithAddModelCallback((entity, heading) => entity.Set(heading));
|
||||||
|
|
||||||
IEnumerable<Entity> output = op.Run(input);
|
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"),
|
new Entity().Set(IsMarkdown.Instance).SetTextContent("# Heading One\n\nContent"),
|
||||||
};
|
};
|
||||||
|
|
||||||
ParseMarkdownHeadingOne op = context
|
ExtractMarkdownHeadingOne op = context
|
||||||
.Resolve<ParseMarkdownHeadingOne>()
|
.Resolve<ExtractMarkdownHeadingOne>()
|
||||||
.WithAddModelCallback((entity, heading) => entity.Set(heading));
|
.WithAddModelCallback((entity, heading) => entity.Set(heading));
|
||||||
|
|
||||||
IEnumerable<Entity> output = op.Run(input);
|
IEnumerable<Entity> output = op.Run(input);
|
Loading…
Reference in a new issue