Skip to content

Commit

Permalink
feat: Allow customization of escaping
Browse files Browse the repository at this point in the history
Adds ITextFormatter abstraction for escaping text. The formatter that
is used for escaping text during serialization of spans can be
customized by providing a implementation of ITextFormatter and passing
it to serialization via MdSerializationOptions.

The formatter is specified using the newly introduced property
MdSerializationOptions.TextFormatter.

If no formatter is specified, DefaultTextFormatter is used which
escapes exactly like the previous implementation in MdTextSpan.
  • Loading branch information
ap0llo committed Oct 1, 2019
1 parent a7e909c commit eafac27
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 36 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Addititional dependencies used for testing:
- [xUnit](http://xunit.github.io/)
- [PublicApiGenerator](https://github.com/JakeGinnivan/ApiApprover)
- [ApprovalTests](https://github.com/approvals/ApprovalTests.Net)
- [Moq](https://github.com/moq/moq4)

## Versioning and Branching

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
[assembly: System.Runtime.Versioning.TargetFrameworkAttribute(".NETStandard,Version=v2.0", FrameworkDisplayName="")]
namespace Grynwald.MarkdownGenerator
{
public sealed class DefaultTextFormatter : Grynwald.MarkdownGenerator.ITextFormatter
{
public static readonly Grynwald.MarkdownGenerator.DefaultTextFormatter Instance;
public string EscapeText(string text) { }
}
public sealed class DocumentNotFoundException : System.Exception
{
public DocumentNotFoundException(string message) { }
Expand Down Expand Up @@ -127,6 +132,10 @@ namespace Grynwald.MarkdownGenerator
{
void Save(string path);
}
public interface ITextFormatter
{
string EscapeText(string text);
}
public abstract class MdBlock
{
internal MdBlock() { }
Expand Down Expand Up @@ -383,6 +392,7 @@ namespace Grynwald.MarkdownGenerator
public int MaxLineLength { get; set; }
public Grynwald.MarkdownGenerator.MdOrderedListStyle OrderedListStyle { get; set; }
public Grynwald.MarkdownGenerator.MdTableStyle TableStyle { get; set; }
public Grynwald.MarkdownGenerator.ITextFormatter TextFormatter { get; set; }
public Grynwald.MarkdownGenerator.MdThematicBreakStyle ThematicBreakStyle { get; set; }
}
public sealed class MdSingleLineSpan : Grynwald.MarkdownGenerator.MdSpan
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Grynwald.Utilities" Version="1.4.9" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
<PackageReference Include="Moq" Version="4.13.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="Xunit.Combinatorial" Version="1.2.7" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
Expand Down
34 changes: 33 additions & 1 deletion src/MarkdownGenerator.Test/_Model/_Spans/MdTextSpanTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Xunit;
using System;
using Moq;
using Xunit;

namespace Grynwald.MarkdownGenerator.Test
{
Expand All @@ -23,6 +25,36 @@ public void ToString_escapes_special_characters(char character)
);
}

[Theory]
[MarkdownSpecialCharacterData]
public void ToString_uses_the_text_formatter_set_in_the_serialization_options_for_escaping(char character)
{
var value = Guid.NewGuid().ToString();

var formatterMock = new Mock<ITextFormatter>();
// return some placeholder data to ensure text is are passed through the formatter
formatterMock.Setup(x => x.EscapeText(It.IsAny<string>())).Returns(value);

var serializationOptions = new MdSerializationOptions { TextFormatter = formatterMock.Object };

Assert.Equal(
value,
new MdTextSpan($"prefix{character}suffix").ToString(serializationOptions)
);
}

[Theory]
[MarkdownSpecialCharacterData]
public void ToString_uses_the_default_text_formatter_if_formatter_from_serialization_options_is_null(char character)
{
var serializationOptions = new MdSerializationOptions { TextFormatter = null };

Assert.Equal(
$"prefix\\{character}suffix",
new MdTextSpan($"prefix{character}suffix").ToString(serializationOptions)
);
}

[Theory]
[InlineData("Some string")]
[InlineData("")]
Expand Down
59 changes: 59 additions & 0 deletions src/MarkdownGenerator/_Model/_Options/DefaultTextFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System.Text;

namespace Grynwald.MarkdownGenerator
{
/// <summary>
/// Default implementation of <see cref="ITextFormatter"/>.
/// </summary>
/// <remarks>
/// <see cref="DefaultTextFormatter"/> escapes all characters with a function in the Markdown syntax
/// using a backslash.
/// </remarks>
public sealed class DefaultTextFormatter : ITextFormatter
{
/// <summary>
/// Gets the singleton instance of <see cref="DefaultTextFormatter"/>
/// </summary>
public static readonly DefaultTextFormatter Instance = new DefaultTextFormatter();


private DefaultTextFormatter()
{ }


/// <inheritdoc />
public string EscapeText(string text)
{
var stringBuilder = new StringBuilder();
for (var i = 0; i < text.Length; i++)
{
switch (text[i])
{
case '\\':
case '/':
case '<':
case '>':
case '*':
case '_':
case '-':
case '=':
case '#':
case '`':
case '~':
case '[':
case ']':
case '!':
case '|':
stringBuilder.Append('\\');
break;

default:
break;
}
stringBuilder.Append(text[i]);
}

return stringBuilder.ToString();
}
}
}
24 changes: 24 additions & 0 deletions src/MarkdownGenerator/_Model/_Options/ITextFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Grynwald.MarkdownGenerator
{
/// <summary>
/// Provides an abstraction over converting text to be written into a Markdown file.
/// </summary>
/// <remarks>
/// When text is written to a Markdown file, some characters with a syntactical meaning
/// in Markdown need to be escaped.
/// <para>
/// As the way escaping works differs between some Markdown-to-HTML converters,
/// the escaping can be customized.
/// </para>
/// </remarks>
/// <seealso cref="MdSerializationOptions.TextFormatter"/>
/// <seealso cref="DefaultTextFormatter"/>
public interface ITextFormatter
{
/// <summary>
/// Escapes the specified text.
/// </summary>
/// <param name="text">The text to process.</param>
string EscapeText(string text);
}
}
12 changes: 12 additions & 0 deletions src/MarkdownGenerator/_Model/_Options/MdSerializationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,17 @@ public class MdSerializationOptions
/// cannot be adhered to
/// </remarks>
public int MaxLineLength { get; set; } = -1;

/// <summary>
/// Gets or sets the implementation to use for escaping text when saving a Markdown document.
/// </summary>
/// <remarks>
/// The implementation of <see cref="ITextFormatter" /> set here will be used to escape
/// plain text before writing it to the output document.
/// <para>
/// When no escaper is set, <see cref="DefaultTextFormatter"/> will be used.
/// </para>
/// </remarks>
public ITextFormatter TextFormatter { get; set; } = DefaultTextFormatter.Instance;
}
}
41 changes: 6 additions & 35 deletions src/MarkdownGenerator/_Model/_Spans/MdTextSpan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,44 +24,15 @@ public sealed class MdTextSpan : MdSpan
public MdTextSpan(string text) =>
Text = text ?? throw new ArgumentNullException(nameof(text));

// TODO: Consider making the characters to escape configurable
/// <inheritdoc />
public override string ToString()
{
var stringBuilder = new StringBuilder();
for (var i = 0; i < Text.Length; i++)
{
switch (Text[i])
{
case '\\':
case '/':
case '<':
case '>':
case '*':
case '_':
case '-':
case '=':
case '#':
case '`':
case '~':
case '[':
case ']':
case '!':
case '|':
stringBuilder.Append('\\');
break;

default:
break;
}
stringBuilder.Append(Text[i]);
}

return stringBuilder.ToString();
}
public override string ToString() => ToString(MdSerializationOptions.Default);

/// <inheritdoc />
public override string ToString(MdSerializationOptions options) => ToString();
public override string ToString(MdSerializationOptions options)
{
var escaper = options.TextFormatter ?? DefaultTextFormatter.Instance;
return escaper.EscapeText(Text);
}

/// <inheritdoc />
public override bool DeepEquals(MdSpan other) => DeepEquals(other as MdTextSpan);
Expand Down

0 comments on commit eafac27

Please sign in to comment.