Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ If you have used and benefitted from this library. Please feel free to sponsor m
**Markdown flavors**
- GitHub Flavoured Markdown conversion for br, pre, tasklists, and table. Use `var config = new ReverseMarkdown.Config(githubFlavoured:true);`. By default the table will always be converted to Github flavored markdown immaterial of this flag
- Slack Flavoured Markdown conversion. Use `var config = new ReverseMarkdown.Config { SlackFlavored = true };`
- CommonMark-focused output with opt-in flags to preserve compatibility. Use `var config = new ReverseMarkdown.Config { CommonMark = true };` This mode may emit inline HTML for tricky emphasis/link cases unless you disable `CommonMarkUseHtmlInlineTags`.

**Tables**
- Support for nested tables (converted as HTML inside markdown)
Expand Down Expand Up @@ -89,6 +90,10 @@ var converter = new ReverseMarkdown.Converter(config);
* `DefaultCodeBlockLanguage` - Option to set the default code block language for Github style markdown if class based language markers are not available
* `GithubFlavored` - Github style markdown for br, pre and table. Default is false
* `SlackFlavored` - Slack style markdown formatting. When enabled, uses `*` for bold, `_` for italic, `~` for strikethrough, and `•` for list bullets. Default is false
* `CommonMark` - Enable CommonMark-focused output rules. Default is false
* `CommonMarkUseHtmlInlineTags` - When CommonMark is enabled, emit HTML for inline tags (`em`, `strong`, `a`, `img`) to avoid delimiter edge cases. Default is true
* `CommonMarkIntrawordEmphasisSpacing` - When CommonMark is enabled, insert spaces to avoid intraword emphasis. Default is false
* Note: CommonMark is best used on its own. Combining `CommonMark` with `GithubFlavored` can produce mixed output; keep them separate unless you explicitly want that behavior.
* `CleanupUnnecessarySpaces` - Cleanup unnecessary spaces in the output. Default is true
* `SuppressDivNewlines` - Removes prefixed newlines from `div` tags. Default is false
* `ListBulletChar` - Allows you to change the bullet character. Default value is `-`. Some systems expect the bullet character to be `*` rather than `-`, this config allows you to change it. Note: This option is ignored when `SlackFlavored` is enabled
Expand Down
134 changes: 134 additions & 0 deletions src/ReverseMarkdown.Test/CommonMarkSpecTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using Markdig;
using Xunit;
using Xunit.Abstractions;

namespace ReverseMarkdown.Test
{
public class CommonMarkSpecTests
{
private readonly ITestOutputHelper _output;

public CommonMarkSpecTests(ITestOutputHelper output)
{
_output = output;
}

[Fact]
public void CommonMark_Spec_Examples_RoundTripHtml()
{
var specPath = GetSpecPath();
Assert.True(
File.Exists(specPath),
"CommonMark spec file not found. Download commonmark.json to src/ReverseMarkdown.Test/TestData/commonmark.json"
);

var json = File.ReadAllText(specPath);
var examples = JsonSerializer.Deserialize<List<CommonMarkExample>>(
json,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }
);
if (examples == null || examples.Count == 0) {
throw new InvalidOperationException("CommonMark spec file is empty or invalid.");
}

var maxExamples = GetIntEnvironmentVariable("COMMONMARK_MAX_EXAMPLES");
var selected = maxExamples > 0 ? examples.Take(maxExamples).ToList() : examples;

var converter = new Converter(new Config { CommonMark = true });
var pipeline = new MarkdownPipelineBuilder().Build();
var failures = new List<string>();

foreach (var example in selected) {
if (string.IsNullOrEmpty(example.Html)) {
continue;
}

var markdown = converter.Convert(example.Html);
var roundTripHtml = Markdown.ToHtml(markdown, pipeline);

var expected = NormalizeHtml(example.Html);
var actual = NormalizeHtml(roundTripHtml);

if (!string.Equals(expected, actual, StringComparison.Ordinal)) {
failures.Add(FormatFailure(example, markdown, expected, actual));
if (failures.Count >= 10) {
break;
}
}
}

if (failures.Count > 0) {
var message = $"CommonMark failures: {failures.Count}/{selected.Count}";
_output.WriteLine(message);
foreach (var failure in failures) {
_output.WriteLine(failure);
}

Assert.Fail(message);
}
}

private static string NormalizeHtml(string html)
{
if (string.IsNullOrEmpty(html)) {
return string.Empty;
}

var normalized = html.Replace("\r\n", "\n").TrimEnd();
normalized = normalized.Replace("<br>", "<br />");
normalized = normalized.Replace("<br/>", "<br />");
normalized = normalized.Replace("<hr>", "<hr />");
normalized = normalized.Replace("<hr/>", "<hr />");
normalized = normalized.Replace("&#10;", "\n");
normalized = normalized.Replace("&#xA;", "\n");
normalized = System.Text.RegularExpressions.Regex.Replace(normalized, @">\s+<", "><");

var doc = new HtmlAgilityPack.HtmlDocument();
doc.LoadHtml(normalized);
normalized = doc.DocumentNode.InnerHtml;
normalized = normalized.Replace("\u00A0", " ");

return normalized;
}

private static string FormatFailure(CommonMarkExample example, string markdown, string expected, string actual)
{
return $"Example {example.Example} ({example.Section}):\n" +
$"Markdown:\n{markdown}\n" +
$"Expected HTML:\n{expected}\n" +
$"Actual HTML:\n{actual}\n";
}

private static int GetIntEnvironmentVariable(string name)
{
var value = Environment.GetEnvironmentVariable(name);
return int.TryParse(value, out var result) ? result : 0;
}

private static string GetSpecPath()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory != null && !File.Exists(Path.Combine(directory.FullName, "ReverseMarkdown.Test.csproj"))) {
directory = directory.Parent;
}

if (directory == null) {
throw new DirectoryNotFoundException("Could not locate test project directory.");
}

return Path.Combine(directory.FullName, "TestData", "commonmark.json");
}

private sealed class CommonMarkExample
{
public int Example { get; set; }
public string Section { get; set; } = string.Empty;
public string Html { get; set; } = string.Empty;
}
}
}

This file was deleted.

This file was deleted.

This file was deleted.

Loading