Skip to content

Commit

Permalink
feat: Add public helper methods for generating Heading ids
Browse files Browse the repository at this point in the history
Add HtmlUtilities.ToUrlSlug() method that exposes the logic to that is used to determine auto-generated ids/anchor for headings
  • Loading branch information
ap0llo committed Mar 5, 2020
1 parent 80a7914 commit 01647f8
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# HtmlUtilities Class

**Namespace:** [Grynwald.MarkdownGenerator](../index.md)

**Assembly:** Grynwald.MarkdownGenerator

```csharp
public static class HtmlUtilities
```

**Inheritance:** objectHtmlUtilities

## Methods

| Name | Description |
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| [ToUrlSlug(string)](methods/ToUrlSlug.md) | Generates a "url slug" from the specified value that can be used as a HTML id in a Markdown document. |

___

*Documentation generated by [MdDocs](https://github.com/ap0llo/mddocs)*
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# HtmlUtilities.ToUrlSlug Method

**Declaring Type:** [HtmlUtilities](../index.md)

Generates a "url slug" from the specified value that can be used as a HTML id in a Markdown document.

```csharp
public static string ToUrlSlug(string value);
```

## Parameters

`value` string

## Remarks

This method can be used to generate a id for linking within a document (typically a link to a heading).

There is no official spec for how anchors for headings work. This implementation follows the guidance from [Stack Overflow](https://stackoverflow.com/questions/27981247/github-markdown-same-page-link):

- Leading and trailing whitespace is dropped.
- Punctuation marks are dropped.
- Upper case letters are converted to lower case.
- Spaces are replaced with `-`

## Returns

string

___

*Documentation generated by [MdDocs](https://github.com/ap0llo/mddocs)*
1 change: 1 addition & 0 deletions docs/apireference/Grynwald/MarkdownGenerator/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
| [DocumentSetExtensions](DocumentSetExtensions/index.md) | Provides Markdown\-specific extension methods for [DocumentSet\<T\>](DocumentSet-1/index.md). |
| [EnumerableExtensions](EnumerableExtensions/index.md) | |
| [FactoryMethods](FactoryMethods/index.md) | Defines static factory methods for blocks in markdown documents. When imported via "using static", this allows for more readable construction of documents, e.g.can be rewritten as |
| [HtmlUtilities](HtmlUtilities/index.md) | |
| [MdBlock](MdBlock/index.md) | Represents a block in a markdown document. Blocks are the basic building units of markdown documents. A document consists of one or more blocks. |
| [MdBlockQuote](MdBlockQuote/index.md) | Represent a block quote in a markdown document. For specification see [CommonMark \- Block quotes](https://spec.commonmark.org/0.28/#block-quotes). |
| [MdBulletList](MdBulletList/index.md) | Represents a bullet list. For specification see [CommonMark \- List items](https://spec.commonmark.org/0.28/#list-items) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ namespace Grynwald.MarkdownGenerator
public static Grynwald.MarkdownGenerator.MdTextSpan Text(string text) { }
public static Grynwald.MarkdownGenerator.MdThematicBreak ThematicBreak() { }
}
public class static HtmlUtilities
{
public static string ToUrlSlug(string value) { }
}
public interface IDocument
{
void Save(string path);
Expand Down
23 changes: 23 additions & 0 deletions src/MarkdownGenerator.Test/HtmlUtilitiesTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Xunit;

namespace Grynwald.MarkdownGenerator.Test
{
public class HtmlUtilitiesTest
{
[Theory]
[InlineData(" ", "")]
[InlineData(" heading", "heading")]
[InlineData("heading123", "heading123")]
[InlineData("Heading", "heading")]
[InlineData("My Heading", "my-heading")]
[InlineData("My Heading with a [link]()", "my-heading-with-a-link")]
public void ToUrlSlug_returns_expected_value(string text, string expected)
{
// ARRANGE
var actual = HtmlUtilities.ToUrlSlug(text);

// ACT / ASSERT
Assert.Equal(expected, actual);
}
}
}
13 changes: 7 additions & 6 deletions src/MarkdownGenerator.Test/_Model/_Blocks/MdHeadingTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ public void MdHeading_can_be_initialized_with_string_content_03()
Assert.Equal("Content2", textSpan2.Text);
}


[Fact]
public void MdHeading_can_be_initialized_with_span_content()
{
Expand All @@ -67,13 +66,13 @@ public void Anchor_is_initlaized_with_auto_generated_value(string headingText, s
var heading = new MdHeading(1, new MdRawMarkdownSpan(headingText));

// ACT / ASSERT
Assert.Equal(expectedAnchor, heading.Anchor);
Assert.Equal(expectedAnchor, heading.Anchor);
}


[Theory, CombinatorialData]
public void DeepEquals_returns_expected_value(
[CombinatorialValues(1,2,3,4,5,6)]int level,
[CombinatorialValues(1, 2, 3, 4, 5, 6)]int level,
[CombinatorialValues("Heading", "", "some text")] string text)
{
var instance1 = new MdHeading(level, text);
Expand All @@ -92,10 +91,12 @@ public void DeepEquals_returns_expected_value(
public void Anchor_can_be_set()
{
// ARRANGE
var heading = new MdHeading(1, "My Heading");
var heading = new MdHeading(1, "My Heading")
{

// ACT
heading.Anchor = "custom-anchor";
// ACT
Anchor = "custom-anchor"
};

// ASSERT
Assert.Equal("custom-anchor", heading.Anchor);
Expand Down
50 changes: 50 additions & 0 deletions src/MarkdownGenerator/HtmlUtilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using System.Text;

namespace Grynwald.MarkdownGenerator
{
public static class HtmlUtilities
{
/// <summary>
/// Generates a "url slug" from the specified value that can be used as a HTML id in a Markdown document.
/// </summary>
/// <remarks>
/// This method can be used to generate a id for linking within a document (typically a link to a heading).
/// <para>
/// There is no official spec for how anchors for headings work. This implementation follows the guidance from <see href="https://stackoverflow.com/questions/27981247/github-markdown-same-page-link">Stack Overflow</see>:
/// <list type="bullet">
/// <item><description>Leading and trailing whitespace is dropped.</description></item>
/// <item><description>Punctuation marks are dropped.</description></item>
/// <item><description>Upper case letters are converted to lower case.</description></item>
/// <item><description>Spaces are replaced with <c>-</c></description></item>
/// </list>
/// </para>
/// </remarks>
public static string ToUrlSlug(string? value)
{
value = value?.Trim();

if (String.IsNullOrEmpty(value))
{
return "";
}

var anchor = new StringBuilder();

foreach (var c in value!)
{
if (Char.IsLetter(c) || Char.IsNumber(c))
{
anchor.Append(Char.ToLower(c));
}
else if (Char.IsWhiteSpace(c))
{
anchor.Append('-');
}
}

return anchor.ToString();
}

}
}
46 changes: 10 additions & 36 deletions src/MarkdownGenerator/_Model/_Blocks/MdHeading.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Text;
using Grynwald.MarkdownGenerator.Internal;

namespace Grynwald.MarkdownGenerator
Expand Down Expand Up @@ -52,7 +51,7 @@ public sealed class MdHeading : MdLeafBlock
/// </remarks>
public string? Anchor { get; set; }


/// <summary>
/// Initializes a new instance of <see cref="MdHeading"/>
/// </summary>
Expand All @@ -61,10 +60,14 @@ public sealed class MdHeading : MdLeafBlock
public MdHeading(MdSpan text, int level)
{
if (level < 1 || level > 6)
{
throw new ArgumentOutOfRangeException(nameof(level), "Value must be in the range [1,6]");
}

if (text == null)
{
throw new ArgumentNullException(nameof(text));
}

if (text is MdSingleLineSpan singleLineSpan)
{
Expand All @@ -77,7 +80,7 @@ public MdHeading(MdSpan text, int level)

Level = level;

Anchor = GetAutoGeneratedAnchor();
Anchor = HtmlUtilities.ToUrlSlug(Text.ToString());
}

/// <summary>
Expand Down Expand Up @@ -108,46 +111,17 @@ public MdHeading(int level, params MdSpan[] text) : this(new MdCompositeSpan(tex
private bool DeepEquals(MdHeading? other)
{
if (other == null)
{
return false;
}

if (ReferenceEquals(this, other))
{
return true;
}

return Level == other.Level &&
Text.DeepEquals(other.Text);
}

private string GetAutoGeneratedAnchor()
{
// There is no official spec for how anchors for headings work
// This implementation follows the guidance here
// https://stackoverflow.com/questions/27981247/github-markdown-same-page-link
//
// - leading white spaces will be dropped
// - punctuation marks will be dropped
// - upper case will be converted to lower
// - spaces between letters will be converted to '-'

var headingText = Text.ToString().Trim();

if (String.IsNullOrEmpty(headingText))
return "";

var anchor = new StringBuilder();

foreach(var c in headingText)
{
if(char.IsLetter(c) || char.IsNumber(c))
{
anchor.Append(char.ToLower(c));
}
else if(char.IsWhiteSpace(c))
{
anchor.Append('-');
}
}

return anchor.ToString();
}
}
}

0 comments on commit 01647f8

Please sign in to comment.