From 5e05124c5ee42ec247a9d9c6ae8844ab871cb46c Mon Sep 17 00:00:00 2001 From: mysticmnd Date: Mon, 6 Jul 2026 07:13:44 +0530 Subject: [PATCH 1/2] feat: re-support net46 and netstandard2.0 targets Add net46 and netstandard2.0 back to the target frameworks, guarding net6+/net7+ APIs (GeneratedRegex, Regex.EnumerateMatches, span line splitting, range/index operators) behind conditional compilation and BCL polyfills (ReplaceLineEndings, DistinctBy, GetValueOrDefault, char-based Contains/StartsWith/EndsWith). Test project drops Verify in favour of plain assertions and checked-in snapshots so the suite runs on net48/net6.0, with downlevel-compatible test tooling. CI builds every TFM on Linux and runs net48 on Windows. --- .github/workflows/ci.yaml | 62 ++++++++++++----- .github/workflows/nuget-publish.yaml | 9 ++- ...With_Unknown_Tag_Raise_Option.verified.txt | 4 -- ...ewlines_ThenTrimExcessNewlines.verified.md | 3 - ...HTML_ThenConvertWithoutHanging.verified.md | 1 - ...ars_ConvertNewlineCharsToSpace.verified.md | 1 - src/ReverseMarkdown.Test/ConverterTests.cs | 43 +++++++----- .../ReverseMarkdown.Test.csproj | 42 +++++++++--- .../Snippets.Usage.verified.txt | 1 - src/ReverseMarkdown.Test/Snippets.cs | 6 +- src/ReverseMarkdown/Cleaner.cs | 11 +++ src/ReverseMarkdown/Converters/Code.cs | 4 +- src/ReverseMarkdown/Converters/H.cs | 2 +- src/ReverseMarkdown/Converters/Pre.cs | 6 ++ src/ReverseMarkdown/Converters/Text.cs | 19 +++++- src/ReverseMarkdown/GlobalUsings.cs | 7 ++ .../Helpers/LineSplitEnumerator.cs | 4 +- src/ReverseMarkdown/Helpers/Polyfills.cs | 67 +++++++++++++++++++ .../Helpers/StringExtensions.cs | 12 ++++ .../Helpers/StringReplaceValues.cs | 22 ++++++ src/ReverseMarkdown/Helpers/StringUtils.cs | 9 +++ src/ReverseMarkdown/ReverseMarkdown.csproj | 6 +- 22 files changed, 275 insertions(+), 66 deletions(-) delete mode 100644 src/ReverseMarkdown.Test/ConverterTests.Check_Converter_With_Unknown_Tag_Raise_Option.verified.txt delete mode 100644 src/ReverseMarkdown.Test/ConverterTests.WhenTableCellsWithPWithMarkupNewlines_ThenTrimExcessNewlines.verified.md delete mode 100644 src/ReverseMarkdown.Test/ConverterTests.When_DeeplyNestedParagraphs_WithMalformedHTML_ThenConvertWithoutHanging.verified.md delete mode 100644 src/ReverseMarkdown.Test/ConverterTests.When_TextWithinParagraphContainsNewlineChars_ConvertNewlineCharsToSpace.verified.md delete mode 100644 src/ReverseMarkdown.Test/Snippets.Usage.verified.txt create mode 100644 src/ReverseMarkdown/GlobalUsings.cs create mode 100644 src/ReverseMarkdown/Helpers/Polyfills.cs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c3de22b6..40565730 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,27 +12,53 @@ env: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 jobs: - job: - strategy: - matrix: - include: - - os: ubuntu-latest - artifact-name: Linux - #- os: macos-11 - # artifact-name: Darwin - #- os: windows-2022 - # artifact-name: Win64 - runs-on: ${{ matrix.os }} - continue-on-error: true + # Compiles every target framework (including the downlevel net46 and netstandard2.0 + # builds) and runs the full test suite on every framework that can execute on Linux. + # net6.0 resolves the library's netstandard2.0 build, so that build is fully tested. + build-and-test: + runs-on: ubuntu-latest steps: - - name: checkout repo + - name: Checkout repo uses: actions/checkout@v4 - - name: Install .NET 9.0.x + - name: Install .NET SDKs uses: actions/setup-dotnet@v4 with: - dotnet-version: 9.0.x + dotnet-version: | + 6.0.x + 8.0.x + 9.0.x + 10.0.x - name: Display dotnet info - run: dotnet --list-sdks - - name: Run tests - run: dotnet test src/ReverseMarkdown.Test/ReverseMarkdown.Test.csproj --framework net9.0 + run: dotnet --list-sdks + # Build all target frameworks: net46;netstandard2.0;net8.0;net9.0;net10.0. + # net46 is built on Linux via the Microsoft.NETFramework.ReferenceAssemblies package. + - name: Build library (all target frameworks) + run: dotnet build src/ReverseMarkdown/ReverseMarkdown.csproj -c Release + - name: Test net6.0 (netstandard2.0 build) + run: dotnet test src/ReverseMarkdown.Test/ReverseMarkdown.Test.csproj --framework net6.0 -c Release + - name: Test net8.0 + run: dotnet test src/ReverseMarkdown.Test/ReverseMarkdown.Test.csproj --framework net8.0 -c Release + - name: Test net9.0 + run: dotnet test src/ReverseMarkdown.Test/ReverseMarkdown.Test.csproj --framework net9.0 -c Release + - name: Test net10.0 + run: dotnet test src/ReverseMarkdown.Test/ReverseMarkdown.Test.csproj --framework net10.0 -c Release + # Runs the full suite on real .NET Framework so the net46 build is exercised at runtime + # (net48 -> library net46 build). Windows-only, since .NET Framework cannot run on Linux. + net-framework: + runs-on: windows-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + - name: Install .NET SDKs + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 9.0.x + - name: Display dotnet info + run: dotnet --list-sdks + - name: Test net48 (net46 build) + run: dotnet test src/ReverseMarkdown.Test/ReverseMarkdown.Test.csproj --framework net48 -c Release + - name: Test net6.0 (netstandard2.0 build) + run: dotnet test src/ReverseMarkdown.Test/ReverseMarkdown.Test.csproj --framework net6.0 -c Release diff --git a/.github/workflows/nuget-publish.yaml b/.github/workflows/nuget-publish.yaml index 2c5f04b4..8b8d99b9 100644 --- a/.github/workflows/nuget-publish.yaml +++ b/.github/workflows/nuget-publish.yaml @@ -19,11 +19,16 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Install .NET 9.0.x + - name: Install .NET SDKs uses: actions/setup-dotnet@v4 with: - dotnet-version: 9.0.x + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x + # Packs all target frameworks: net46;netstandard2.0;net8.0;net9.0;net10.0. + # net46 is built on Linux via the Microsoft.NETFramework.ReferenceAssemblies package. - name: Run Pack run: dotnet pack src/ReverseMarkdown/ReverseMarkdown.csproj -c Release shell: bash diff --git a/src/ReverseMarkdown.Test/ConverterTests.Check_Converter_With_Unknown_Tag_Raise_Option.verified.txt b/src/ReverseMarkdown.Test/ConverterTests.Check_Converter_With_Unknown_Tag_Raise_Option.verified.txt deleted file mode 100644 index 600441b7..00000000 --- a/src/ReverseMarkdown.Test/ConverterTests.Check_Converter_With_Unknown_Tag_Raise_Option.verified.txt +++ /dev/null @@ -1,4 +0,0 @@ -{ - Type: UnknownTagException, - Message: Unknown tag: unknown-tag -} \ No newline at end of file diff --git a/src/ReverseMarkdown.Test/ConverterTests.WhenTableCellsWithPWithMarkupNewlines_ThenTrimExcessNewlines.verified.md b/src/ReverseMarkdown.Test/ConverterTests.WhenTableCellsWithPWithMarkupNewlines_ThenTrimExcessNewlines.verified.md deleted file mode 100644 index c31f4c29..00000000 --- a/src/ReverseMarkdown.Test/ConverterTests.WhenTableCellsWithPWithMarkupNewlines_ThenTrimExcessNewlines.verified.md +++ /dev/null @@ -1,3 +0,0 @@ -| col1 | -| --- | -| data1 | \ No newline at end of file diff --git a/src/ReverseMarkdown.Test/ConverterTests.When_DeeplyNestedParagraphs_WithMalformedHTML_ThenConvertWithoutHanging.verified.md b/src/ReverseMarkdown.Test/ConverterTests.When_DeeplyNestedParagraphs_WithMalformedHTML_ThenConvertWithoutHanging.verified.md deleted file mode 100644 index 5f282702..00000000 --- a/src/ReverseMarkdown.Test/ConverterTests.When_DeeplyNestedParagraphs_WithMalformedHTML_ThenConvertWithoutHanging.verified.md +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/ReverseMarkdown.Test/ConverterTests.When_TextWithinParagraphContainsNewlineChars_ConvertNewlineCharsToSpace.verified.md b/src/ReverseMarkdown.Test/ConverterTests.When_TextWithinParagraphContainsNewlineChars_ConvertNewlineCharsToSpace.verified.md deleted file mode 100644 index 449f85f0..00000000 --- a/src/ReverseMarkdown.Test/ConverterTests.When_TextWithinParagraphContainsNewlineChars_ConvertNewlineCharsToSpace.verified.md +++ /dev/null @@ -1 +0,0 @@ -This service will be temporarily unavailable due to planned maintenance from 02:00-04:00 on 30/01/2020 \ No newline at end of file diff --git a/src/ReverseMarkdown.Test/ConverterTests.cs b/src/ReverseMarkdown.Test/ConverterTests.cs index 9ec8c82c..035dd80e 100644 --- a/src/ReverseMarkdown.Test/ConverterTests.cs +++ b/src/ReverseMarkdown.Test/ConverterTests.cs @@ -4,8 +4,6 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; -using VerifyTests; -using VerifyXunit; using Xunit; using Xunit.Abstractions; @@ -14,13 +12,24 @@ namespace ReverseMarkdown.Test public class ConverterTests { private readonly ITestOutputHelper _testOutputHelper; - private readonly VerifySettings _verifySettings; public ConverterTests(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; - _verifySettings = new VerifySettings(); - _verifySettings.DisableRequireUniquePrefix(); + } + + // Reads a checked-in snapshot file (formerly compared by Verify) and normalises + // line endings + trailing newline so it can be asserted on every target framework. + private static string LoadVerified(string testName, string extension) + { + var path = Path.Combine(GetProjectDirectory().FullName, + $"ConverterTests.{testName}.verified.{extension}"); + return Normalize(File.ReadAllText(path)); + } + + private static string Normalize(string text) + { + return text.Replace("\r\n", "\n").Replace("\r", "\n").TrimEnd('\n'); } private static readonly Lazy> CaseHtmlById = new(() => @@ -197,7 +206,7 @@ public void WhenOutputLineEndingConfigured_ThenNormalizeOutputLineEndings() var result = converter.Convert(html); - Assert.Equal(result, result.ReplaceLineEndings("\n")); + Assert.Equal(result, result.Replace("\r\n", "\n").Replace("\r", "\n")); Assert.DoesNotContain("\r", result); } @@ -591,15 +600,14 @@ public void WhenBase64ImgTag_WithSaveToFileAndNonExistentDirectory_ThenCreateDir [Fact] - public Task When_Underline_Tag_With_AliasConverter_Register_ThenConvertToItalics() + public void When_Underline_Tag_With_AliasConverter_Register_ThenConvertToItalics() { var html = LoadHtml("Underline_Tag_Alias"); var converter = new Converter(); converter.Register("u", new ReverseMarkdown.Converters.AliasConverter(converter, "em")); var result = converter.Convert(html); - var settings = new VerifySettings(); - settings.DisableRequireUniquePrefix(); - return Verifier.Verify(result, settings: settings, extension: "md"); + var expected = LoadVerified(nameof(When_Underline_Tag_With_AliasConverter_Register_ThenConvertToItalics), "md"); + Assert.Equal(expected, Normalize(result)); } @@ -611,7 +619,7 @@ public Task When_Underline_Tag_With_AliasConverter_Register_ThenConvertToItalics [Fact] - public Task Check_Converter_With_Unknown_Tag_Raise_Option() + public void Check_Converter_With_Unknown_Tag_Raise_Option() { var html = LoadHtml("Unknown_Tag_Raise"); var config = new Config @@ -619,8 +627,8 @@ public Task Check_Converter_With_Unknown_Tag_Raise_Option() UnknownTags = Config.UnknownTagsOption.Raise }; var converter = new Converter(config); - return Verifier.Throws(() => converter.Convert(html), settings: _verifySettings) - .IgnoreMember(e => e.StackTrace); + var ex = Assert.Throws(() => converter.Convert(html)); + Assert.Equal("Unknown tag: unknown-tag", ex.Message); } @@ -936,7 +944,10 @@ private static IEnumerable ApplyTagFilter(IEnumerable cases) return cases; } - var tags = filter.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var tags = filter.Split(',') + .Select(tag => tag.Trim()) + .Where(tag => tag.Length > 0) + .ToArray(); return cases.Where(testCase => testCase.Tags.Any(tag => tags.Contains(tag, StringComparer.OrdinalIgnoreCase))); } @@ -958,11 +969,11 @@ private static string LoadExpected(CaseData testCase) var expected = File.ReadAllText(path); if (expected.EndsWith("\r\n", StringComparison.Ordinal)) { - return expected[..^2]; + return expected.Substring(0, expected.Length - 2); } if (expected.EndsWith("\n", StringComparison.Ordinal)) { - return expected[..^1]; + return expected.Substring(0, expected.Length - 1); } return expected; diff --git a/src/ReverseMarkdown.Test/ReverseMarkdown.Test.csproj b/src/ReverseMarkdown.Test/ReverseMarkdown.Test.csproj index 3b027949..c7297467 100644 --- a/src/ReverseMarkdown.Test/ReverseMarkdown.Test.csproj +++ b/src/ReverseMarkdown.Test/ReverseMarkdown.Test.csproj @@ -1,23 +1,51 @@  - net8.0;net9.0;net10.0 + + net48;net6.0;net8.0;net9.0;net10.0 true preview + true + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + @@ -44,12 +72,6 @@ ConverterTests.cs - - ConverterTests.cs - - - ConverterTests.cs - ConverterTests.cs diff --git a/src/ReverseMarkdown.Test/Snippets.Usage.verified.txt b/src/ReverseMarkdown.Test/Snippets.Usage.verified.txt deleted file mode 100644 index 11d35d69..00000000 --- a/src/ReverseMarkdown.Test/Snippets.Usage.verified.txt +++ /dev/null @@ -1 +0,0 @@ -This a sample **paragraph** from [my site](http://test.com) \ No newline at end of file diff --git a/src/ReverseMarkdown.Test/Snippets.cs b/src/ReverseMarkdown.Test/Snippets.cs index 6bea4223..833abeba 100644 --- a/src/ReverseMarkdown.Test/Snippets.cs +++ b/src/ReverseMarkdown.Test/Snippets.cs @@ -1,13 +1,11 @@ using System; -using System.Threading.Tasks; using ReverseMarkdown; -using VerifyXunit; using Xunit; public class Snippets { [Fact] - public async Task Usage() + public void Usage() { #region Usage @@ -19,7 +17,7 @@ public async Task Usage() #endregion - await Verifier.Verify(result); + Assert.Equal("This a sample **paragraph** from [my site](http://test.com)", result); } [Fact] diff --git a/src/ReverseMarkdown/Cleaner.cs b/src/ReverseMarkdown/Cleaner.cs index 779001d7..143a88f5 100644 --- a/src/ReverseMarkdown/Cleaner.cs +++ b/src/ReverseMarkdown/Cleaner.cs @@ -6,6 +6,7 @@ namespace ReverseMarkdown; public static partial class Cleaner { +#if NET7_0_OR_GREATER [GeneratedRegex(@"\*(\s\*)+")] private static partial Regex SlackBoldCleaner(); @@ -14,6 +15,16 @@ public static partial class Cleaner { [GeneratedRegex(@"[\u0020\u00A0]")] private static partial Regex NonBreakingSpaces(); +#else + private static readonly Regex _slackBoldCleaner = new(@"\*(\s\*)+", RegexOptions.Compiled); + private static Regex SlackBoldCleaner() => _slackBoldCleaner; + + private static readonly Regex _slackItalicCleaner = new(@"_(\s_)+", RegexOptions.Compiled); + private static Regex SlackItalicCleaner() => _slackItalicCleaner; + + private static readonly Regex _nonBreakingSpaces = new(@"[\u0020\u00A0]", RegexOptions.Compiled); + private static Regex NonBreakingSpaces() => _nonBreakingSpaces; +#endif private static readonly StringReplaceValues TagBorders = new() { ["\n\t"] = string.Empty, diff --git a/src/ReverseMarkdown/Converters/Code.cs b/src/ReverseMarkdown/Converters/Code.cs index e857ceaa..180119b3 100644 --- a/src/ReverseMarkdown/Converters/Code.cs +++ b/src/ReverseMarkdown/Converters/Code.cs @@ -24,9 +24,9 @@ public override void Convert(TextWriter writer, HtmlNode node) var content = node.InnerHtml; var fence = CreateCommonMarkCodeFence(content); writer.Write(fence); - var needsBacktickPadding = content.Length > 0 && (content[0] == '`' || content[^1] == '`'); + var needsBacktickPadding = content.Length > 0 && (content[0] == '`' || content[content.Length - 1] == '`'); var needsWhitespacePadding = content.Length > 0 && - (char.IsWhiteSpace(content[0]) || char.IsWhiteSpace(content[^1])); + (char.IsWhiteSpace(content[0]) || char.IsWhiteSpace(content[content.Length - 1])); var needsPadding = needsBacktickPadding || needsWhitespacePadding; if (needsPadding) { diff --git a/src/ReverseMarkdown/Converters/H.cs b/src/ReverseMarkdown/Converters/H.cs index fd1b6df4..8ed4f815 100644 --- a/src/ReverseMarkdown/Converters/H.cs +++ b/src/ReverseMarkdown/Converters/H.cs @@ -67,7 +67,7 @@ private static string EscapeTrailingHashes(string content) var hashCount = content.Length - 1 - index; var escapedHashes = new string('#', hashCount); escapedHashes = escapedHashes.Replace("#", "\\#"); - return content[..(index + 1)] + escapedHashes; + return content.Substring(0, index + 1) + escapedHashes; } } } diff --git a/src/ReverseMarkdown/Converters/Pre.cs b/src/ReverseMarkdown/Converters/Pre.cs index 98be4956..202899f9 100644 --- a/src/ReverseMarkdown/Converters/Pre.cs +++ b/src/ReverseMarkdown/Converters/Pre.cs @@ -129,8 +129,14 @@ private static string GetLanguageFromHighlightClassAttribute(HtmlNode node) /// Extracts class attribute syntax using: highlight-json, highlight-source-json, language-json, brush: language /// Returns the Language in Match.Groups[2] /// +#if NET7_0_OR_GREATER [GeneratedRegex(@"(highlight-source-|language-|highlight-|brush:\s)([^\s]+)")] private static partial Regex ClassRegex(); +#else + private static readonly Regex _classRegex = + new(@"(highlight-source-|language-|highlight-|brush:\s)([^\s]+)", RegexOptions.Compiled); + private static Regex ClassRegex() => _classRegex; +#endif /// /// Checks class attribute for language class identifiers for various diff --git a/src/ReverseMarkdown/Converters/Text.cs b/src/ReverseMarkdown/Converters/Text.cs index f16c590c..a6088225 100644 --- a/src/ReverseMarkdown/Converters/Text.cs +++ b/src/ReverseMarkdown/Converters/Text.cs @@ -37,6 +37,7 @@ public Text(Converter converter) : base(converter) ["%3E"] = ">", }; +#if NET7_0_OR_GREATER [GeneratedRegex(@"`.*?`")] private static partial Regex BackTicks(); @@ -48,6 +49,22 @@ public Text(Converter converter) : base(converter) [GeneratedRegex(@"(?m)^ {0,3}\[[^\]\r\n]+\]:")] private static partial Regex CommonMarkLinkDefinitionPattern(); +#else + private static readonly Regex _backTicks = new(@"`.*?`", RegexOptions.Compiled); + private static Regex BackTicks() => _backTicks; + + private static readonly Regex _commonMarkInlineLinkOrImagePattern = + new(@"!?\[[^\]\r\n]*\]\([^\)\r\n]*\)", RegexOptions.Compiled); + private static Regex CommonMarkInlineLinkOrImagePattern() => _commonMarkInlineLinkOrImagePattern; + + private static readonly Regex _commonMarkReferenceLinkPattern = + new(@"\[[^\]\r\n]+\]\[[^\]\r\n]*\]", RegexOptions.Compiled); + private static Regex CommonMarkReferenceLinkPattern() => _commonMarkReferenceLinkPattern; + + private static readonly Regex _commonMarkLinkDefinitionPattern = + new(@"(?m)^ {0,3}\[[^\]\r\n]+\]:", RegexOptions.Compiled); + private static Regex CommonMarkLinkDefinitionPattern() => _commonMarkLinkDefinitionPattern; +#endif #endregion @@ -447,7 +464,7 @@ private static string EscapeLineStart(string line) private static bool IsSetextUnderline(string line, int index) { - var trimmed = line[index..].TrimEnd(); + var trimmed = line.Substring(index).TrimEnd(); if (trimmed.Length < 3) { return false; } diff --git a/src/ReverseMarkdown/GlobalUsings.cs b/src/ReverseMarkdown/GlobalUsings.cs new file mode 100644 index 00000000..e23cc595 --- /dev/null +++ b/src/ReverseMarkdown/GlobalUsings.cs @@ -0,0 +1,7 @@ +// On downlevel targets the polyfill extension methods (see Helpers/Polyfills.cs) are +// brought into scope everywhere so the rest of the code can call the modern instance +// methods (ReplaceLineEndings, Contains(char), DistinctBy, GetValueOrDefault, ...) +// unchanged. On net6.0+ the BCL provides these, so no global using is needed. +#if NETSTANDARD2_0 || NETFRAMEWORK +global using ReverseMarkdown.Compat; +#endif diff --git a/src/ReverseMarkdown/Helpers/LineSplitEnumerator.cs b/src/ReverseMarkdown/Helpers/LineSplitEnumerator.cs index f07cb271..35133df1 100644 --- a/src/ReverseMarkdown/Helpers/LineSplitEnumerator.cs +++ b/src/ReverseMarkdown/Helpers/LineSplitEnumerator.cs @@ -1,4 +1,5 @@ -using System; +#if NET6_0_OR_GREATER +using System; namespace ReverseMarkdown.Helpers; @@ -50,3 +51,4 @@ private ReadOnlySpan ReadNextLine(int pos) } } } +#endif diff --git a/src/ReverseMarkdown/Helpers/Polyfills.cs b/src/ReverseMarkdown/Helpers/Polyfills.cs new file mode 100644 index 00000000..d19799be --- /dev/null +++ b/src/ReverseMarkdown/Helpers/Polyfills.cs @@ -0,0 +1,67 @@ +#if NETSTANDARD2_0 || NETFRAMEWORK +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace ReverseMarkdown.Compat; + +/// +/// Polyfills for BCL APIs that exist on net6.0+/netstandard2.1 but are missing on +/// netstandard2.0 and .NET Framework (net46). These are surfaced everywhere on those +/// targets via a global using (see GlobalUsings.cs) so the main code can use the modern +/// instance methods unchanged. On net6.0+ targets this file is compiled out and the BCL +/// implementations are used instead. +/// +internal static class Polyfills { + // string.ReplaceLineEndings(string) — added in .NET 6. Mirrors the BCL's recognised + // line-break set (CR, LF, CRLF, FF, NEL, LS, PS), treating CRLF as a single sequence. + private static readonly Regex LineEndings = + new(@"\r\n|[\r\n\f\u0085\u2028\u2029]", RegexOptions.Compiled); + + internal static string ReplaceLineEndings(this string text, string replacementText) + { + return LineEndings.Replace(text, replacementText); + } + + // string.Contains(char) — added in netstandard2.1. + internal static bool Contains(this string text, char value) + { + return text.IndexOf(value) >= 0; + } + + // string.Contains(string, StringComparison) — added in netstandard2.1. + internal static bool Contains(this string text, string value, StringComparison comparisonType) + { + return text.IndexOf(value, comparisonType) >= 0; + } + + // string.StartsWith(char) — added in netstandard2.1. + internal static bool StartsWith(this string text, char value) + { + return text.Length > 0 && text[0] == value; + } + + // string.EndsWith(char) — added in netstandard2.1. + internal static bool EndsWith(this string text, char value) + { + return text.Length > 0 && text[text.Length - 1] == value; + } + + // Enumerable.DistinctBy(..) — added in .NET 6. + internal static IEnumerable DistinctBy( + this IEnumerable source, + Func keySelector, + IEqualityComparer? comparer = null) + { + return source.GroupBy(keySelector, comparer).Select(group => group.First()); + } + + // CollectionExtensions.GetValueOrDefault(..) — added in netstandard2.1. + internal static TValue? GetValueOrDefault( + this Dictionary dictionary, TKey key) + { + return dictionary.TryGetValue(key, out var value) ? value : default; + } +} +#endif diff --git a/src/ReverseMarkdown/Helpers/StringExtensions.cs b/src/ReverseMarkdown/Helpers/StringExtensions.cs index 6cca92ac..26a10a64 100644 --- a/src/ReverseMarkdown/Helpers/StringExtensions.cs +++ b/src/ReverseMarkdown/Helpers/StringExtensions.cs @@ -12,10 +12,22 @@ public static string Chomp(this string content) .Trim(); } +#if NET6_0_OR_GREATER internal static LineSplitEnumerator ReadLines(this string content) { return new LineSplitEnumerator(content); } +#else + // LineSplitEnumerator relies on ReadOnlySpan, which we avoid on netstandard2.0/net46. + // StringReader.ReadLine splits on \r, \n and \r\n with identical line semantics. + internal static System.Collections.Generic.IEnumerable ReadLines(this string content) + { + using var reader = new System.IO.StringReader(content); + while (reader.ReadLine() is { } line) { + yield return line; + } + } +#endif internal static string Replace(this string content, StringReplaceValues replacements) { diff --git a/src/ReverseMarkdown/Helpers/StringReplaceValues.cs b/src/ReverseMarkdown/Helpers/StringReplaceValues.cs index d3ce93f2..8869967d 100644 --- a/src/ReverseMarkdown/Helpers/StringReplaceValues.cs +++ b/src/ReverseMarkdown/Helpers/StringReplaceValues.cs @@ -12,6 +12,7 @@ internal class StringReplaceValues : Dictionary { private Regex Regex => _regex ??= new Regex($"{string.Join("|", this.Keys.Select(Regex.Escape))}", RegexOptions.Compiled); +#if NET7_0_OR_GREATER public string Replace(string input) { var offset = 0; @@ -29,4 +30,25 @@ public string Replace(string input) return sb?.ToString() ?? input; } +#else + // Regex.EnumerateMatches (and StringBuilder.Append(ReadOnlySpan)) are not available + // on netstandard2.0/net46, so fall back to Regex.Matches and substring-based appends. + public string Replace(string input) + { + var offset = 0; + StringBuilder? sb = null; + foreach (Match match in Regex.Matches(input)) { + sb ??= new StringBuilder(input.Length); + sb.Append(input.Substring(offset, match.Index - offset)); + sb.Append(this[match.Value]); + offset = match.Index + match.Length; + } + + if (sb is not null && offset != input.Length) { + sb.Append(input.Substring(offset, input.Length - offset)); + } + + return sb?.ToString() ?? input; + } +#endif } diff --git a/src/ReverseMarkdown/Helpers/StringUtils.cs b/src/ReverseMarkdown/Helpers/StringUtils.cs index ce98f70b..be277cbd 100644 --- a/src/ReverseMarkdown/Helpers/StringUtils.cs +++ b/src/ReverseMarkdown/Helpers/StringUtils.cs @@ -39,11 +39,20 @@ public static string GetScheme(string url) } +#if NET7_0_OR_GREATER [GeneratedRegex(@"\r?\n\s*\r?\n", RegexOptions.Singleline)] private static partial Regex LinkTextRegex(); [GeneratedRegex(@"`+")] private static partial Regex BackTickRunRegex(); +#else + private static readonly Regex _linkTextRegex = + new(@"\r?\n\s*\r?\n", RegexOptions.Singleline | RegexOptions.Compiled); + private static Regex LinkTextRegex() => _linkTextRegex; + + private static readonly Regex _backTickRunRegex = new(@"`+", RegexOptions.Compiled); + private static Regex BackTickRunRegex() => _backTickRunRegex; +#endif private static readonly StringReplaceValues _linkTextReplaceValues = new() { ["["] = @"\[", diff --git a/src/ReverseMarkdown/ReverseMarkdown.csproj b/src/ReverseMarkdown/ReverseMarkdown.csproj index ea40a4fc..b0b160b6 100644 --- a/src/ReverseMarkdown/ReverseMarkdown.csproj +++ b/src/ReverseMarkdown/ReverseMarkdown.csproj @@ -3,7 +3,7 @@ ReverseMarkdown is a Html to Markdown converter library in c# 5.4.0 Babu Annamalai - net8.0;net9.0;net10.0 + net46;netstandard2.0;net8.0;net9.0;net10.0 preview enable https://github.com/mysticmind/reversemarkdown-net @@ -13,6 +13,10 @@ + + + + https://github.com/mysticmind/reversemarkdown-net.git From b58b14af210c64be7e08c9a3d9977aabc28f7df8 Mon Sep 17 00:00:00 2001 From: mysticmnd Date: Mon, 6 Jul 2026 07:19:41 +0530 Subject: [PATCH 2/2] ci: run Build workflow on the 5.x maintenance branch The PR for the downlevel re-support targets 5.x, but the workflow only triggered on push/pull_request to master, so no checks ran. Add 5.x to both trigger filters. --- .github/workflows/ci.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 40565730..e8b4294f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,9 +3,11 @@ on: push: branches: - master + - 5.x pull_request: branches: - master + - 5.x env: DOTNET_CLI_TELEMETRY_OPTOUT: 1