Skip to content

Commit

Permalink
feat: Add "preset" for MkDocs
Browse files Browse the repository at this point in the history
Adds a "preset" (a preconfigured MdSerializationOptions instance)
with settings that make generated documents render properly when
using MkDocs:

- Increase MinimumListIndentationWidth to 4 so nested lists are rendered
  properly when using MkDocs default settings
- Change escaping of '<' and '>' characters to '&lt;' respectively '&gt;'
  • Loading branch information
ap0llo committed Oct 3, 2019
1 parent 784633c commit 3d63def
Show file tree
Hide file tree
Showing 6 changed files with 283 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,12 @@ namespace Grynwald.MarkdownGenerator
public Grynwald.MarkdownGenerator.MdTableStyle TableStyle { get; set; }
public Grynwald.MarkdownGenerator.ITextFormatter TextFormatter { get; set; }
public Grynwald.MarkdownGenerator.MdThematicBreakStyle ThematicBreakStyle { get; set; }
public class static Presets
{
public static readonly Grynwald.MarkdownGenerator.MdSerializationOptions Default;
public static readonly Grynwald.MarkdownGenerator.MdSerializationOptions MkDocs;
public static Grynwald.MarkdownGenerator.MdSerializationOptions Get(string name) { }
}
}
public sealed class MdSingleLineSpan : Grynwald.MarkdownGenerator.MdSpan
{
Expand Down Expand Up @@ -479,6 +485,15 @@ namespace Grynwald.MarkdownGenerator
Dash = 2,
Asterisk = 1,
}
public sealed class MkDocsTextFormatter : Grynwald.MarkdownGenerator.ITextFormatter
{
public static readonly Grynwald.MarkdownGenerator.MkDocsTextFormatter Instance;
public string EscapeText(string text) { }
}
public sealed class PresetNotFoundException : System.Exception
{
public PresetNotFoundException(string message) { }
}
public class static SyntaxVisualizer
{
public static string GetSyntaxTree(Grynwald.MarkdownGenerator.MdDocument document) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,51 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Xunit;

namespace Grynwald.MarkdownGenerator.Test
{
public class MdSerializationOptionsTest
{
public static IEnumerable<object[]> Properties()
private object GetTestValue(Type type)
{
foreach (var property in typeof(MdSerializationOptions).GetProperties(BindingFlags.Instance | BindingFlags.Public))
if (!type.IsValueType)
{
return null;
}

var defaultValue = Activator.CreateInstance(type);
if (type.IsEnum)
{
var values = Enum.GetValues(type);
if (values.Length <= 1)
return defaultValue;
else
return values.Cast<object>().First(x => !x.Equals(defaultValue));
}
else
{
yield return new[] { property.Name };
return defaultValue;
}
}

public static IEnumerable<object[]> DefaultInstancesAndProperties()
{
foreach (var property in typeof(MdSerializationOptions).GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
yield return new object[] { MdSerializationOptions.Default, property.Name };
foreach(var presetField in typeof(MdSerializationOptions.Presets).GetFields(BindingFlags.Static | BindingFlags.Public))
{
yield return new object[] { presetField.GetValue(null), property.Name };
}
}
}

[Theory]
[MemberData(nameof(Properties))]
public void Properties_of_the_default_instance_cannot_be_modified(string propertyName)
[MemberData(nameof(DefaultInstancesAndProperties))]
public void Properties_of_the_default_instances_cannot_be_modified(MdSerializationOptions instance, string propertyName)
{
// ARRANGE
var instance = MdSerializationOptions.Default;

var property = typeof(MdSerializationOptions).GetProperty(propertyName);

var testValue = GetTestValue(property.PropertyType);
Expand All @@ -36,6 +59,14 @@ public void Properties_of_the_default_instance_cannot_be_modified(string propert
Assert.Contains(propertyName, exception.InnerException.Message);
}

public static IEnumerable<object[]> Properties()
{
foreach (var property in typeof(MdSerializationOptions).GetProperties(BindingFlags.Instance | BindingFlags.Public))
{
yield return new object[] { property.Name };
}
}

[Theory]
[MemberData(nameof(Properties))]
public void Properties_of_non_default_instance_can_be_modified(string propertyName)
Expand Down Expand Up @@ -63,27 +94,44 @@ public void ListIndentationWidth_throws_ArgumentOutOfRangeException_when_set_to_
Assert.Throws<ArgumentOutOfRangeException>(() => instance.MinimumListIndentationWidth = value);
}

private object GetTestValue(Type type)
[Fact]
public void Presets_get_throws_PresetNotFoundException_for_unknown_preset_name()
{
if(!type.IsValueType)
{
return null;
}
Assert.Throws<PresetNotFoundException>(() => MdSerializationOptions.Presets.Get("Some unknown preset name"));
}

var defaultValue = Activator.CreateInstance(type);
if(type.IsEnum)
{
var values = Enum.GetValues(type);
if (values.Length <= 1)
return defaultValue;
else
return values.Cast<object>().First(x => !x.Equals(defaultValue));
}
else
[Fact]
public void Default_Preset_is_the_default_MdSerializationOptions_instance()
{
Assert.Same(MdSerializationOptions.Default, MdSerializationOptions.Presets.Default);
}


public static IEnumerable<object[]> PresetInstancesAndNames()
{
foreach (var presetField in typeof(MdSerializationOptions.Presets).GetFields(BindingFlags.Static | BindingFlags.Public))
{
return defaultValue;
yield return new object[] { presetField.GetValue(null), presetField.Name };
}
}

[Theory]
[MemberData(nameof(PresetInstancesAndNames))]
public void Presets_get_returns_expected_preset(MdSerializationOptions preset, string presetName)
{
Assert.Same(preset, MdSerializationOptions.Presets.Get(presetName));
Assert.Same(preset, MdSerializationOptions.Presets.Get(presetName.ToLower()));
Assert.Same(preset, MdSerializationOptions.Presets.Get(presetName.ToUpper()));
}


[Fact]
public void MkDocs_preset_has_expected_settings()
{
var sut = MdSerializationOptions.Presets.MkDocs;

Assert.Equal(4, sut.MinimumListIndentationWidth);
Assert.IsType<MkDocsTextFormatter>(sut.TextFormatter);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Xunit;

namespace Grynwald.MarkdownGenerator.Test
{
public class MkDocsTextFormatterTest
{
[Theory]
[InlineData("<", "&lt;")]
[InlineData(">", "&gt;")]
[InlineData("/", "\\/")]
[InlineData("\\", "\\\\")]
[InlineData("*", "\\*")]
[InlineData("_", "\\_")]
[InlineData("-", "\\-")]
[InlineData("=", "\\=")]
[InlineData("#", "\\#")]
[InlineData("`", "\\`")]
[InlineData("~", "\\~")]
[InlineData("[", "\\[")]
[InlineData("]", "\\]")]
[InlineData("!", "\\!")]
[InlineData("|", "\\|")]
public void Escape_text_escapes_expected_characters(string input, string expected)
{
// ARRANGE
var sut = MkDocsTextFormatter.Instance;

// ACT
var actual = sut.EscapeText(input);

// ASSERT
Assert.Equal(expected, actual);
}
}
}
95 changes: 81 additions & 14 deletions src/MarkdownGenerator/_Model/_Options/MdSerializationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,95 @@ public class MdSerializationOptions
{
public static readonly MdSerializationOptions Default = new MdSerializationOptions(isReadOnly: true);

public static class Presets
{
public static readonly MdSerializationOptions Default = MdSerializationOptions.Default;

/// <summary>
/// Gets serialization options optimized for rendering the generated Markdown file
/// using <see href="https://www.mkdocs.org/">MkDocs</see>.
/// </summary>
/// <remarks>
/// The MkDocs preset changes the following settings compared to the default preset:
/// <list type="bullet">
/// <item>
/// <description>
/// Increase <see cref="MinimumListIndentationWidth"/> to 4 so nested lists
/// are rendered properly when using MkDocs default settings (see
/// <see href="https://github.com/mkdocs/mkdocs/issues/545">GitHub issue 545</see>)
/// </description>
/// </item>
/// <item>
/// <description>
/// Change escaping of some characters because documents are not rendered correctly
/// when using the default escaping (see <see cref="MkDocsTextFormatter"/>)
/// </description>
/// </item>
/// </list>
/// </remarks>
public static readonly MdSerializationOptions MkDocs = new MdSerializationOptions(
isReadOnly: true,
minimumListIndentationWidth: 4,
textFormatter: MkDocsTextFormatter.Instance);

public static MdSerializationOptions Get(string name)
{
switch (name.ToLower())
{
case "default":
return Default;

case "mkdocs":
return MkDocs;

default:
throw new PresetNotFoundException($"Unknown preset '{name}'");
}
}
}

private readonly bool m_IsReadOnly;

private MdEmphasisStyle m_EmphasisStyle = MdEmphasisStyle.Asterisk;
private MdThematicBreakStyle m_ThematicBreakStyle = MdThematicBreakStyle.Underscore;
private MdHeadingStyle m_HeadingStyle = MdHeadingStyle.Atx;
private MdCodeBlockStyle m_CodeBlockStyle = MdCodeBlockStyle.Backtick;
private MdBulletListStyle m_BulletListStyle = MdBulletListStyle.Dash;
private MdOrderedListStyle m_OrderedListStyle = MdOrderedListStyle.Dot;
private int m_ListIndentationWidth = 2;
private MdTableStyle m_TableStyle = MdTableStyle.GFM;
private int m_MaxLineLength = -1;
private ITextFormatter m_TextFormatter = DefaultTextFormatter.Instance;
private MdEmphasisStyle m_EmphasisStyle;
private MdThematicBreakStyle m_ThematicBreakStyle;
private MdHeadingStyle m_HeadingStyle;
private MdCodeBlockStyle m_CodeBlockStyle;
private MdBulletListStyle m_BulletListStyle;
private MdOrderedListStyle m_OrderedListStyle;
private int m_MinimumListIndentationWidth;
private MdTableStyle m_TableStyle;
private int m_MaxLineLength;
private ITextFormatter m_TextFormatter;

public MdSerializationOptions() : this(isReadOnly: false)
{ }

private MdSerializationOptions(bool isReadOnly)
private MdSerializationOptions(
bool isReadOnly,
MdEmphasisStyle emphasisStyle = MdEmphasisStyle.Asterisk,
MdThematicBreakStyle thematicBreakStyle = MdThematicBreakStyle.Underscore,
MdHeadingStyle headingStyle = MdHeadingStyle.Atx,
MdCodeBlockStyle codeBlockStyle = MdCodeBlockStyle.Backtick,
MdBulletListStyle bulletListStyle = MdBulletListStyle.Dash,
MdOrderedListStyle orderedListStyle = MdOrderedListStyle.Dot,
int minimumListIndentationWidth = 2,
MdTableStyle tableStyle = MdTableStyle.GFM,
int maxLineLength = -1,
ITextFormatter textFormatter = null)
{
m_IsReadOnly = isReadOnly;
m_EmphasisStyle = emphasisStyle;
m_ThematicBreakStyle = thematicBreakStyle;
m_HeadingStyle = headingStyle;
m_CodeBlockStyle = codeBlockStyle;
m_BulletListStyle = bulletListStyle;
m_OrderedListStyle = orderedListStyle;
m_MinimumListIndentationWidth = minimumListIndentationWidth;
m_TableStyle = tableStyle;
m_MaxLineLength = maxLineLength;
m_TextFormatter = textFormatter ?? DefaultTextFormatter.Instance;
}


/// <summary>
/// Gets or sets the style for emphasized and strongly emphasized text.
/// <para>
Expand Down Expand Up @@ -124,13 +191,13 @@ public MdOrderedListStyle OrderedListStyle
/// <exception cref="ArgumentOutOfRangeException">Thrown when setting the property to a negative value.</exception>
public int MinimumListIndentationWidth
{
get => m_ListIndentationWidth;
get => m_MinimumListIndentationWidth;
set
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value), "Value must not be less than 0");

SetValue(nameof(MinimumListIndentationWidth), value, ref m_ListIndentationWidth);
SetValue(nameof(MinimumListIndentationWidth), value, ref m_MinimumListIndentationWidth);
}
}

Expand Down
70 changes: 70 additions & 0 deletions src/MarkdownGenerator/_Model/_Options/MkDocsTextFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System.Text;

namespace Grynwald.MarkdownGenerator
{
/// <summary>
/// Implementation of <see cref="ITextFormatter"/> optimized for rendering
/// generated Markdown file using <see href="https://www.mkdocs.org/">MkDocs</see>.
/// </summary>
/// <remarks>
/// This implementation works largely the same as <see cref="DefaultTextFormatter"/> but changes
/// how some characters are escaped:
/// <list type="bullet">
/// <item><description><c>&lt;</c> is escaped as <c>&amp;lt;</c>.</description></item>
/// <item><description><c>&gt;</c> is escaped as <c>&amp;gt;</c>.</description></item>
/// </list>
/// </remarks>
public sealed class MkDocsTextFormatter : ITextFormatter
{
/// <summary>
/// Gets the singleton instance of <see cref="MkDocsTextFormatter"/>
/// </summary>
public static readonly MkDocsTextFormatter Instance = new MkDocsTextFormatter();


private MkDocsTextFormatter()
{ }


/// <inheritdoc />
public string EscapeText(string text)
{
var stringBuilder = new StringBuilder();
for (var i = 0; i < text.Length; i++)
{
switch (text[i])
{
case '<':
stringBuilder.Append("&lt;");
break;
case '>':
stringBuilder.Append("&gt;");
break;

case '\\':
case '/':
case '*':
case '_':
case '-':
case '=':
case '#':
case '`':
case '~':
case '[':
case ']':
case '!':
case '|':
stringBuilder.Append('\\');
stringBuilder.Append(text[i]);
break;

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

return stringBuilder.ToString();
}
}
}
Loading

0 comments on commit 3d63def

Please sign in to comment.