├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── ci.yaml │ ├── nuget-publish.yaml │ └── on-push-do-docs.yml ├── .gitignore ├── LICENSE ├── README.md ├── README.source.md └── src ├── .editorconfig ├── ReverseMarkdown.Test ├── ChildConverterTests.cs ├── Children │ └── IgnoreAWhenHasClass.cs ├── ConverterTests.Bug255_table_newline_char_issue.verified.md ├── ConverterTests.Bug294_Table_bug_with_row_superfluous_newlines.verified.md ├── ConverterTests.Bug391_AnchorTagUnnecessarilyIndented.verified.md ├── ConverterTests.Bug393_RegressionWithVaryingNewLines.verified.md ├── ConverterTests.Check_Converter_With_Unknown_Tag_ByPass_Option.verified.md ├── ConverterTests.Check_Converter_With_Unknown_Tag_Drop_Option.verified.md ├── ConverterTests.Check_Converter_With_Unknown_Tag_PassThrough_Option.verified.md ├── ConverterTests.Check_Converter_With_Unknown_Tag_Raise_Option.verified.txt ├── ConverterTests.Li_With_No_Parent.verified.md ├── ConverterTests.SlackFlavored_Bold.verified.md ├── ConverterTests.SlackFlavored_Bullets.verified.md ├── ConverterTests.SlackFlavored_Italic.verified.md ├── ConverterTests.SlackFlavored_Strikethrough.verified.md ├── ConverterTests.TestConversionOfMultiParagraphWithHeaders.verified.md ├── ConverterTests.WhenAnchorTagContainsImgTag_LinkTextShouldNotBeEscaped.verified.md ├── ConverterTests.WhenBoldTagContainsBRTag_ThenConvertToMarkdown.verified.md ├── ConverterTests.WhenCommentOverlapTag_WithRemoveComments_ThenDoNotStripContentBetweenComments.verified.md ├── ConverterTests.WhenListContainsMultipleParagraphs_ConvertToMarkdownAndIndentSiblings.verified.md ├── ConverterTests.WhenListContainsNewlineAndTabBetweenTagBorders_CleanupAndConvertToMarkdown.verified.md ├── ConverterTests.WhenListContainsParagraphsOutsideItems_ConvertToMarkdownAndIndentSiblings.verified.md ├── ConverterTests.WhenListItemTextContainsLeadingAndTrailingSpacesAndTabs_ThenConvertToMarkdownListItemWithSpacesAndTabsStripped.verified.md ├── ConverterTests.WhenRemovedCommentsIsEnabled_CommentsAreRemoved.verified.md ├── ConverterTests.WhenStyletagWithBypassOption_ReturnEmpty.verified.md ├── ConverterTests.WhenTableCellsWithDataAndP_ThenNewlineBeforeP.verified.md ├── ConverterTests.WhenTableCellsWithDiv_ThenDoNotAddNewlines.verified.md ├── ConverterTests.WhenTableCellsWithMultipleP_ThenNoNewlines.verified.md ├── ConverterTests.WhenTableCellsWithPWithMarkupNewlines_ThenTrimExcessNewlines.verified.md ├── ConverterTests.WhenTableCellsWithP_ThenDoNotAddNewlines.verified.md ├── ConverterTests.WhenTableCellsWithP_ThenNoNewlines.verified.md ├── ConverterTests.WhenTableHeadingWithAlignmentStyles_ThenTableHeaderShouldHaveProperAlignment.verified.md ├── ConverterTests.WhenTable_CellContainsBr_PreserveBrAndConvertToGFMTable.verified.md ├── ConverterTests.WhenTable_CellContainsParagraph_AddBrThenConvertToGFMTable.verified.md ├── ConverterTests.WhenTable_Cell_Content_WithNewline_Add_BR_ThenConvertToGFMTable.verified.md ├── ConverterTests.WhenTable_ContainsTheadTd_ConvertToGFMTable.verified.md ├── ConverterTests.WhenTable_ContainsTheadTh_ConvertToGFMTable.verified.md ├── ConverterTests.WhenTable_HasEmptyRow_DropsEmptyRow.verified.md ├── ConverterTests.WhenTable_ThenConvertToGFMTable.verified.md ├── ConverterTests.WhenTable_WithColSpan_TableHeaderColumnSpansHandling_ThenConvertToGFMTable.verified.md ├── ConverterTests.WhenTable_WithoutHeaderRow_With_TableWithoutHeaderRowHandlingOptionDefault_ThenConvertToGFMTable_WithFirstRowAsHeaderRow.verified.md ├── ConverterTests.WhenTable_WithoutHeaderRow_With_TableWithoutHeaderRowHandlingOptionEmptyRow_ThenConvertToGFMTable_WithEmptyHeaderRow.verified.md ├── ConverterTests.WhenThereAreBTag_ThenConvertToMarkdownDoubleAsterisks.verified.md ├── ConverterTests.WhenThereAreLineBreaksEncompassingParagraphText_It_Should_be_Removed.verified.md ├── ConverterTests.WhenThereAreMultipleLinks_ThenConvertThemToMarkdownLinks.verified.md ├── ConverterTests.WhenThereAreStrongTag_ThenConvertToMarkdownDoubleAsterisks.verified.md ├── ConverterTests.WhenThereHtmlWithHrefAndNoSchema_NotWhitelisted_ThenConvertToPlain.verified.md ├── ConverterTests.WhenThereHtmlWithHrefAndNoSchema_WhitelistedEmptyString_ThenConvertToMarkdown.verified.md ├── ConverterTests.WhenThereIsAsideTag.verified.md ├── ConverterTests.WhenThereIsBlockquoteTag_ThenConvertToMarkdownBlockquote.verified.md ├── ConverterTests.WhenThereIsBreakTag_ThenConvertToMarkdownDoubleSpacesCarriageReturn.verified.md ├── ConverterTests.WhenThereIsCodeTag_ThenConvertToMarkdownWithBackTick.verified.md ├── ConverterTests.WhenThereIsEmTag_ThenConvertToMarkdownSingleAsterisks.verified.md ├── ConverterTests.WhenThereIsEmptyBlockquoteTag_ThenConvertToMarkdownBlockquote.verified.md ├── ConverterTests.WhenThereIsEmptyPreTag_ThenConvertToMarkdownPre.verified.md ├── ConverterTests.WhenThereIsEmptyPreTag_ThenConvertToMarkdownPre_GFM.verified.md ├── ConverterTests.WhenThereIsEncompassingEmOrITag_ThenConvertToMarkdownSingleAsterisks_AnyEmOrITagsInsideAreIgnored.verified.md ├── ConverterTests.WhenThereIsEncompassingStrongOrBTag_ThenConvertToMarkdownDoubleAsterisks_AnyStrongOrBTagsInsideAreIgnored.verified.md ├── ConverterTests.WhenThereIsH1Tag_ThenConvertToMarkdownHeader.verified.md ├── ConverterTests.WhenThereIsH2Tag_ThenConvertToMarkdownHeader.verified.md ├── ConverterTests.WhenThereIsH3Tag_ThenConvertToMarkdownHeader.verified.md ├── ConverterTests.WhenThereIsH4Tag_ThenConvertToMarkdownHeader.verified.md ├── ConverterTests.WhenThereIsH5Tag_ThenConvertToMarkdownHeader.verified.md ├── ConverterTests.WhenThereIsH6Tag_ThenConvertToMarkdownHeader.verified.md ├── ConverterTests.WhenThereIsHeadingInsideTable_ThenIgnoreHeadingLevel.verified.md ├── ConverterTests.WhenThereIsHorizontalRule_ThenConvertToMarkdownHorizontalRule.verified.md ├── ConverterTests.WhenThereIsHtmlLinkNotWhitelisted_ThenBypass.verified.md ├── ConverterTests.WhenThereIsHtmlLinkWithDisallowedCharsInChildren_ThenEscapeTextInMarkdown.verified.md ├── ConverterTests.WhenThereIsHtmlLinkWithParensInHref_ThenEscapeHrefInMarkdown.verified.md ├── ConverterTests.WhenThereIsHtmlLinkWithTitle_ThenConvertToMarkdownLink.verified.md ├── ConverterTests.WhenThereIsHtmlLinkWithoutHttpSchemaAndNameWithoutScheme_SmartHandling_ThenConvertToMarkdown.verified.md ├── ConverterTests.WhenThereIsHtmlLink_ThenConvertToMarkdownLink.verified.md ├── ConverterTests.WhenThereIsHtmlWithHrefAndNameMatching_SmartHandling_ThenConvertToPlain.verified.md ├── ConverterTests.WhenThereIsHtmlWithHrefAndNameNotMatching_SmartHandling_ThenConvertToMarkdown.verified.md ├── ConverterTests.WhenThereIsHtmlWithMailtoSchemeAndNameWithoutScheme_SmartHandling_ThenConvertToPlain.verified.md ├── ConverterTests.WhenThereIsHtmlWithProtocolRelativeUrlHrefAndNameNotMatching_SmartHandling_ThenConvertToMarkdown.verified.md ├── ConverterTests.WhenThereIsHtmlWithTelSchemeAndNameWithoutScheme_SmartHandling_ThenConvertToPlain.verified.md ├── ConverterTests.WhenThereIsITag_ThenConvertToMarkdownSingleAsterisks.verified.md ├── ConverterTests.WhenThereIsImgTagAndSrcWithNoSchema_NotWhitelisted_ThenConvertToPlain.verified.md ├── ConverterTests.WhenThereIsImgTagAndSrcWithNoSchema_WhitelistedEmptyString_ThenConvertToMarkdown.verified.md ├── ConverterTests.WhenThereIsImgTagWithBracesInAltText_ThenEnsureAltTextIsEscapedInMarkdown.verified.md ├── ConverterTests.WhenThereIsImgTagWithHttpProtocolRelativeUrl_ConfigHasWhitelist_ThenConvertToMarkdown.verified.md ├── ConverterTests.WhenThereIsImgTagWithMultilineAltText_ThenEnsureNoBlankLinesInMarkdownAltText.verified.md ├── ConverterTests.WhenThereIsImgTagWithRelativeUrl_NotWhitelisted_ThenConvertToMarkdown.verified.md ├── ConverterTests.WhenThereIsImgTagWithUnixUrl_ConfigHasWhitelist_ThenConvertToMarkdown.verified.md ├── ConverterTests.WhenThereIsImgTagWithoutAltText_ThenConvertToMarkdownImageWithoutAltText.verified.md ├── ConverterTests.WhenThereIsImgTagWithoutTitle_ThenConvertToMarkdownImageWithoutTitle.verified.md ├── ConverterTests.WhenThereIsImgTag_SchemeIsWhitelisted_ThenConvertToMarkdown.verified.md ├── ConverterTests.WhenThereIsImgTag_SchemeNotWhitelisted_ThenEmptyOutput.verified.md ├── ConverterTests.WhenThereIsImgTag_ThenConvertToMarkdownImage.verified.md ├── ConverterTests.WhenThereIsOrderedListWithNestedUnorderedList_ThenConvertToMarkdownListWithNestedList.verified.md ├── ConverterTests.WhenThereIsOrderedList_ThenConvertToMarkdownList.verified.md ├── ConverterTests.WhenThereIsParagraphTag_ThenConvertToMarkdownDoubleLineBreakBeforeAndAfter.verified.md ├── ConverterTests.WhenThereIsPreTag_ThenConvertToMarkdownPre.verified.md ├── ConverterTests.WhenThereIsSingleAsteriskInText_ThenConvertToMarkdownEscapedAsterisk.verified.md ├── ConverterTests.WhenThereIsUnorderedListAndBulletIsAsterisk_ThenConvertToMarkdownList.verified.md ├── ConverterTests.WhenThereIsUnorderedListWithNestedOrderedList_ThenConvertToMarkdownListWithNestedList.verified.md ├── ConverterTests.WhenThereIsUnorderedList_ThenConvertToMarkdownList.verified.md ├── ConverterTests.WhenThereIsWhitespaceAroundNestedLists_PreventBlankLinesWhenConvertingToMarkdownList.verified.md ├── ConverterTests.When_Anchor_Text_with_Underscore_Do_Not_Escape.verified.md ├── ConverterTests.When_BR_With_GitHubFlavored_Config_ThenConvertToGFM_BR.verified.md ├── ConverterTests.When_CodeContainsSpacesAndIsSurroundedByWhitespace_Should_NotRemoveSpaces.verified.md ├── ConverterTests.When_CodeContainsSpaces_ShouldPreserveSpaces.verified.md ├── ConverterTests.When_CodeContainsSpanWithExtraSpaces_Should_NotNormalizeSpaces.verified.md ├── ConverterTests.When_Consecutive_Em_Tags_Should_Convert_Properly.verified.md ├── ConverterTests.When_Consecutive_Strong_Tags_Should_Convert_Properly.verified.md ├── ConverterTests.When_Content_Contains_script_tags_ignore_it.verified.md ├── ConverterTests.When_Converting_HTML_Ensure_To_Process_Only_Body.verified.md ├── ConverterTests.When_DescriptionListTag_ThenConvertToMarkdown_List.verified.md ├── ConverterTests.When_FencedCodeBlocks_Shouldnt_Have_Trailing_Line.verified.md ├── ConverterTests.When_Html_Containing_Nested_DIVs_Process_ONLY_Inner_Most_DIV.verified.md ├── ConverterTests.When_InlineCode_Shouldnt_Contain_Encoded_Chars.verified.md ├── ConverterTests.When_OrderedListIsInTable_LeaveListAsHtml.verified.md ├── ConverterTests.When_PRE_With_Confluence_Lang_Class_Att_And_GitHubFlavored_Config_ThenConvertToGFM_PRE.verified.md ├── ConverterTests.When_PRE_With_GitHubFlavored_Config_ThenConvertToGFM_PRE.verified.md ├── ConverterTests.When_PRE_With_Github_Site_DIV_Parent_And_GitHubFlavored_Config_ThenConvertToGFM_PRE.verified.md ├── ConverterTests.When_PRE_With_HighlightJs_Lang_Class_Att_And_GitHubFlavored_Config_ThenConvertToGFM_PRE.verified.md ├── ConverterTests.When_PRE_With_Lang_Highlight_Class_Att_And_GitHubFlavored_Config_ThenConvertToGFM_PRE.verified.md ├── ConverterTests.When_PRE_With_Parent_DIV_And_Non_GitHubFlavored_Config_FirstLine_CodeBlock_SpaceIndent_Should_Be_Retained.verified.md ├── ConverterTests.When_PRE_Without_Lang_Marker_Class_Att_And_GitHubFlavored_Config_With_DefaultCodeBlockLanguage_ThenConvertToGFM_PRE.verified.md ├── ConverterTests.When_PreTag_Contains_IndentedFirstLine_Should_PreserveIndentation.verified.md ├── ConverterTests.When_PreTag_Contains_IndentedFirstLine_Should_PreserveIndentation_GFM.verified.md ├── ConverterTests.When_PreTag_Within_List_Should_Be_Indented.verified.md ├── ConverterTests.When_PreTag_Within_List_Should_Be_Indented_With_GitHub_FlavouredMarkdown.verified.md ├── ConverterTests.When_SingleChild_BlockTag_With_Parent_DIV_Ignore_Processing_DIV.verified.md ├── ConverterTests.When_Spaces_In_Inline_Tags_Should_Be_Retained.verified.md ├── ConverterTests.When_Span_with_newline_Should_Convert_Properly.verified.md ├── ConverterTests.When_Strikethrough_And_Nested_Strikethrough.verified.md ├── ConverterTests.When_Sup_And_Nested_Sup.verified.md ├── ConverterTests.When_SuppressNewlineFlag_PrefixDiv_Should_Be_Empty.md ├── ConverterTests.When_SuppressNewlineFlag_PrefixDiv_Should_Be_Empty.verified.md ├── ConverterTests.When_Table_Within_List_Should_Be_Indented.verified.md ├── ConverterTests.When_Tag_In_PassThoughTags_List_Then_Use_PassThroughConverter.verified.md ├── ConverterTests.When_TextContainsAngleBrackets_HexEscapeAngleBrackets.verified.md ├── ConverterTests.When_TextIsHtmlEncoded_DecodeText.verified.md ├── ConverterTests.When_TextWithinParagraphContainsNewlineChars_ConvertNewlineCharsToSpace.verified.md ├── ConverterTests.When_Text_Contains_NewLineChars_Should_Not_Convert_To_BR.verified.md ├── ConverterTests.When_Text_Contains_NewLineChars_Should_Not_Convert_To_BR_GitHub_Flavoured.verified.md ├── ConverterTests.When_UnorderedListIsInTable_LeaveListAsHtml.verified.md ├── ConverterTests.cs ├── ReverseMarkdown.Test.csproj ├── Snippets.Usage.verified.txt └── Snippets.cs ├── ReverseMarkdown.sln ├── ReverseMarkdown ├── Cleaner.cs ├── Config.cs ├── Converter.cs ├── Converters │ ├── A.cs │ ├── Aside.cs │ ├── Blockquote.cs │ ├── Br.cs │ ├── ByPass.cs │ ├── Code.cs │ ├── ConverterBase.cs │ ├── Dd.cs │ ├── Div.cs │ ├── Dl.cs │ ├── Drop.cs │ ├── Dt.cs │ ├── Em.cs │ ├── H.cs │ ├── Hr.cs │ ├── IConverter.cs │ ├── Ignore.cs │ ├── Img.cs │ ├── Li.cs │ ├── Ol.cs │ ├── P.cs │ ├── PassThrough.cs │ ├── Pre.cs │ ├── S.cs │ ├── Strong.cs │ ├── Sup.cs │ ├── Table.cs │ ├── Td.cs │ ├── Text.cs │ └── Tr.cs ├── ReverseMarkdown.csproj ├── StringUtils.cs ├── UnknownTagException.cs └── UnsupportedTagExtension.cs └── mdsnippets.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | github: mysticmind 3 | # ko_fi: 'babuannamalai' 4 | # custom: ['https://www.buymeacoffee.com/babuannamalai'] 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "nuget" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | 10 | env: 11 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 12 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 13 | 14 | jobs: 15 | job: 16 | strategy: 17 | matrix: 18 | include: 19 | - os: ubuntu-latest 20 | artifact-name: Linux 21 | #- os: macos-11 22 | # artifact-name: Darwin 23 | #- os: windows-2022 24 | # artifact-name: Win64 25 | runs-on: ${{ matrix.os }} 26 | continue-on-error: true 27 | steps: 28 | - name: checkout repo 29 | uses: actions/checkout@v4 30 | - name: Install .NET 9.0.x 31 | uses: actions/setup-dotnet@v4 32 | with: 33 | dotnet-version: 9.0.x 34 | - name: Display dotnet info 35 | run: dotnet --list-sdks 36 | - name: Run tests 37 | run: dotnet test src/ReverseMarkdown.Test/ReverseMarkdown.Test.csproj --framework net9.0 38 | 39 | -------------------------------------------------------------------------------- /.github/workflows/nuget-publish.yaml: -------------------------------------------------------------------------------- 1 | name: NuGet Manual Publish 2 | 3 | on: [workflow_dispatch] 4 | 5 | env: 6 | config: Release 7 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 8 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 9 | 10 | jobs: 11 | publish_job: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | - name: Install .NET 9.0.x 19 | uses: actions/setup-dotnet@v4 20 | with: 21 | dotnet-version: 9.0.x 22 | 23 | - name: Run Pack 24 | run: dotnet pack src/ReverseMarkdown/ReverseMarkdown.csproj -c Release 25 | shell: bash 26 | 27 | - name: Publish to NuGet 28 | run: | 29 | find . -name '*.nupkg' -exec dotnet nuget push "{}" -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_API_KEY }} --skip-duplicate \; 30 | # find . -name '*.snupkg' -exec dotnet nuget push "{}" -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_API_KEY }} \; 31 | shell: bash 32 | 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/on-push-do-docs.yml: -------------------------------------------------------------------------------- 1 | name: on-push-do-docs 2 | on: 3 | push: 4 | jobs: 5 | docs: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - name: Run MarkdownSnippets 10 | run: | 11 | dotnet tool install --global MarkdownSnippets.Tool --version 27.0.2 12 | mdsnippets ${GITHUB_WORKSPACE} 13 | shell: bash 14 | - name: Push changes 15 | run: | 16 | git config --local user.email "action@github.com" 17 | git config --local user.name "GitHub Action" 18 | git commit -m "Docs changes [skip ci]" -a || echo "nothing to commit" 19 | remote="https://${GITHUB_ACTOR}:${{secrets.GITHUB_TOKEN}}@github.com/${GITHUB_REPOSITORY}.git" 20 | branch="${GITHUB_REF:11}" 21 | git push "${remote}" ${branch} || echo "nothing to push" 22 | shell: bash 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | obj 3 | bin 4 | bin/* 5 | deploy 6 | deploy/* 7 | _ReSharper.* 8 | *.user 9 | *.suo 10 | *.cache 11 | *.Cache 12 | Thumbs.db 13 | **/packages 14 | .idea 15 | *.received.* 16 | .DS_Store 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Babu Annamalai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | # Meet ReverseMarkdown 9 | 10 | [![Build status](https://github.com/mysticmind/reversemarkdown-net/actions/workflows/ci.yaml/badge.svg)](https://github.com/mysticmind/reversemarkdown-net/actions/workflows/ci.yaml) [![NuGet Version](https://badgen.net/nuget/v/reversemarkdown)](https://www.nuget.org/packages/ReverseMarkdown/) 11 | 12 | ReverseMarkdown is a Html to Markdown converter library in C#. Conversion is very reliable since HtmlAgilityPack (HAP) library is used for traversing the Html DOM. 13 | 14 | If you have used and benefitted from this library. Please feel free to buy me a coffee!
15 | GitHub Sponsor 16 | 17 | ## Usage 18 | 19 | Install the package from NuGet using `Install-Package ReverseMarkdown` or clone the repository and built it yourself. 20 | 21 | 22 | 23 | ```cs 24 | var converter = new ReverseMarkdown.Converter(); 25 | 26 | string html = "This a sample paragraph from my site"; 27 | 28 | string result = converter.Convert(html); 29 | ``` 30 | snippet source | anchor 31 | 32 | 33 | Will result in: 34 | 35 | 36 | 37 | ```txt 38 | This a sample **paragraph** from [my site](http://test.com) 39 | ``` 40 | snippet source | anchor 41 | 42 | 43 | The conversion can be customized: 44 | 45 | 46 | 47 | ```cs 48 | var config = new ReverseMarkdown.Config 49 | { 50 | // Include the unknown tag completely in the result (default as well) 51 | UnknownTags = Config.UnknownTagsOption.PassThrough, 52 | // generate GitHub flavoured markdown, supported for BR, PRE and table tags 53 | GithubFlavored = true, 54 | // will ignore all comments 55 | RemoveComments = true, 56 | // remove markdown output for links where appropriate 57 | SmartHrefHandling = true 58 | }; 59 | 60 | var converter = new ReverseMarkdown.Converter(config); 61 | ``` 62 | snippet source | anchor 63 | 64 | 65 | ## Configuration options 66 | 67 | * `DefaultCodeBlockLanguage` - Option to set the default code block language for Github style markdown if class based language markers are not available 68 | * `GithubFlavored` - Github style markdown for br, pre and table. Default is false 69 | * `SuppressDivNewlines` - Removes prefixed newlines from `div` tags. Default is false 70 | * `ListBulletChar` - Allows to change the bullet character. Default value is `-`. Some systems expect the bullet character to be `*` rather than `-`, this config allows to change it. 71 | * `RemoveComments` - Remove comment tags with text. Default is false 72 | * `SmartHrefHandling` - how to handle `` tag href attribute 73 | * `false` - Outputs `[{name}]({href}{title})` even if name and href is identical. This is the default option. 74 | * `true` - If name and href equals, outputs just the `name`. Note that if Uri is not well formed as per [`Uri.IsWellFormedUriString`](https://docs.microsoft.com/en-us/dotnet/api/system.uri.iswellformeduristring) (i.e string is not correctly escaped like `http://example.com/path/file name.docx`) then markdown syntax will be used anyway. 75 | 76 | If `href` contains `http/https` protocol, and `name` doesn't but otherwise are the same, output `href` only 77 | 78 | If `tel:` or `mailto:` scheme, but afterwards identical with name, output `name` only. 79 | * `UnknownTags` - handle unknown tags. 80 | * `UnknownTagsOption.PassThrough` - Include the unknown tag completely into the result. That is, the tag along with the text will be left in output. This is the default 81 | * `UnknownTagsOption.Drop` - Drop the unknown tag and its content 82 | * `UnknownTagsOption.Bypass` - Ignore the unknown tag but try to convert its content 83 | * `UnknownTagsOption.Raise` - Raise an error to let you know 84 | * `PassThroughTags` - Pass a list of tags to pass through as-is without any processing. 85 | * `WhitelistUriSchemes` - Specify which schemes (without trailing colon) are to be allowed for `` and `` tags. Others will be bypassed (output text or nothing). By default allows everything. 86 | 87 | If `string.Empty` provided and when `href` or `src` schema couldn't be determined - whitelists 88 | 89 | Schema is determined by `Uri` class, with exception when url begins with `/` (file schema) and `//` (http schema) 90 | * `TableWithoutHeaderRowHandling` - handle table without header rows 91 | * `TableWithoutHeaderRowHandlingOption.Default` - First row will be used as header row (default) 92 | * `TableWithoutHeaderRowHandlingOption.EmptyRow` - An empty row will be added as the header row 93 | 94 | > Note that UnknownTags config has been changed to an enumeration in v2.0.0 (breaking change) 95 | 96 | ## Features 97 | 98 | * Supports all the established html tags like h1, h2, h3, h4, h5, h6, p, em, strong, i, b, blockquote, code, img, a, hr, li, ol, ul, table, tr, th, td, br 99 | * Can deal with nested lists 100 | * Github Flavoured Markdown conversion supported for br, pre and table. Use `var config = new ReverseMarkdown.Config(githubFlavoured:true);`. By default table will always be converted to Github flavored markdown immaterial of this flag. 101 | 102 | ## Acknowledgements 103 | This library's initial implementation ideas were from the Ruby based Html to Markdown converter [ xijo/reverse_markdown](https://github.com/xijo/reverse_markdown). 104 | 105 | ## Copyright 106 | 107 | Copyright © Babu Annamalai 108 | 109 | ## License 110 | 111 | ReverseMarkdown is licensed under [MIT](http://www.opensource.org/licenses/mit-license.php "Read more about the MIT license form"). Refer to [License file](https://github.com/mysticmind/reversemarkdown-net/blob/master/LICENSE) for more information. 112 | -------------------------------------------------------------------------------- /README.source.md: -------------------------------------------------------------------------------- 1 | # Meet ReverseMarkdown 2 | 3 | [![Build status](https://github.com/mysticmind/reversemarkdown-net/actions/workflows/ci.yaml/badge.svg)](https://github.com/mysticmind/reversemarkdown-net/actions/workflows/ci.yaml) [![NuGet Version](https://badgen.net/nuget/v/reversemarkdown)](https://www.nuget.org/packages/ReverseMarkdown/) 4 | 5 | ReverseMarkdown is a Html to Markdown converter library in C#. Conversion is very reliable since the HtmlAgilityPack (HAP) library is used for traversing the HTML DOM. 6 | 7 | If you have used and benefitted from this library. Please feel free to buy me a coffee!
8 |
GitHub Sponsor Buy Me a Coffee at ko-fi.com Buy Me A Coffee 9 | 10 | ## Usage 11 | 12 | Install the package from NuGet using `Install-Package ReverseMarkdown` or clone the repository and build it yourself. 13 | 14 | 15 | 16 | ```cs 17 | var converter = new ReverseMarkdown.Converter(); 18 | 19 | string html = "This a sample paragraph from my site"; 20 | 21 | string result = converter.Convert(html); 22 | ``` 23 | snippet source | anchor 24 | 25 | 26 | Will result in: 27 | 28 | 29 | 30 | ```txt 31 | This a sample **paragraph** from [my site](http://test.com) 32 | ``` 33 | snippet source | anchor 34 | 35 | 36 | The conversion can also be customized: 37 | 38 | 39 | 40 | ```cs 41 | var config = new ReverseMarkdown.Config 42 | { 43 | // Include the unknown tag completely in the result (default as well) 44 | UnknownTags = Config.UnknownTagsOption.PassThrough, 45 | // generate GitHub flavoured markdown, supported for BR, PRE and table tags 46 | GithubFlavored = true, 47 | // will ignore all comments 48 | RemoveComments = true, 49 | // remove markdown output for links where appropriate 50 | SmartHrefHandling = true 51 | }; 52 | 53 | var converter = new ReverseMarkdown.Converter(config); 54 | ``` 55 | snippet source | anchor 56 | 57 | 58 | ## Configuration options 59 | 60 | * `DefaultCodeBlockLanguage` - Option to set the default code block language for Github style markdown if class based language markers are not available 61 | * `GithubFlavored` - Github style markdown for br, pre and table. Default is false 62 | * `SuppressDivNewlines` - Removes prefixed newlines from `div` tags. Default is false 63 | * `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. 64 | * `RemoveComments` - Remove comment tags with text. Default is false 65 | * `SmartHrefHandling` - How to handle `` tag href attribute 66 | * `false` - Outputs `[{name}]({href}{title})` even if the name and href is identical. This is the default option. 67 | * `true` - If the name and href equals, outputs just the `name`. Note that if the Uri is not well formed as per [`Uri.IsWellFormedUriString`](https://docs.microsoft.com/en-us/dotnet/api/system.uri.iswellformeduristring) (i.e string is not correctly escaped like `http://example.com/path/file name.docx`) then markdown syntax will be used anyway. 68 | 69 | If `href` contains `http/https` protocol, and `name` doesn't but otherwise are the same, output `href` only 70 | 71 | If `tel:` or `mailto:` scheme, but afterwards identical with name, output `name` only. 72 | * `UnknownTags` - handle unknown tags. 73 | * `UnknownTagsOption.PassThrough` - Include the unknown tag completely into the result. That is, the tag along with the text will be left in output. This is the default 74 | * `UnknownTagsOption.Drop` - Drop the unknown tag and its content 75 | * `UnknownTagsOption.Bypass` - Ignore the unknown tag but try to convert its content 76 | * `UnknownTagsOption.Raise` - Raise an error to let you know 77 | * `PassThroughTags` - Pass a list of tags to pass through as-is without any processing. 78 | * `WhitelistUriSchemes` - Specify which schemes (without trailing colon) are to be allowed for `` and `` tags. Others will be bypassed (output text or nothing). By default allows everything. 79 | 80 | If `string.Empty` provided and when `href` or `src` schema couldn't be determined - whitelists 81 | 82 | Schema is determined by `Uri` class, with exception when url begins with `/` (file schema) and `//` (http schema) 83 | * `TableWithoutHeaderRowHandling` - handle table without header rows 84 | * `TableWithoutHeaderRowHandlingOption.Default` - First row will be used as header row (default) 85 | * `TableWithoutHeaderRowHandlingOption.EmptyRow` - An empty row will be added as the header row 86 | * `TableHeaderColumnSpanHandling` - Set this flag to handle or process table header column with column spans 87 | 88 | > Note that UnknownTags config has been changed to an enumeration in v2.0.0 (breaking change) 89 | 90 | ## Features 91 | 92 | * Supports all the established html tags like h1, h2, h3, h4, h5, h6, p, em, strong, i, b, blockquote, code, img, a, hr, li, ol, ul, table, tr, th, td, br 93 | * Supports nested lists 94 | * Github Flavoured Markdown conversion supported for br, pre 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. 95 | 96 | ## Acknowledgements 97 | This library's initial implementation ideas were from the Ruby based Html to Markdown converter [ xijo/reverse_markdown](https://github.com/xijo/reverse_markdown). 98 | 99 | ## Copyright 100 | 101 | Copyright © Babu Annamalai 102 | 103 | ## License 104 | 105 | ReverseMarkdown is licensed under [MIT](http://www.opensource.org/licenses/mit-license.php "Read more about the MIT license form"). Refer to [License file](https://github.com/mysticmind/reversemarkdown-net/blob/master/LICENSE) for more information. 106 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cs] 4 | indent_style = space 5 | indent_size = 4 6 | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ChildConverterTests.cs: -------------------------------------------------------------------------------- 1 | using ReverseMarkdown.Converters; 2 | using ReverseMarkdown.Test.Children; 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Reflection; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | using Xunit; 12 | 13 | namespace ReverseMarkdown.Test 14 | { 15 | public class ChildConverterTests 16 | { 17 | [Fact] 18 | public void WhenConverter_A_IsReplacedByConverter_IgnoreAWhenHasClass() 19 | { 20 | var converter = new ReverseMarkdown.Converter(new Config(), typeof(IgnoreAWhenHasClass).Assembly); 21 | 22 | var type = converter.GetType(); 23 | var prop = type.GetField("Converters", BindingFlags.NonPublic | BindingFlags.Instance); 24 | 25 | Assert.NotNull(prop); 26 | 27 | var propValRaw = prop.GetValue(converter); 28 | 29 | Assert.NotNull(propValRaw); 30 | 31 | var propVal = (IDictionary)propValRaw; 32 | 33 | Assert.NotNull(propVal); 34 | 35 | var converters = propVal.Select(e => e.Value.GetType()).ToArray(); 36 | 37 | Assert.DoesNotContain(typeof(A), converters); 38 | Assert.Contains(typeof(IgnoreAWhenHasClass), converters); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/Children/IgnoreAWhenHasClass.cs: -------------------------------------------------------------------------------- 1 | using HtmlAgilityPack; 2 | 3 | using ReverseMarkdown.Converters; 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace ReverseMarkdown.Test.Children 12 | { 13 | internal class IgnoreAWhenHasClass : A 14 | { 15 | private readonly string _ignore = "ignore"; 16 | 17 | public IgnoreAWhenHasClass(Converter converter) : base(converter) 18 | { } 19 | 20 | public override string Convert(HtmlNode node) 21 | { 22 | if (node.HasClass(_ignore)) 23 | return ""; 24 | 25 | return base.Convert(node); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.Bug255_table_newline_char_issue.verified.md: -------------------------------------------------------------------------------- 1 | | Progression | Focus | 2 | | :--- | :--- | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.Bug294_Table_bug_with_row_superfluous_newlines.verified.md: -------------------------------------------------------------------------------- 1 | | 比较 | wordpress | hexo & hugo | 2 | | --- | --- | --- | 3 | | 搭建要求 | 一台服务器以及运行环境 | 静态生成页面,无需服务器。 | 4 | | 性能 | 由于是动态生成页面,可以通过自行配置提高性能,但是仍然无法媲美静态页面 | 几乎无需考虑性能问题 | 5 | | 访问速度 | 依赖于服务器配置以及cdn加速。 | 只需考虑cdn加速 | 6 | | 功能完善 | 作为强大的cms功能很完善,需要的功能基本可以插件下载直接实现。 | 额外功能也可以通过插件实现,不过稍微需要自行查找以及diy | 7 | | 后台管理 | 现成的后台管理功能,开箱即用 | 由于静态博客,本身没有后台管理,有需求需要自行搜索实现 | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.Bug391_AnchorTagUnnecessarilyIndented.verified.md: -------------------------------------------------------------------------------- 1 | An error occurred while importing data from feed 'FBA Producten'. More details can be found in the latest [feed validation report](). 2 | 3 | [View feed 4]() -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.Bug393_RegressionWithVaryingNewLines.verified.md: -------------------------------------------------------------------------------- 1 | This is regular text 2 | 3 | This is HTML: 4 | 5 | * Line 1 6 | * Line 2 7 | * Line 3 has an unknown tag -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.Check_Converter_With_Unknown_Tag_ByPass_Option.verified.md: -------------------------------------------------------------------------------- 1 | text in unknown tag -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.Check_Converter_With_Unknown_Tag_Drop_Option.verified.md: -------------------------------------------------------------------------------- 1 | paragraph text -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.Check_Converter_With_Unknown_Tag_PassThrough_Option.verified.md: -------------------------------------------------------------------------------- 1 | text in unknown tag 2 | paragraph text -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.Check_Converter_With_Unknown_Tag_Raise_Option.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Type: UnknownTagException, 3 | Message: Unknown tag: unknown-tag 4 | } -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.Li_With_No_Parent.verified.md: -------------------------------------------------------------------------------- 1 | - item -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.SlackFlavored_Bold.verified.md: -------------------------------------------------------------------------------- 1 | *test* | *test* -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.SlackFlavored_Bullets.verified.md: -------------------------------------------------------------------------------- 1 | • Item 1 2 | • Item 2 3 | • Item 3 -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.SlackFlavored_Italic.verified.md: -------------------------------------------------------------------------------- 1 | _test_ | _test_ -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.SlackFlavored_Strikethrough.verified.md: -------------------------------------------------------------------------------- 1 | ~test~ -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.TestConversionOfMultiParagraphWithHeaders.verified.md: -------------------------------------------------------------------------------- 1 | # Heading1 2 | 3 | First paragraph. 4 | 5 | # Heading2 6 | 7 | Second paragraph. -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenAnchorTagContainsImgTag_LinkTextShouldNotBeEscaped.verified.md: -------------------------------------------------------------------------------- 1 | [![](https://example.com/image.jpg)](https://www.example.com) -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenBoldTagContainsBRTag_ThenConvertToMarkdown.verified.md: -------------------------------------------------------------------------------- 1 | test**test** -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenCommentOverlapTag_WithRemoveComments_ThenDoNotStripContentBetweenComments.verified.md: -------------------------------------------------------------------------------- 1 | test content -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenListContainsMultipleParagraphs_ConvertToMarkdownAndIndentSiblings.verified.md: -------------------------------------------------------------------------------- 1 | 1. Paragraph 1 2 | 3 | Paragraph 1.1 4 | 5 | Paragraph 1.2 6 | 2. Paragraph 3 -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenListContainsNewlineAndTabBetweenTagBorders_CleanupAndConvertToMarkdown.verified.md: -------------------------------------------------------------------------------- 1 | 1. **Item1** 2 | 2. Item2 -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenListContainsParagraphsOutsideItems_ConvertToMarkdownAndIndentSiblings.verified.md: -------------------------------------------------------------------------------- 1 | 1. Item1 2 | 3 | Item 1 additional info 4 | 2. Item2 -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenListItemTextContainsLeadingAndTrailingSpacesAndTabs_ThenConvertToMarkdownListItemWithSpacesAndTabsStripped.verified.md: -------------------------------------------------------------------------------- 1 | 1. This is a text with leading and trailing spaces and tabs -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenRemovedCommentsIsEnabled_CommentsAreRemoved.verified.md: -------------------------------------------------------------------------------- 1 | Hello there -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenStyletagWithBypassOption_ReturnEmpty.verified.md: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenTableCellsWithDataAndP_ThenNewlineBeforeP.verified.md: -------------------------------------------------------------------------------- 1 | | data1
p | 2 | | --- | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenTableCellsWithDiv_ThenDoNotAddNewlines.verified.md: -------------------------------------------------------------------------------- 1 | | col1 | col2 | 2 | | --- | --- | 3 | | data1 | data2 | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenTableCellsWithMultipleP_ThenNoNewlines.verified.md: -------------------------------------------------------------------------------- 1 | | p1

p2 | 2 | | --- | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenTableCellsWithPWithMarkupNewlines_ThenTrimExcessNewlines.verified.md: -------------------------------------------------------------------------------- 1 | | col1 | 2 | | --- | 3 | | data1 | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenTableCellsWithP_ThenDoNotAddNewlines.verified.md: -------------------------------------------------------------------------------- 1 | | col1 | col2 | 2 | | --- | --- | 3 | | data1 | data2 | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenTableCellsWithP_ThenNoNewlines.verified.md: -------------------------------------------------------------------------------- 1 | | data1 | 2 | | --- | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenTableHeadingWithAlignmentStyles_ThenTableHeaderShouldHaveProperAlignment.verified.md: -------------------------------------------------------------------------------- 1 | | Col1 | Col2 | Col2 | 2 | | :--- | :---: | ---: | 3 | | 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenTable_CellContainsBr_PreserveBrAndConvertToGFMTable.verified.md: -------------------------------------------------------------------------------- 1 | | col1 | 2 | | --- | 3 | | line 1
line 2 | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenTable_CellContainsParagraph_AddBrThenConvertToGFMTable.verified.md: -------------------------------------------------------------------------------- 1 | | col1 | 2 | | --- | 3 | | line1

line2 | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenTable_Cell_Content_WithNewline_Add_BR_ThenConvertToGFMTable.verified.md: -------------------------------------------------------------------------------- 1 | | col1 | col2 | col3 | 2 | | --- | --- | --- | 3 | | data line1
line2 | data2 | data3 | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenTable_ContainsTheadTd_ConvertToGFMTable.verified.md: -------------------------------------------------------------------------------- 1 | | col1 | col2 | 2 | | --- | --- | 3 | | data1 | data2 | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenTable_ContainsTheadTh_ConvertToGFMTable.verified.md: -------------------------------------------------------------------------------- 1 | | col1 | col2 | 2 | | --- | --- | 3 | | data1 | data2 | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenTable_HasEmptyRow_DropsEmptyRow.verified.md: -------------------------------------------------------------------------------- 1 | | | 2 | | --- | 3 | | abc | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenTable_ThenConvertToGFMTable.verified.md: -------------------------------------------------------------------------------- 1 | | col1 | col2 | col3 | 2 | | --- | --- | --- | 3 | | data1 | data2 | data3 | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenTable_WithColSpan_TableHeaderColumnSpansHandling_ThenConvertToGFMTable.verified.md: -------------------------------------------------------------------------------- 1 | | col1 | col2 | col2 | col3 | 2 | | --- | --- | --- | --- | 3 | | data1 | data2.1 | data2.2 | data3 | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenTable_WithoutHeaderRow_With_TableWithoutHeaderRowHandlingOptionDefault_ThenConvertToGFMTable_WithFirstRowAsHeaderRow.verified.md: -------------------------------------------------------------------------------- 1 | | data1 | data2 | data3 | 2 | | --- | --- | --- | 3 | | data4 | data5 | data6 | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenTable_WithoutHeaderRow_With_TableWithoutHeaderRowHandlingOptionEmptyRow_ThenConvertToGFMTable_WithEmptyHeaderRow.verified.md: -------------------------------------------------------------------------------- 1 | | | | | 2 | | --- | --- | --- | 3 | | data1 | data2 | data3 | 4 | | data4 | data5 | data6 | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereAreBTag_ThenConvertToMarkdownDoubleAsterisks.verified.md: -------------------------------------------------------------------------------- 1 | This paragraph contains **bold** text -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereAreLineBreaksEncompassingParagraphText_It_Should_be_Removed.verified.md: -------------------------------------------------------------------------------- 1 | Some text goes here. -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereAreMultipleLinks_ThenConvertThemToMarkdownLinks.verified.md: -------------------------------------------------------------------------------- 1 | This is [first link](http://test.com) and [second link](http://test1.com) -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereAreStrongTag_ThenConvertToMarkdownDoubleAsterisks.verified.md: -------------------------------------------------------------------------------- 1 | This paragraph contains **bold** text -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereHtmlWithHrefAndNoSchema_NotWhitelisted_ThenConvertToPlain.verified.md: -------------------------------------------------------------------------------- 1 | yeah -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereHtmlWithHrefAndNoSchema_WhitelistedEmptyString_ThenConvertToMarkdown.verified.md: -------------------------------------------------------------------------------- 1 | [yeah](example.com) -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsAsideTag.verified.md: -------------------------------------------------------------------------------- 1 | This text is in an aside tag. 2 | This text appears after aside. -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsBlockquoteTag_ThenConvertToMarkdownBlockquote.verified.md: -------------------------------------------------------------------------------- 1 | This text has 2 | 3 | > blockquote 4 | 5 | . This text appear after header. -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsBreakTag_ThenConvertToMarkdownDoubleSpacesCarriageReturn.verified.md: -------------------------------------------------------------------------------- 1 | This is a paragraph. 2 | This line appears after break. -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsCodeTag_ThenConvertToMarkdownWithBackTick.verified.md: -------------------------------------------------------------------------------- 1 | This text has code `alert();` -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsEmTag_ThenConvertToMarkdownSingleAsterisks.verified.md: -------------------------------------------------------------------------------- 1 | This is a *sample* paragraph -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsEmptyBlockquoteTag_ThenConvertToMarkdownBlockquote.verified.md: -------------------------------------------------------------------------------- 1 | This text has 2 | 3 | . This text appear after header. -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsEmptyPreTag_ThenConvertToMarkdownPre.verified.md: -------------------------------------------------------------------------------- 1 | This text has pre tag content 2 | 3 | Next line of text -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsEmptyPreTag_ThenConvertToMarkdownPre_GFM.verified.md: -------------------------------------------------------------------------------- 1 | This text has pre tag content 2 | 3 | ``` 4 | 5 | ``` 6 | Next line of text -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsEncompassingEmOrITag_ThenConvertToMarkdownSingleAsterisks_AnyEmOrITagsInsideAreIgnored.verified.md: -------------------------------------------------------------------------------- 1 | *This is a sample paragraph* -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsEncompassingStrongOrBTag_ThenConvertToMarkdownDoubleAsterisks_AnyStrongOrBTagsInsideAreIgnored.verified.md: -------------------------------------------------------------------------------- 1 | **Paragraph is encompassed with strong tag and also has bold text words within it** -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsH1Tag_ThenConvertToMarkdownHeader.verified.md: -------------------------------------------------------------------------------- 1 | This text has 2 | # header 3 | . This text appear after header. -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsH2Tag_ThenConvertToMarkdownHeader.verified.md: -------------------------------------------------------------------------------- 1 | This text has 2 | ## header 3 | . This text appear after header. -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsH3Tag_ThenConvertToMarkdownHeader.verified.md: -------------------------------------------------------------------------------- 1 | This text has 2 | ### header 3 | . This text appear after header. -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsH4Tag_ThenConvertToMarkdownHeader.verified.md: -------------------------------------------------------------------------------- 1 | This text has 2 | #### header 3 | . This text appear after header. -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsH5Tag_ThenConvertToMarkdownHeader.verified.md: -------------------------------------------------------------------------------- 1 | This text has 2 | ##### header 3 | . This text appear after header. -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsH6Tag_ThenConvertToMarkdownHeader.verified.md: -------------------------------------------------------------------------------- 1 | This text has 2 | ###### header 3 | . This text appear after header. -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHeadingInsideTable_ThenIgnoreHeadingLevel.verified.md: -------------------------------------------------------------------------------- 1 | | Heading **text** | 2 | | --- | 3 | | Content | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHorizontalRule_ThenConvertToMarkdownHorizontalRule.verified.md: -------------------------------------------------------------------------------- 1 | This text has horizontal rule. 2 | * * * 3 | Next line of text -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHtmlLinkNotWhitelisted_ThenBypass.verified.md: -------------------------------------------------------------------------------- 1 | Leave [http](http://example.com), [https](https://example.com), [ftp](ftp://example.com), [ftps](ftps://example.com), [file](file://example.com). Remove data, tel and whatever -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHtmlLinkWithDisallowedCharsInChildren_ThenEscapeTextInMarkdown.verified.md: -------------------------------------------------------------------------------- 1 | [this \]( might break things](http://example.com) -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHtmlLinkWithParensInHref_ThenEscapeHrefInMarkdown.verified.md: -------------------------------------------------------------------------------- 1 | [link](http://example.com?id=foo%29bar) -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHtmlLinkWithTitle_ThenConvertToMarkdownLink.verified.md: -------------------------------------------------------------------------------- 1 | This is [a link](http://test.com "with title") -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHtmlLinkWithoutHttpSchemaAndNameWithoutScheme_SmartHandling_ThenConvertToMarkdown.verified.md: -------------------------------------------------------------------------------- 1 | [example.com](ftp://example.com) -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHtmlLink_ThenConvertToMarkdownLink.verified.md: -------------------------------------------------------------------------------- 1 | This is [a link](http://test.com) -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHtmlWithHrefAndNameMatching_SmartHandling_ThenConvertToPlain.verified.md: -------------------------------------------------------------------------------- 1 | http://example.com/abc?x -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHtmlWithHrefAndNameNotMatching_SmartHandling_ThenConvertToMarkdown.verified.md: -------------------------------------------------------------------------------- 1 | [Something intact](https://example.com) -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHtmlWithMailtoSchemeAndNameWithoutScheme_SmartHandling_ThenConvertToPlain.verified.md: -------------------------------------------------------------------------------- 1 | george@example.com -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHtmlWithProtocolRelativeUrlHrefAndNameNotMatching_SmartHandling_ThenConvertToMarkdown.verified.md: -------------------------------------------------------------------------------- 1 | [example.com](//example.com) -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHtmlWithTelSchemeAndNameWithoutScheme_SmartHandling_ThenConvertToPlain.verified.md: -------------------------------------------------------------------------------- 1 | +1123-45678 -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsITag_ThenConvertToMarkdownSingleAsterisks.verified.md: -------------------------------------------------------------------------------- 1 | This is a *sample* paragraph -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagAndSrcWithNoSchema_NotWhitelisted_ThenConvertToPlain.verified.md: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagAndSrcWithNoSchema_WhitelistedEmptyString_ThenConvertToMarkdown.verified.md: -------------------------------------------------------------------------------- 1 | ![](example.com) -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagWithBracesInAltText_ThenEnsureAltTextIsEscapedInMarkdown.verified.md: -------------------------------------------------------------------------------- 1 | This text has image ![a\]b](http://test.com/images/test.png). Next line of text -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagWithHttpProtocolRelativeUrl_ConfigHasWhitelist_ThenConvertToMarkdown.verified.md: -------------------------------------------------------------------------------- 1 | ![](//example.gif) -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagWithMultilineAltText_ThenEnsureNoBlankLinesInMarkdownAltText.verified.md: -------------------------------------------------------------------------------- 1 | This text has image ![cat 2 | dog](http://test.com/images/test.png). Next line of text -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagWithRelativeUrl_NotWhitelisted_ThenConvertToMarkdown.verified.md: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagWithUnixUrl_ConfigHasWhitelist_ThenConvertToMarkdown.verified.md: -------------------------------------------------------------------------------- 1 | ![](/example.gif) -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagWithoutAltText_ThenConvertToMarkdownImageWithoutAltText.verified.md: -------------------------------------------------------------------------------- 1 | This text has image ![](http://test.com/images/test.png). Next line of text -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagWithoutTitle_ThenConvertToMarkdownImageWithoutTitle.verified.md: -------------------------------------------------------------------------------- 1 | This text has image ![alt](http://test.com/images/test.png). Next line of text -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTag_SchemeIsWhitelisted_ThenConvertToMarkdown.verified.md: -------------------------------------------------------------------------------- 1 | ![](data:image/gif;base64,R0lGODlhEAAQ...) -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTag_SchemeNotWhitelisted_ThenEmptyOutput.verified.md: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTag_ThenConvertToMarkdownImage.verified.md: -------------------------------------------------------------------------------- 1 | This text has image ![alt](http://test.com/images/test.png "title"). Next line of text -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsOrderedListWithNestedUnorderedList_ThenConvertToMarkdownListWithNestedList.verified.md: -------------------------------------------------------------------------------- 1 | This text has ordered list. 2 | 1. OuterItem1 3 | - InnerItem1 4 | - InnerItem2 5 | 2. Item2 -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsOrderedList_ThenConvertToMarkdownList.verified.md: -------------------------------------------------------------------------------- 1 | This text has ordered list. 2 | 1. Item1 3 | 2. Item2 -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsParagraphTag_ThenConvertToMarkdownDoubleLineBreakBeforeAndAfter.verified.md: -------------------------------------------------------------------------------- 1 | This text has markup 2 | paragraph. 3 | Next line of text -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsPreTag_ThenConvertToMarkdownPre.verified.md: -------------------------------------------------------------------------------- 1 | This text has pre tag content 2 | 3 | Predefined text 4 | 5 | Next line of text -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsSingleAsteriskInText_ThenConvertToMarkdownEscapedAsterisk.verified.md: -------------------------------------------------------------------------------- 1 | This is a sample(\*) paragraph -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsUnorderedListAndBulletIsAsterisk_ThenConvertToMarkdownList.verified.md: -------------------------------------------------------------------------------- 1 | This text has unordered list. 2 | * Item1 3 | * Item2 -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsUnorderedListWithNestedOrderedList_ThenConvertToMarkdownListWithNestedList.verified.md: -------------------------------------------------------------------------------- 1 | This text has ordered list. 2 | - OuterItem1 3 | 1. InnerItem1 4 | 2. InnerItem2 5 | - Item2 -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsUnorderedList_ThenConvertToMarkdownList.verified.md: -------------------------------------------------------------------------------- 1 | This text has unordered list. 2 | - Item1 3 | - Item2 -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.WhenThereIsWhitespaceAroundNestedLists_PreventBlankLinesWhenConvertingToMarkdownList.verified.md: -------------------------------------------------------------------------------- 1 | - OuterItem1 2 | 1. InnerItem1 3 | - Item2 4 | 1. InnerItem2 5 | - Item3 -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_Anchor_Text_with_Underscore_Do_Not_Escape.verified.md: -------------------------------------------------------------------------------- 1 | This a sample **paragraph** from [https://www.w3schools.com/html/mov_bbb.mp4](https://www.w3schools.com/html/mov_bbb.mp4) -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_BR_With_GitHubFlavored_Config_ThenConvertToGFM_BR.verified.md: -------------------------------------------------------------------------------- 1 | First part 2 | Second part -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_CodeContainsSpacesAndIsSurroundedByWhitespace_Should_NotRemoveSpaces.verified.md: -------------------------------------------------------------------------------- 1 | A JavaScript ` function ` ... -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_CodeContainsSpaces_ShouldPreserveSpaces.verified.md: -------------------------------------------------------------------------------- 1 | A JavaScript` function `... -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_CodeContainsSpanWithExtraSpaces_Should_NotNormalizeSpaces.verified.md: -------------------------------------------------------------------------------- 1 | A JavaScript` function `... -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_Consecutive_Em_Tags_Should_Convert_Properly.verified.md: -------------------------------------------------------------------------------- 1 | *block1* *block2* *block3* *block4* -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_Consecutive_Strong_Tags_Should_Convert_Properly.verified.md: -------------------------------------------------------------------------------- 1 | **block1** **block2** **block3** **block4** -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_Content_Contains_script_tags_ignore_it.verified.md: -------------------------------------------------------------------------------- 1 | simple paragraph -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_Converting_HTML_Ensure_To_Process_Only_Body.verified.md: -------------------------------------------------------------------------------- 1 | sample text -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_DescriptionListTag_ThenConvertToMarkdown_List.verified.md: -------------------------------------------------------------------------------- 1 | - Coffee 2 | - Filter Coffee 3 | - Hot Black Coffee 4 | - Milk 5 | - White Cold Drink -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_FencedCodeBlocks_Shouldnt_Have_Trailing_Line.verified.md: -------------------------------------------------------------------------------- 1 | ```xml 2 | InProcess 3 | ``` -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_Html_Containing_Nested_DIVs_Process_ONLY_Inner_Most_DIV.verified.md: -------------------------------------------------------------------------------- 1 | sample text -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_InlineCode_Shouldnt_Contain_Encoded_Chars.verified.md: -------------------------------------------------------------------------------- 1 | This is inline code: ``. -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_OrderedListIsInTable_LeaveListAsHtml.verified.md: -------------------------------------------------------------------------------- 1 | | Heading | 2 | | --- | 3 | |
  1. Item1
| -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_PRE_With_Confluence_Lang_Class_Att_And_GitHubFlavored_Config_ThenConvertToGFM_PRE.verified.md: -------------------------------------------------------------------------------- 1 | ```python 2 | var test = 'hello world'; 3 | ``` -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_PRE_With_GitHubFlavored_Config_ThenConvertToGFM_PRE.verified.md: -------------------------------------------------------------------------------- 1 | ``` 2 | var test = 'hello world'; 3 | ``` -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_PRE_With_Github_Site_DIV_Parent_And_GitHubFlavored_Config_ThenConvertToGFM_PRE.verified.md: -------------------------------------------------------------------------------- 1 | ```csharp 2 | var test = "hello world"; 3 | ``` -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_PRE_With_HighlightJs_Lang_Class_Att_And_GitHubFlavored_Config_ThenConvertToGFM_PRE.verified.md: -------------------------------------------------------------------------------- 1 | ```csharp 2 | var test = "hello world"; 3 | ``` -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_PRE_With_Lang_Highlight_Class_Att_And_GitHubFlavored_Config_ThenConvertToGFM_PRE.verified.md: -------------------------------------------------------------------------------- 1 | ```python 2 | var test = 'hello world'; 3 | ``` -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_PRE_With_Parent_DIV_And_Non_GitHubFlavored_Config_FirstLine_CodeBlock_SpaceIndent_Should_Be_Retained.verified.md: -------------------------------------------------------------------------------- 1 | var test = "hello world"; -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_PRE_Without_Lang_Marker_Class_Att_And_GitHubFlavored_Config_With_DefaultCodeBlockLanguage_ThenConvertToGFM_PRE.verified.md: -------------------------------------------------------------------------------- 1 | ```csharp 2 | var test = "hello world"; 3 | ``` -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_PreTag_Contains_IndentedFirstLine_Should_PreserveIndentation.verified.md: -------------------------------------------------------------------------------- 1 | function foo { -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_PreTag_Contains_IndentedFirstLine_Should_PreserveIndentation_GFM.verified.md: -------------------------------------------------------------------------------- 1 | ``` 2 | function foo { 3 | ``` -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_PreTag_Within_List_Should_Be_Indented.verified.md: -------------------------------------------------------------------------------- 1 | 1. Item1 2 | 2. Item2 3 | 4 | test 5 | test 6 | 3. Item3 -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_PreTag_Within_List_Should_Be_Indented_With_GitHub_FlavouredMarkdown.verified.md: -------------------------------------------------------------------------------- 1 | 1. Item1 2 | 2. Item2 3 | 4 | ``` 5 | test 6 | test 7 | ``` 8 | 3. Item3 -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_SingleChild_BlockTag_With_Parent_DIV_Ignore_Processing_DIV.verified.md: -------------------------------------------------------------------------------- 1 | sample text -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_Spaces_In_Inline_Tags_Should_Be_Retained.verified.md: -------------------------------------------------------------------------------- 1 | ... example html *code* block -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_Span_with_newline_Should_Convert_Properly.verified.md: -------------------------------------------------------------------------------- 1 | **2 sets** 2 | 30 mountain climbers -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_Strikethrough_And_Nested_Strikethrough.verified.md: -------------------------------------------------------------------------------- 1 | This is the 1~~st~~ sentence to t~~est the strikethrough tag conversion~~ -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_Sup_And_Nested_Sup.verified.md: -------------------------------------------------------------------------------- 1 | This is the 1^st^ sentence to t^es^t the sup tag conversion -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_SuppressNewlineFlag_PrefixDiv_Should_Be_Empty.md: -------------------------------------------------------------------------------- 1 | the 2 | fox 3 | jumps 4 | over -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_SuppressNewlineFlag_PrefixDiv_Should_Be_Empty.verified.md: -------------------------------------------------------------------------------- 1 | the 2 | fox 3 | jumps 4 | over -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_Table_Within_List_Should_Be_Indented.verified.md: -------------------------------------------------------------------------------- 1 | 1. Item1 2 | 2. Item2 3 | 4 | | col1 | col2 | col3 | 5 | | --- | --- | --- | 6 | | data1 | data2 | data3 | 7 | 3. Item3 -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_Tag_In_PassThoughTags_List_Then_Use_PassThroughConverter.verified.md: -------------------------------------------------------------------------------- 1 | This text has image alt. Next line of text -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_TextContainsAngleBrackets_HexEscapeAngleBrackets.verified.md: -------------------------------------------------------------------------------- 1 | Value = <Your text here> -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_TextIsHtmlEncoded_DecodeText.verified.md: -------------------------------------------------------------------------------- 1 | cat's -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_TextWithinParagraphContainsNewlineChars_ConvertNewlineCharsToSpace.verified.md: -------------------------------------------------------------------------------- 1 | This service will be temporarily unavailable due to planned maintenance from 02:00-04:00 on 30/01/2020 -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_Text_Contains_NewLineChars_Should_Not_Convert_To_BR.verified.md: -------------------------------------------------------------------------------- 1 | line 1 2 | line 2 -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_Text_Contains_NewLineChars_Should_Not_Convert_To_BR_GitHub_Flavoured.verified.md: -------------------------------------------------------------------------------- 1 | line 1 2 | line 2 -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.When_UnorderedListIsInTable_LeaveListAsHtml.verified.md: -------------------------------------------------------------------------------- 1 | | Heading | 2 | | --- | 3 | |
  • Item1
| -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ConverterTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using VerifyTests; 4 | using VerifyXunit; 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | 8 | namespace ReverseMarkdown.Test 9 | { 10 | public class ConverterTests 11 | { 12 | private readonly ITestOutputHelper _testOutputHelper; 13 | private readonly VerifySettings _verifySettings; 14 | 15 | public ConverterTests(ITestOutputHelper testOutputHelper) 16 | { 17 | _testOutputHelper = testOutputHelper; 18 | _verifySettings = new VerifySettings(); 19 | _verifySettings.DisableRequireUniquePrefix(); 20 | } 21 | 22 | [Fact] 23 | public Task WhenThereIsAsideTag() 24 | { 25 | var html = " This text appears after aside."; 26 | return CheckConversion(html); 27 | } 28 | 29 | [Fact] 30 | public Task WhenThereIsHtmlLink_ThenConvertToMarkdownLink() 31 | { 32 | var html = @"This is
a link"; 33 | return CheckConversion(html); 34 | } 35 | 36 | [Fact] 37 | public Task WhenThereIsHtmlLinkWithTitle_ThenConvertToMarkdownLink() 38 | { 39 | var html = @"This is a link"; 40 | return CheckConversion(html); 41 | } 42 | 43 | [Fact] 44 | public Task WhenThereAreMultipleLinks_ThenConvertThemToMarkdownLinks() 45 | { 46 | var html = 47 | @"This is first link and second link"; 48 | return CheckConversion(html); 49 | } 50 | 51 | [Fact] 52 | public Task WhenThereIsHtmlLinkNotWhitelisted_ThenBypass() 53 | { 54 | var html = 55 | @"Leave http, https, ftp, ftps, file. Remove data, tel and whatever"; 56 | return CheckConversion(html, new Config 57 | { 58 | WhitelistUriSchemes = new[] {"http", "https", "ftp", "ftps", "file"} 59 | }); 60 | } 61 | 62 | [Fact] 63 | public Task WhenThereHtmlWithHrefAndNoSchema_WhitelistedEmptyString_ThenConvertToMarkdown() 64 | { 65 | return CheckConversion( 66 | html: @"yeah", 67 | config: new Config 68 | { 69 | WhitelistUriSchemes = new[] {""} 70 | } 71 | ); 72 | } 73 | 74 | [Fact] 75 | public Task WhenThereHtmlWithHrefAndNoSchema_NotWhitelisted_ThenConvertToPlain() 76 | { 77 | return CheckConversion( 78 | html: @"yeah", 79 | config: new Config 80 | { 81 | WhitelistUriSchemes = new[] {"whatever"} 82 | } 83 | ); 84 | } 85 | 86 | [Fact] 87 | public Task WhenThereIsHtmlLinkWithDisallowedCharsInChildren_ThenEscapeTextInMarkdown() 88 | { 89 | return CheckConversion( 90 | html: @"this ]( might break things", 91 | config: new Config 92 | { 93 | SmartHrefHandling = true 94 | } 95 | ); 96 | } 97 | 98 | [Fact] 99 | public Task WhenThereIsHtmlLinkWithParensInHref_ThenEscapeHrefInMarkdown() 100 | { 101 | return CheckConversion( 102 | html: @"link", 103 | config: new Config 104 | { 105 | SmartHrefHandling = true 106 | } 107 | ); 108 | } 109 | 110 | [Fact] 111 | public Task WhenThereIsHtmlWithProtocolRelativeUrlHrefAndNameNotMatching_SmartHandling_ThenConvertToMarkdown() 112 | { 113 | return CheckConversion( 114 | html: @"example.com", 115 | config: new Config 116 | { 117 | SmartHrefHandling = true 118 | } 119 | ); 120 | } 121 | 122 | [Fact] 123 | public Task WhenThereIsHtmlWithHrefAndNameNotMatching_SmartHandling_ThenConvertToMarkdown() 124 | { 125 | return CheckConversion( 126 | html: @"Something intact", 127 | config: new Config 128 | { 129 | SmartHrefHandling = true 130 | } 131 | ); 132 | } 133 | 134 | [Fact] 135 | public Task WhenThereIsHtmlWithHrefAndNameMatching_SmartHandling_ThenConvertToPlain() 136 | { 137 | return CheckConversion( 138 | html: @"http://example.com/abc?x", 139 | config: new Config 140 | { 141 | SmartHrefHandling = true 142 | } 143 | ); 144 | } 145 | 146 | [Fact] 147 | public void WhenThereIsHtmlWithHttpSchemeAndNameWithoutScheme_SmartHandling_ThenConvertToPlain() 148 | { 149 | var config = new Config() 150 | { 151 | SmartHrefHandling = true 152 | }; 153 | var converter = new Converter(config); 154 | var result = converter.Convert(@"example.com"); 155 | Assert.Equal("http://example.com", result, StringComparer.OrdinalIgnoreCase); 156 | 157 | var result1 = converter.Convert(@"example.com"); 158 | Assert.Equal("https://example.com", result1, StringComparer.OrdinalIgnoreCase); 159 | } 160 | 161 | [Fact] 162 | public Task WhenThereIsHtmlWithMailtoSchemeAndNameWithoutScheme_SmartHandling_ThenConvertToPlain() 163 | { 164 | return CheckConversion( 165 | html: @"george@example.com", 166 | config: new Config 167 | { 168 | SmartHrefHandling = true 169 | } 170 | ); 171 | } 172 | 173 | [Fact] 174 | public Task WhenThereIsHtmlWithTelSchemeAndNameWithoutScheme_SmartHandling_ThenConvertToPlain() 175 | { 176 | return CheckConversion( 177 | html: @"+1123-45678", 178 | config: new Config 179 | { 180 | SmartHrefHandling = true 181 | } 182 | ); 183 | } 184 | 185 | [Fact] 186 | public void WhenThereIsHtmlLinkWithHttpSchemaAndNameWithout_SmartHandling_ThenOutputOnlyHref() 187 | { 188 | var config = new Config 189 | { 190 | SmartHrefHandling = true 191 | }; 192 | var converter = new Converter(config); 193 | var result = converter.Convert(@"example.com"); 194 | Assert.Equal("http://example.com", result, StringComparer.OrdinalIgnoreCase); 195 | var result1 = converter.Convert(@"example.com"); 196 | Assert.Equal("https://example.com", result1, StringComparer.OrdinalIgnoreCase); 197 | } 198 | 199 | [Fact] 200 | public void WhenThereIsHtmlNonWellFormedLinkLink_SmartHandling_ThenConvertToMarkdown() 201 | { 202 | var config = new Config 203 | { 204 | SmartHrefHandling = true 205 | }; 206 | 207 | //The string is not correctly escaped. 208 | var converter = new Converter(config); 209 | var result = 210 | converter.Convert( 211 | @"http://example.com/path/file name.docx"); 212 | Assert.Equal("[http://example.com/path/file name.docx](http://example.com/path/file%20name.docx)", result, 213 | StringComparer.OrdinalIgnoreCase); 214 | 215 | //The string is an absolute Uri that represents an implicit file Uri. 216 | var result1 = converter.Convert(@" c:\\directory\filename"); 217 | Assert.Equal(@"[c:\\directory\filename](c:\\directory\filename)", result1, 218 | StringComparer.OrdinalIgnoreCase); 219 | 220 | //The string is an absolute URI that is missing a slash before the path. 221 | var result2 = 222 | converter.Convert(@"file://c:/directory/filename"); 223 | Assert.Equal("[file://c:/directory/filename](file://c:/directory/filename)", result2, 224 | StringComparer.OrdinalIgnoreCase); 225 | 226 | //The string contains unescaped backslashes even if they are treated as forward slashes. 227 | var result3 = converter.Convert(@"http:\\host/path/file"); 228 | Assert.Equal(@"[http:\\host/path/file](http:\\host/path/file)", result3, StringComparer.OrdinalIgnoreCase); 229 | } 230 | 231 | [Fact] 232 | public Task WhenThereIsHtmlLinkWithoutHttpSchemaAndNameWithoutScheme_SmartHandling_ThenConvertToMarkdown() 233 | { 234 | return CheckConversion( 235 | html: @"example.com", 236 | config: new Config 237 | { 238 | SmartHrefHandling = true 239 | } 240 | ); 241 | } 242 | 243 | [Fact] 244 | public Task WhenThereAreStrongTag_ThenConvertToMarkdownDoubleAsterisks() 245 | { 246 | var html = "This paragraph contains bold text"; 247 | return CheckConversion(html); 248 | } 249 | 250 | [Fact] 251 | public Task WhenThereAreBTag_ThenConvertToMarkdownDoubleAsterisks() 252 | { 253 | var html = "This paragraph contains bold text"; 254 | return CheckConversion(html); 255 | } 256 | 257 | [Fact] 258 | public Task 259 | WhenThereIsEncompassingStrongOrBTag_ThenConvertToMarkdownDoubleAsterisks_AnyStrongOrBTagsInsideAreIgnored() 260 | { 261 | var html = 262 | "Paragraph is encompassed with strong tag and also has bold text words within it"; 263 | return CheckConversion(html); 264 | } 265 | 266 | [Fact] 267 | public Task WhenThereIsSingleAsteriskInText_ThenConvertToMarkdownEscapedAsterisk() 268 | { 269 | var html = "This is a sample(*) paragraph"; 270 | return CheckConversion(html); 271 | } 272 | 273 | [Fact] 274 | public Task WhenThereIsEmTag_ThenConvertToMarkdownSingleAsterisks() 275 | { 276 | var html = "This is a sample paragraph"; 277 | return CheckConversion(html); 278 | } 279 | 280 | [Fact] 281 | public Task WhenThereIsITag_ThenConvertToMarkdownSingleAsterisks() 282 | { 283 | var html = "This is a sample paragraph"; 284 | return CheckConversion(html); 285 | } 286 | 287 | [Fact] 288 | public Task WhenThereIsEncompassingEmOrITag_ThenConvertToMarkdownSingleAsterisks_AnyEmOrITagsInsideAreIgnored() 289 | { 290 | var html = "This is a sample paragraph"; 291 | return CheckConversion(html); 292 | } 293 | 294 | [Fact] 295 | public Task WhenThereIsBreakTag_ThenConvertToMarkdownDoubleSpacesCarriageReturn() 296 | { 297 | var html = "This is a paragraph.
This line appears after break."; 298 | return CheckConversion(html); 299 | } 300 | 301 | [Fact] 302 | public Task WhenThereIsCodeTag_ThenConvertToMarkdownWithBackTick() 303 | { 304 | var html = "This text has code alert();"; 305 | return CheckConversion(html); 306 | } 307 | 308 | [Fact] 309 | public Task WhenThereIsH1Tag_ThenConvertToMarkdownHeader() 310 | { 311 | var html = "This text has

header

. This text appear after header."; 312 | return CheckConversion(html); 313 | } 314 | 315 | [Fact] 316 | public Task WhenThereIsH2Tag_ThenConvertToMarkdownHeader() 317 | { 318 | var html = "This text has

header

. This text appear after header."; 319 | return CheckConversion(html); 320 | } 321 | 322 | [Fact] 323 | public Task WhenThereIsH3Tag_ThenConvertToMarkdownHeader() 324 | { 325 | var html = "This text has

header

. This text appear after header."; 326 | return CheckConversion(html); 327 | } 328 | 329 | [Fact] 330 | public Task WhenThereIsH4Tag_ThenConvertToMarkdownHeader() 331 | { 332 | var html = "This text has

header

. This text appear after header."; 333 | return CheckConversion(html); 334 | } 335 | 336 | [Fact] 337 | public Task WhenThereIsH5Tag_ThenConvertToMarkdownHeader() 338 | { 339 | var html = "This text has
header
. This text appear after header."; 340 | return CheckConversion(html); 341 | } 342 | 343 | [Fact] 344 | public Task WhenThereIsH6Tag_ThenConvertToMarkdownHeader() 345 | { 346 | var html = "This text has
header
. This text appear after header."; 347 | return CheckConversion(html); 348 | } 349 | 350 | [Fact] 351 | public Task WhenThereIsHeadingInsideTable_ThenIgnoreHeadingLevel() 352 | { 353 | var html = 354 | $"{Environment.NewLine}{Environment.NewLine}{Environment.NewLine}

Heading text

Content
"; 355 | return CheckConversion(html); 356 | } 357 | 358 | [Fact] 359 | public Task WhenThereIsBlockquoteTag_ThenConvertToMarkdownBlockquote() 360 | { 361 | var html = "This text has
blockquote
. This text appear after header."; 362 | return CheckConversion(html); 363 | } 364 | 365 | [Fact] 366 | public Task WhenThereIsEmptyBlockquoteTag_ThenConvertToMarkdownBlockquote() 367 | { 368 | var html = "This text has
. This text appear after header."; 369 | return CheckConversion(html); 370 | } 371 | 372 | [Fact] 373 | public Task WhenThereIsParagraphTag_ThenConvertToMarkdownDoubleLineBreakBeforeAndAfter() 374 | { 375 | var html = "This text has markup

paragraph.

Next line of text"; 376 | return CheckConversion(html); 377 | } 378 | 379 | [Fact] 380 | public Task WhenThereIsHorizontalRule_ThenConvertToMarkdownHorizontalRule() 381 | { 382 | var html = "This text has horizontal rule.
Next line of text"; 383 | return CheckConversion(html); 384 | } 385 | 386 | [Fact] 387 | public Task WhenThereIsImgTag_ThenConvertToMarkdownImage() 388 | { 389 | var html = 390 | @"This text has image . Next line of text"; 391 | return CheckConversion(html); 392 | } 393 | 394 | [Fact] 395 | public Task WhenThereIsImgTagWithoutTitle_ThenConvertToMarkdownImageWithoutTitle() 396 | { 397 | var html = 398 | @"This text has image . Next line of text"; 399 | return CheckConversion(html); 400 | } 401 | 402 | [Fact] 403 | public Task WhenThereIsImgTagWithoutAltText_ThenConvertToMarkdownImageWithoutAltText() 404 | { 405 | var html = 406 | @"This text has image . Next line of text"; 407 | return CheckConversion(html); 408 | } 409 | 410 | [Fact] 411 | public Task WhenThereIsImgTagWithMultilineAltText_ThenEnsureNoBlankLinesInMarkdownAltText() 412 | { 413 | var html = 414 | $@"This text has image . Next line of text"; 415 | return CheckConversion(html); 416 | } 417 | 418 | [Fact] 419 | public Task WhenThereIsImgTagWithBracesInAltText_ThenEnsureAltTextIsEscapedInMarkdown() 420 | { 421 | var html = 422 | @"This text has image . Next line of text"; 423 | return CheckConversion(html); 424 | } 425 | 426 | [Fact] 427 | public Task WhenThereIsImgTag_SchemeNotWhitelisted_ThenEmptyOutput() 428 | { 429 | return CheckConversion( 430 | html: @"", 431 | config: new Config 432 | { 433 | WhitelistUriSchemes = new[] {"http"} 434 | } 435 | ); 436 | } 437 | 438 | [Fact] 439 | public Task WhenThereIsImgTag_SchemeIsWhitelisted_ThenConvertToMarkdown() 440 | { 441 | return CheckConversion( 442 | html: @"", 443 | config: new Config() 444 | { 445 | WhitelistUriSchemes = new[] {"data"} 446 | } 447 | ); 448 | } 449 | 450 | [Fact] 451 | public Task WhenThereIsImgTagAndSrcWithNoSchema_WhitelistedEmptyString_ThenConvertToMarkdown() 452 | { 453 | return CheckConversion( 454 | html: @"", 455 | config: new Config() 456 | { 457 | WhitelistUriSchemes = new[] {""} 458 | } 459 | ); 460 | } 461 | 462 | [Fact] 463 | public Task WhenThereIsImgTagAndSrcWithNoSchema_NotWhitelisted_ThenConvertToPlain() 464 | { 465 | return CheckConversion( 466 | html: @"", 467 | config: new Config() 468 | { 469 | WhitelistUriSchemes = new[] {"whatever"} 470 | } 471 | ); 472 | } 473 | 474 | [Fact] 475 | public Task WhenThereIsImgTagWithRelativeUrl_NotWhitelisted_ThenConvertToMarkdown() 476 | { 477 | return CheckConversion( 478 | html: @"", 479 | config: new Config() 480 | { 481 | WhitelistUriSchemes = new[] {"data"} 482 | } 483 | ); 484 | } 485 | 486 | [Fact] 487 | public Task WhenThereIsImgTagWithUnixUrl_ConfigHasWhitelist_ThenConvertToMarkdown() 488 | { 489 | return CheckConversion( 490 | html: @"", 491 | config: new Config() 492 | { 493 | WhitelistUriSchemes = new[] {"file"} 494 | } 495 | ); 496 | } 497 | 498 | [Fact] 499 | public Task WhenThereIsImgTagWithHttpProtocolRelativeUrl_ConfigHasWhitelist_ThenConvertToMarkdown() 500 | { 501 | var html = @""; 502 | var config = new Config 503 | { 504 | WhitelistUriSchemes = new[] {"http"} 505 | }; 506 | return CheckConversion(html, config); 507 | } 508 | 509 | [Fact] 510 | public Task WhenThereIsPreTag_ThenConvertToMarkdownPre() 511 | { 512 | var html = "This text has pre tag content
Predefined text
Next line of text"; 513 | return CheckConversion(html); 514 | } 515 | 516 | [Fact] 517 | public Task WhenThereIsEmptyPreTag_ThenConvertToMarkdownPre() 518 | { 519 | var html = "This text has pre tag content

Next line of text"; 520 | return CheckConversion(html); 521 | } 522 | 523 | [Fact] 524 | public Task WhenThereIsEmptyPreTag_ThenConvertToMarkdownPre_GFM() 525 | { 526 | var html = "This text has pre tag content

Next line of text"; 527 | return CheckConversion(html, new Config {GithubFlavored = true}); 528 | } 529 | 530 | [Fact] 531 | public Task WhenThereIsUnorderedList_ThenConvertToMarkdownList() 532 | { 533 | var html = "This text has unordered list."; 534 | return CheckConversion(html); 535 | } 536 | 537 | [Fact] 538 | public Task WhenThereIsUnorderedListAndBulletIsAsterisk_ThenConvertToMarkdownList() 539 | { 540 | var html = "This text has unordered list."; 541 | return CheckConversion(html, new Config {ListBulletChar = '*'}); 542 | } 543 | 544 | [Fact] 545 | public Task WhenThereIsOrderedList_ThenConvertToMarkdownList() 546 | { 547 | var html = "This text has ordered list.
  1. Item1
  2. Item2
"; 548 | return CheckConversion(html); 549 | } 550 | 551 | [Fact] 552 | public Task WhenThereIsOrderedListWithNestedUnorderedList_ThenConvertToMarkdownListWithNestedList() 553 | { 554 | var html = 555 | "This text has ordered list.
  1. OuterItem1
    • InnerItem1
    • InnerItem2
  2. Item2
"; 556 | return CheckConversion(html); 557 | } 558 | 559 | [Fact] 560 | public Task WhenThereIsUnorderedListWithNestedOrderedList_ThenConvertToMarkdownListWithNestedList() 561 | { 562 | var html = 563 | "This text has ordered list."; 564 | return CheckConversion(html); 565 | } 566 | 567 | [Fact] 568 | public Task WhenThereIsWhitespaceAroundNestedLists_PreventBlankLinesWhenConvertingToMarkdownList() 569 | { 570 | var html = $""; 578 | 579 | return CheckConversion(html); 580 | } 581 | 582 | [Fact] 583 | public Task 584 | WhenListItemTextContainsLeadingAndTrailingSpacesAndTabs_ThenConvertToMarkdownListItemWithSpacesAndTabsStripped() 585 | { 586 | var html = @"
  1. This is a text with leading and trailing spaces and tabs
"; 587 | return CheckConversion(html); 588 | } 589 | 590 | [Fact] 591 | public Task WhenListContainsNewlineAndTabBetweenTagBorders_CleanupAndConvertToMarkdown() 592 | { 593 | var html = 594 | $"
    {Environment.NewLine}\t
  1. {Environment.NewLine}\t\tItem1
  2. {Environment.NewLine}\t
  3. {Environment.NewLine}\t\tItem2
"; 595 | return CheckConversion(html); 596 | } 597 | 598 | [Fact] 599 | public Task WhenListContainsMultipleParagraphs_ConvertToMarkdownAndIndentSiblings() 600 | { 601 | var html = 602 | @"
    603 |
  1. 604 |

    Paragraph 1

    605 |

    Paragraph 1.1

    606 |

    Paragraph 1.2

  2. 607 |
  3. 608 |

    Paragraph 3

"; 609 | 610 | return CheckConversion(html); 611 | } 612 | 613 | [Fact] 614 | public Task WhenListContainsParagraphsOutsideItems_ConvertToMarkdownAndIndentSiblings() 615 | { 616 | var html = 617 | @"
    618 |
  1. Item1
  2. 619 |

    Item 1 additional info

    620 |
  3. Item2
  4. 621 |
"; 622 | 623 | return CheckConversion(html); 624 | } 625 | 626 | [Fact] 627 | public Task When_OrderedListIsInTable_LeaveListAsHtml() 628 | { 629 | var html = "
Heading
  1. Item1
"; 630 | return CheckConversion(html); 631 | } 632 | 633 | [Fact] 634 | public Task When_UnorderedListIsInTable_LeaveListAsHtml() 635 | { 636 | var html = "
Heading
  • Item1
"; 637 | return CheckConversion(html); 638 | } 639 | 640 | [Fact] 641 | public Task Check_Converter_With_Unknown_Tag_ByPass_Option() 642 | { 643 | var html = "text in unknown tag"; 644 | var config = new Config 645 | { 646 | UnknownTags = Config.UnknownTagsOption.Bypass 647 | }; 648 | return CheckConversion(html, config); 649 | } 650 | 651 | [Fact] 652 | public Task WhenStyletagWithBypassOption_ReturnEmpty() 653 | { 654 | var html = @""; 655 | var config = new Config() 656 | { 657 | UnknownTags = Config.UnknownTagsOption.Bypass 658 | }; 659 | return CheckConversion(html, config); 660 | } 661 | 662 | [Fact] 663 | public Task Check_Converter_With_Unknown_Tag_Drop_Option() 664 | { 665 | var html = "text in unknown tag

paragraph text

"; 666 | var config = new Config 667 | { 668 | UnknownTags = Config.UnknownTagsOption.Drop 669 | }; 670 | return CheckConversion(html, config); 671 | } 672 | 673 | [Fact] 674 | public Task Check_Converter_With_Unknown_Tag_PassThrough_Option() 675 | { 676 | var html = "text in unknown tag

paragraph text

"; 677 | var config = new Config 678 | { 679 | UnknownTags = Config.UnknownTagsOption.PassThrough 680 | }; 681 | return CheckConversion(html, config); 682 | } 683 | 684 | [Fact] 685 | public Task Check_Converter_With_Unknown_Tag_Raise_Option() 686 | { 687 | var html = "text in unknown tag

paragraph text

"; 688 | var config = new Config 689 | { 690 | UnknownTags = Config.UnknownTagsOption.Raise 691 | }; 692 | var converter = new Converter(config); 693 | return Verifier.Throws(() => converter.Convert(html), settings: _verifySettings) 694 | .IgnoreMember(e => e.StackTrace); 695 | } 696 | 697 | [Fact] 698 | public Task WhenTable_ThenConvertToGFMTable() 699 | { 700 | var html = 701 | "
col1col2col3
data1data2data3
"; 702 | 703 | var config = new Config 704 | { 705 | UnknownTags = Config.UnknownTagsOption.Bypass 706 | }; 707 | return CheckConversion(html, config); 708 | } 709 | 710 | [Fact] 711 | public Task 712 | WhenTable_WithoutHeaderRow_With_TableWithoutHeaderRowHandlingOptionEmptyRow_ThenConvertToGFMTable_WithEmptyHeaderRow() 713 | { 714 | var html = 715 | "
data1data2data3
data4data5data6
"; 716 | var config = new Config 717 | { 718 | UnknownTags = Config.UnknownTagsOption.Bypass, 719 | TableWithoutHeaderRowHandling = Config.TableWithoutHeaderRowHandlingOption.EmptyRow 720 | }; 721 | return CheckConversion(html, config); 722 | } 723 | 724 | [Fact] 725 | public Task 726 | WhenTable_WithoutHeaderRow_With_TableWithoutHeaderRowHandlingOptionDefault_ThenConvertToGFMTable_WithFirstRowAsHeaderRow() 727 | { 728 | var html = 729 | "
data1data2data3
data4data5data6
"; 730 | var config = new Config 731 | { 732 | UnknownTags = Config.UnknownTagsOption.Bypass, 733 | // TableWithoutHeaderRowHandling = Config.TableWithoutHeaderRowHandlingOption.Default - this is default 734 | }; 735 | return CheckConversion(html, config); 736 | } 737 | 738 | [Fact] 739 | public Task WhenTable_Cell_Content_WithNewline_Add_BR_ThenConvertToGFMTable() 740 | { 741 | var html = 742 | $"
col1col2col3
data line1{Environment.NewLine}line2data2data3
"; 743 | var config = new Config 744 | { 745 | UnknownTags = Config.UnknownTagsOption.Bypass 746 | }; 747 | return CheckConversion(html, config); 748 | } 749 | 750 | [Fact] 751 | public Task WhenTable_CellContainsParagraph_AddBrThenConvertToGFMTable() 752 | { 753 | var html = "
col1

line1

line2

"; 754 | return CheckConversion(html); 755 | } 756 | 757 | [Fact] 758 | public Task WhenTable_ContainsTheadTh_ConvertToGFMTable() 759 | { 760 | var html = 761 | "
col1col2
data1data2
"; 762 | var config = new Config 763 | { 764 | GithubFlavored = true, 765 | }; 766 | return CheckConversion(html, config); 767 | } 768 | 769 | [Fact] 770 | public Task WhenTable_ContainsTheadTd_ConvertToGFMTable() 771 | { 772 | var html = 773 | "
col1col2
data1data2
"; 774 | var config = new Config 775 | { 776 | GithubFlavored = true, 777 | }; 778 | return CheckConversion(html, config); 779 | } 780 | 781 | [Fact] 782 | public Task WhenTable_CellContainsBr_PreserveBrAndConvertToGFMTable() 783 | { 784 | var html = "
col1
line 1
line 2
"; 785 | var config = new Config 786 | { 787 | GithubFlavored = true, 788 | }; 789 | return CheckConversion(html, config); 790 | } 791 | 792 | [Fact] 793 | public Task WhenTable_HasEmptyRow_DropsEmptyRow() 794 | { 795 | var html = "
abc
"; 796 | var config = new Config 797 | { 798 | GithubFlavored = true, 799 | TableWithoutHeaderRowHandling = Config.TableWithoutHeaderRowHandlingOption.EmptyRow, 800 | }; 801 | return CheckConversion(html, config); 802 | } 803 | 804 | [Fact] 805 | public Task When_BR_With_GitHubFlavored_Config_ThenConvertToGFM_BR() 806 | { 807 | var html = "First part
Second part"; 808 | var config = new Config 809 | { 810 | GithubFlavored = true 811 | }; 812 | return CheckConversion(html, config); 813 | } 814 | 815 | [Fact] 816 | public Task When_PRE_With_GitHubFlavored_Config_ThenConvertToGFM_PRE() 817 | { 818 | var html = "
var test = 'hello world';
"; 819 | var config = new Config 820 | { 821 | GithubFlavored = true 822 | }; 823 | return CheckConversion(html, config); 824 | } 825 | 826 | [Fact] 827 | public Task When_PRE_With_Confluence_Lang_Class_Att_And_GitHubFlavored_Config_ThenConvertToGFM_PRE() 828 | { 829 | var html = @"
var test = 'hello world';
"; 830 | var config = new Config 831 | { 832 | GithubFlavored = true 833 | }; 834 | return CheckConversion(html, config); 835 | } 836 | 837 | [Fact] 838 | public Task When_PRE_With_Github_Site_DIV_Parent_And_GitHubFlavored_Config_ThenConvertToGFM_PRE() 839 | { 840 | var html = @"
var test = ""hello world"";
"; 841 | var config = new Config 842 | { 843 | GithubFlavored = true 844 | }; 845 | return CheckConversion(html, config); 846 | } 847 | 848 | [Fact] 849 | public Task When_PRE_With_HighlightJs_Lang_Class_Att_And_GitHubFlavored_Config_ThenConvertToGFM_PRE() 850 | { 851 | var html = @"
var test = ""hello world"";
"; 852 | var config = new Config 853 | { 854 | GithubFlavored = true 855 | }; 856 | return CheckConversion(html, config); 857 | } 858 | 859 | [Fact] 860 | public Task When_PRE_With_Lang_Highlight_Class_Att_And_GitHubFlavored_Config_ThenConvertToGFM_PRE() 861 | { 862 | var html = @"
var test = 'hello world';
"; 863 | var config = new Config 864 | { 865 | GithubFlavored = true 866 | }; 867 | return CheckConversion(html, config); 868 | } 869 | 870 | [Fact] 871 | public Task WhenRemovedCommentsIsEnabled_CommentsAreRemoved() 872 | { 873 | var html = 874 | "Hello there content

"; 1049 | 1050 | var config = new Config 1051 | { 1052 | RemoveComments = true 1053 | }; 1054 | return CheckConversion(html, config); 1055 | } 1056 | 1057 | [Fact] 1058 | public Task WhenBoldTagContainsBRTag_ThenConvertToMarkdown() 1059 | { 1060 | var html = "test
test
"; 1061 | return CheckConversion(html); 1062 | } 1063 | 1064 | [Fact] 1065 | public Task WhenAnchorTagContainsImgTag_LinkTextShouldNotBeEscaped() 1066 | { 1067 | var html = ""; 1068 | return CheckConversion(html); 1069 | } 1070 | 1071 | [Fact] 1072 | public Task 1073 | When_PRE_Without_Lang_Marker_Class_Att_And_GitHubFlavored_Config_With_DefaultCodeBlockLanguage_ThenConvertToGFM_PRE() 1074 | { 1075 | var html = @"
var test = ""hello world"";
"; 1076 | var config = new Config 1077 | { 1078 | GithubFlavored = true, 1079 | DefaultCodeBlockLanguage = "csharp" 1080 | }; 1081 | return CheckConversion(html, config); 1082 | } 1083 | 1084 | [Fact] 1085 | public Task 1086 | When_PRE_With_Parent_DIV_And_Non_GitHubFlavored_Config_FirstLine_CodeBlock_SpaceIndent_Should_Be_Retained() 1087 | { 1088 | var html = @"
var test = ""hello world"";
"; 1089 | return CheckConversion(html); 1090 | } 1091 | 1092 | [Fact] 1093 | public Task When_Converting_HTML_Ensure_To_Process_Only_Body() 1094 | { 1095 | var html = 1096 | "sample text"; 1097 | return CheckConversion(html); 1098 | } 1099 | 1100 | [Fact] 1101 | public Task When_Html_Containing_Nested_DIVs_Process_ONLY_Inner_Most_DIV() 1102 | { 1103 | var html = "
sample text
"; 1104 | return CheckConversion(html); 1105 | } 1106 | 1107 | [Fact] 1108 | public Task When_SingleChild_BlockTag_With_Parent_DIV_Ignore_Processing_DIV() 1109 | { 1110 | var html = "

sample text

"; 1111 | return CheckConversion(html); 1112 | } 1113 | 1114 | [Fact] 1115 | public Task When_Table_Within_List_Should_Be_Indented() 1116 | { 1117 | var html = 1118 | "
  1. Item1
  2. Item2
    col1col2col3
    data1data2data3
  3. Item3
"; 1119 | return CheckConversion(html); 1120 | } 1121 | 1122 | [Fact] 1123 | public Task When_Tag_In_PassThoughTags_List_Then_Use_PassThroughConverter() 1124 | { 1125 | var html = 1126 | @"This text has image . Next line of text"; 1127 | var config = new Config 1128 | { 1129 | PassThroughTags = new[] {"img"} 1130 | }; 1131 | return CheckConversion(html, config); 1132 | } 1133 | 1134 | [Fact] 1135 | public Task When_CodeContainsSpaces_ShouldPreserveSpaces() 1136 | { 1137 | var html = "A JavaScript function ..."; 1138 | return CheckConversion(html); 1139 | } 1140 | 1141 | [Fact] 1142 | public Task When_CodeContainsSpanWithExtraSpaces_Should_NotNormalizeSpaces() 1143 | { 1144 | var html = "A JavaScript function ..."; 1145 | return CheckConversion(html); 1146 | } 1147 | 1148 | 1149 | [Fact] 1150 | public Task When_CodeContainsSpacesAndIsSurroundedByWhitespace_Should_NotRemoveSpaces() 1151 | { 1152 | var html = "A JavaScript function ..."; 1153 | return CheckConversion(html); 1154 | } 1155 | 1156 | [Fact] 1157 | public Task When_PreTag_Contains_IndentedFirstLine_Should_PreserveIndentation() 1158 | { 1159 | var html = "
    function foo {
"; 1160 | return CheckConversion(html); 1161 | } 1162 | 1163 | [Fact] 1164 | public Task When_PreTag_Contains_IndentedFirstLine_Should_PreserveIndentation_GFM() 1165 | { 1166 | var html = "
    function foo {
"; 1167 | 1168 | var config = new Config 1169 | { 1170 | GithubFlavored = true 1171 | }; 1172 | return CheckConversion(html, config); 1173 | } 1174 | 1175 | [Fact] 1176 | public Task When_PreTag_Within_List_Should_Be_Indented() 1177 | { 1178 | var html = 1179 | $"
  1. Item1
  2. Item2
     test
    {Environment.NewLine} test
  3. Item3
"; 1180 | return CheckConversion(html); 1181 | } 1182 | 1183 | [Fact] 1184 | public Task When_PreTag_Within_List_Should_Be_Indented_With_GitHub_FlavouredMarkdown() 1185 | { 1186 | var html = 1187 | $"
  1. Item1
  2. Item2
     test
    {Environment.NewLine} test
  3. Item3
"; 1188 | 1189 | var config = new Config 1190 | { 1191 | GithubFlavored = true 1192 | }; 1193 | return CheckConversion(html, config); 1194 | } 1195 | 1196 | [Fact] 1197 | public Task When_Text_Contains_NewLineChars_Should_Not_Convert_To_BR() 1198 | { 1199 | var html = "

line 1
line 2
"; 1200 | return CheckConversion(html); 1201 | } 1202 | 1203 | [Fact] 1204 | public Task When_Text_Contains_NewLineChars_Should_Not_Convert_To_BR_GitHub_Flavoured() 1205 | { 1206 | var html = "

line 1
line 2
"; 1207 | return CheckConversion(html, new Config 1208 | { 1209 | GithubFlavored = true 1210 | }); 1211 | } 1212 | 1213 | [Fact] 1214 | public Task When_Consecutive_Strong_Tags_Should_Convert_Properly() 1215 | { 1216 | var html = "block1block2block3block4"; 1217 | return CheckConversion(html); 1218 | } 1219 | 1220 | [Fact] 1221 | public Task When_Consecutive_Em_Tags_Should_Convert_Properly() 1222 | { 1223 | var html = "block1block2block3block4"; 1224 | return CheckConversion(html); 1225 | } 1226 | 1227 | [Fact] 1228 | public Task Li_With_No_Parent() 1229 | { 1230 | var html = "

  • item
  • "; 1231 | return CheckConversion(html); 1232 | } 1233 | 1234 | [Fact] 1235 | public Task When_Span_with_newline_Should_Convert_Properly() 1236 | { 1237 | var html = $"2 sets{Environment.NewLine}30 mountain climbers"; 1238 | return CheckConversion(html); 1239 | } 1240 | 1241 | [Fact] 1242 | public Task Bug255_table_newline_char_issue() 1243 | { 1244 | var html = 1245 | $"{Environment.NewLine}{Environment.NewLine}Progression{Environment.NewLine}Focus{Environment.NewLine}{Environment.NewLine}"; 1246 | return CheckConversion(html); 1247 | } 1248 | 1249 | [Fact] 1250 | public Task When_Content_Contains_script_tags_ignore_it() 1251 | { 1252 | var html = 1253 | $"

    simple paragraph

    "; 1254 | return CheckConversion(html); 1255 | } 1256 | 1257 | [Fact] 1258 | public Task When_DescriptionListTag_ThenConvertToMarkdown_List() 1259 | { 1260 | var html = 1261 | "
    Coffee
    Filter Coffee
    Hot Black Coffee
    Milk
    White Cold Drink
    "; 1262 | return CheckConversion(html); 1263 | } 1264 | 1265 | [Fact] 1266 | public Task Bug294_Table_bug_with_row_superfluous_newlines() 1267 | { 1268 | var html = @" 1269 | 1270 | 1271 | 1272 | 1273 | 1274 | 1275 | 1276 | 1277 | 1278 | 1279 | 1280 | 1281 | 1282 | 1283 | 1284 | 1285 | 1286 | 1287 | 1288 | 1289 | 1290 | 1291 | 1292 | 1293 | 1294 | 1295 | 1296 | 1297 | 1298 | 1299 | 1300 | 1301 | 1302 | 1303 |
    比较wordpresshexo & hugo
    搭建要求一台服务器以及运行环境静态生成页面,无需服务器。
    性能由于是动态生成页面,可以通过自行配置提高性能,但是仍然无法媲美静态页面几乎无需考虑性能问题
    访问速度依赖于服务器配置以及cdn加速。只需考虑cdn加速
    功能完善作为强大的cms功能很完善,需要的功能基本可以插件下载直接实现。额外功能也可以通过插件实现,不过稍微需要自行查找以及diy
    后台管理现成的后台管理功能,开箱即用由于静态博客,本身没有后台管理,有需求需要自行搜索实现
    "; 1304 | 1305 | return CheckConversion(html); 1306 | } 1307 | 1308 | [Fact] 1309 | public Task WhenTableHeadingWithAlignmentStyles_ThenTableHeaderShouldHaveProperAlignment() 1310 | { 1311 | var html = 1312 | $"
    Col1Col2Col2
    123
    "; 1313 | return CheckConversion(html); 1314 | } 1315 | 1316 | [Fact] 1317 | public Task When_Sup_And_Nested_Sup() 1318 | { 1319 | var html = $"This is the 1st sentence to test the sup tag conversion"; 1320 | return CheckConversion(html); 1321 | } 1322 | 1323 | [Fact] 1324 | public Task When_Anchor_Text_with_Underscore_Do_Not_Escape() 1325 | { 1326 | var html = $"This a sample paragraph from https://www.w3schools.com/html/mov_bbb.mp4"; 1327 | return CheckConversion(html); 1328 | } 1329 | 1330 | [Fact] 1331 | public Task When_Strikethrough_And_Nested_Strikethrough() 1332 | { 1333 | var html = $"This is the 1st sentence to test the strikethrough tag conversion"; 1334 | return CheckConversion(html); 1335 | } 1336 | 1337 | [Fact] 1338 | public Task When_Spaces_In_Inline_Tags_Should_Be_Retained() 1339 | { 1340 | var html = $"... example html code block"; 1341 | return CheckConversion(html); 1342 | } 1343 | 1344 | [Fact] 1345 | public Task When_SuppressNewlineFlag_PrefixDiv_Should_Be_Empty() 1346 | { 1347 | var html = $"
    the
    fox
    jumps
    over
    "; 1348 | return CheckConversion(html, new Config 1349 | { 1350 | SuppressDivNewlines = true 1351 | }); 1352 | } 1353 | 1354 | [Fact] 1355 | public Task WhenTable_WithColSpan_TableHeaderColumnSpansHandling_ThenConvertToGFMTable() 1356 | { 1357 | var html = 1358 | "
    col1col2col3
    data1data2.1data2.2data3
    "; 1359 | 1360 | var config = new Config 1361 | { 1362 | UnknownTags = Config.UnknownTagsOption.Bypass 1363 | }; 1364 | return CheckConversion(html, config); 1365 | } 1366 | 1367 | [Fact] 1368 | public Task Bug391_AnchorTagUnnecessarilyIndented() 1369 | { 1370 | var html = 1371 | "

    \n\n

    \n\n\n
    \nAn error occurred while importing data from feed 'FBA Producten'. More details can be found in the latest \">feed validation report.\n
    \n\n\n\n\n\" class=\"btn btn-primary btn-sm my-2\" target=\"_blank\">View feed 4"; 1372 | var config = new Config 1373 | { 1374 | GithubFlavored = true, 1375 | }; 1376 | 1377 | return CheckConversion(html, config); 1378 | } 1379 | 1380 | [Fact] 1381 | public Task Bug393_RegressionWithVaryingNewLines() 1382 | { 1383 | const string html = "This is regular text\r\n

    This is HTML:

    • Line 1
    • Line 2
    • Line 3 has an unknown tag

    "; 1384 | var config = new Config { UnknownTags = Config.UnknownTagsOption.Bypass, ListBulletChar = '*' }; 1385 | return CheckConversion(html, config); 1386 | } 1387 | 1388 | [Fact] 1389 | public Task SlackFlavored_Bold() 1390 | { 1391 | const string html = "test | test"; 1392 | var config = new Config { SlackFlavored = true }; 1393 | return CheckConversion(html, config); 1394 | } 1395 | 1396 | [Fact] 1397 | public Task SlackFlavored_Italic() 1398 | { 1399 | const string html = "test | test"; 1400 | var config = new Config { SlackFlavored = true }; 1401 | return CheckConversion(html, config); 1402 | } 1403 | 1404 | [Fact] 1405 | public Task SlackFlavored_Strikethrough() 1406 | { 1407 | const string html = "test"; 1408 | var config = new Config { SlackFlavored = true }; 1409 | return CheckConversion(html, config); 1410 | } 1411 | 1412 | [Fact] 1413 | public Task SlackFlavored_Bullets() 1414 | { 1415 | const string html = "
      \n
    • Item 1
    • \n
    • Item 2
    • \n
    • Item 3
    • \n
    "; 1416 | var config = new Config { SlackFlavored = true }; 1417 | return CheckConversion(html, config); 1418 | } 1419 | 1420 | [Fact] 1421 | public void SlackFlavored_Unsupported_Hr() 1422 | { 1423 | const string html = "
    "; 1424 | var config = new Config { SlackFlavored = true }; 1425 | var converter = new Converter(config); 1426 | Assert.Throws(() => converter.Convert(html)); 1427 | } 1428 | 1429 | [Fact] 1430 | public void SlackFlavored_Unsupported_Img() 1431 | { 1432 | const string html = ""; 1433 | var config = new Config { SlackFlavored = true }; 1434 | var converter = new Converter(config); 1435 | Assert.Throws(() => converter.Convert(html)); 1436 | } 1437 | 1438 | [Fact] 1439 | public void SlackFlavored_Unsupported_Sup() 1440 | { 1441 | const string html = "test"; 1442 | var config = new Config { SlackFlavored = true }; 1443 | var converter = new Converter(config); 1444 | Assert.Throws(() => converter.Convert(html)); 1445 | } 1446 | 1447 | [Fact] 1448 | public void SlackFlavored_Unsupported_Table() 1449 | { 1450 | const string html = "
    "; 1451 | var config = new Config { SlackFlavored = true }; 1452 | var converter = new Converter(config); 1453 | Assert.Throws(() => converter.Convert(html)); 1454 | } 1455 | 1456 | [Fact] 1457 | public void SlackFlavored_Unsupported_Table_Td() 1458 | { 1459 | const string html = ""; 1460 | var config = new Config { SlackFlavored = true }; 1461 | var converter = new Converter(config); 1462 | Assert.Throws(() => converter.Convert(html)); 1463 | } 1464 | 1465 | [Fact] 1466 | public void SlackFlavored_Unsupported_Table_Tr() 1467 | { 1468 | const string html = ""; 1469 | var config = new Config { SlackFlavored = true }; 1470 | var converter = new Converter(config); 1471 | Assert.Throws(() => converter.Convert(html)); 1472 | } 1473 | } 1474 | } 1475 | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/ReverseMarkdown.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0;net7.0;net8.0;net9.0 4 | true 5 | 6 | 7 | 8 | 9 | runtime; build; native; contentfiles; analyzers; buildtransitive 10 | all 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | 20 | 21 | 22 | ConverterTests.cs 23 | 24 | 25 | ConverterTests.cs 26 | 27 | 28 | ConverterTests.cs 29 | 30 | 31 | ConverterTests.cs 32 | 33 | 34 | ConverterTests.cs 35 | 36 | 37 | ConverterTests.WhenThereIsSingleAsteriskInText_ThenConvertToMarkdownEscapedAsterisk.verified.md 38 | 39 | 40 | ConverterTests.WhenThereIsUnorderedListAndBulletIsAsterisk_ThenConvertToMarkdownList.verified.md 41 | 42 | 43 | ConverterTests.WhenThereIsSingleAsteriskInText_ThenConvertToMarkdownEscapedAsterisk.verified.md 44 | 45 | 46 | ConverterTests.WhenThereIsUnorderedListAndBulletIsAsterisk_ThenConvertToMarkdownList.verified.md 47 | 48 | 49 | ConverterTests.WhenThereIsUnorderedListWithNestedOrderedList_ThenConvertToMarkdownListWithNestedList.verified.md 50 | 51 | 52 | ConverterTests.WhenThereIsSingleAsteriskInText_ThenConvertToMarkdownEscapedAsterisk.verified.md 53 | 54 | 55 | ConverterTests.WhenThereIsPreTag_ThenConvertToMarkdownPre.verified.md 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/Snippets.Usage.verified.txt: -------------------------------------------------------------------------------- 1 | This a sample **paragraph** from [my site](http://test.com) -------------------------------------------------------------------------------- /src/ReverseMarkdown.Test/Snippets.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using ReverseMarkdown; 3 | using VerifyXunit; 4 | using Xunit; 5 | 6 | public class Snippets 7 | { 8 | [Fact] 9 | public async Task Usage() 10 | { 11 | #region Usage 12 | 13 | var converter = new ReverseMarkdown.Converter(); 14 | 15 | string html = "This a sample paragraph from my site"; 16 | 17 | string result = converter.Convert(html); 18 | 19 | #endregion 20 | 21 | await Verifier.Verify(result); 22 | } 23 | 24 | [Fact] 25 | public void UsageWithConfig() 26 | { 27 | #region UsageWithConfig 28 | 29 | var config = new ReverseMarkdown.Config 30 | { 31 | // Include the unknown tag completely in the result (default as well) 32 | UnknownTags = Config.UnknownTagsOption.PassThrough, 33 | // generate GitHub flavoured markdown, supported for BR, PRE and table tags 34 | GithubFlavored = true, 35 | // will ignore all comments 36 | RemoveComments = true, 37 | // remove markdown output for links where appropriate 38 | SmartHrefHandling = true 39 | }; 40 | 41 | var converter = new ReverseMarkdown.Converter(config); 42 | 43 | #endregion 44 | } 45 | } -------------------------------------------------------------------------------- /src/ReverseMarkdown.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26228.9 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReverseMarkdown", "ReverseMarkdown\ReverseMarkdown.csproj", "{518A6D79-4596-4086-BA5D-DD353BCEC9A6}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReverseMarkdown.Test", "ReverseMarkdown.Test\ReverseMarkdown.Test.csproj", "{1756FE48-2619-459C-A0D0-2D2A1D163A03}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {518A6D79-4596-4086-BA5D-DD353BCEC9A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {518A6D79-4596-4086-BA5D-DD353BCEC9A6}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {518A6D79-4596-4086-BA5D-DD353BCEC9A6}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {518A6D79-4596-4086-BA5D-DD353BCEC9A6}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {1756FE48-2619-459C-A0D0-2D2A1D163A03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {1756FE48-2619-459C-A0D0-2D2A1D163A03}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {1756FE48-2619-459C-A0D0-2D2A1D163A03}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {1756FE48-2619-459C-A0D0-2D2A1D163A03}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Cleaner.cs: -------------------------------------------------------------------------------- 1 |  2 | using System; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace ReverseMarkdown 6 | { 7 | public static class Cleaner 8 | { 9 | private static readonly Regex SlackBoldCleaner = new Regex(@"\*(\s\*)+"); 10 | private static readonly Regex SlackItalicCleaner = new Regex(@"_(\s_)+"); 11 | 12 | private static string CleanTagBorders(string content) 13 | { 14 | // content from some htl editors such as CKEditor emits newline and tab between tags, clean that up 15 | content = content.Replace("\n\t", ""); 16 | content = content.Replace(Environment.NewLine + "\t", ""); 17 | return content; 18 | } 19 | 20 | private static string NormalizeSpaceChars(string content) 21 | { 22 | // replace unicode and non-breaking spaces to normal space 23 | content = Regex.Replace(content, @"[\u0020\u00A0]", " "); 24 | return content; 25 | } 26 | 27 | public static string PreTidy(string content, bool removeComments) 28 | { 29 | content = NormalizeSpaceChars(content); 30 | content = CleanTagBorders(content); 31 | 32 | return content; 33 | } 34 | 35 | public static string SlackTidy(string content) 36 | { 37 | // Slack's escaping rules depend on whether the key characters appear in 38 | // next to word characters or not. 39 | content = SlackBoldCleaner.Replace(content, "*"); 40 | content = SlackItalicCleaner.Replace(content, "_"); 41 | 42 | return content; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Config.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace ReverseMarkdown 5 | { 6 | public class Config 7 | { 8 | public UnknownTagsOption UnknownTags { get; set; } = UnknownTagsOption.PassThrough; 9 | 10 | public bool GithubFlavored { get; set; } = false; 11 | 12 | public bool SlackFlavored { get; set; } = false; 13 | 14 | public bool SuppressDivNewlines { get; set; } = false; 15 | 16 | public bool RemoveComments { get; set; } = false; 17 | 18 | /// 19 | /// Specify which schemes (without trailing colon) are to be allowed for <a> and <img> tags. Others will be bypassed. By default allows everything. 20 | /// If provided and when href schema couldn't be determined - whitelists 21 | /// 22 | public string[] WhitelistUriSchemes { get; set; } 23 | 24 | /// 25 | /// How to handle <a> tag href attribute 26 | /// false - Outputs [{name}]({href}{title}) even if name and href is identical. This is the default option. 27 | /// true - If name and href equals, outputs just the `name`. Note that if Uri is not well formed as per (i.e string is not correctly escaped like `http://example.com/path/file name.docx`) then markdown syntax will be used anyway. 28 | /// If href contains http/https protocol, and name doesn't but otherwise are the same, output href only 29 | /// If tel: or mailto: scheme, but afterwards identical with name, output name only. 30 | /// 31 | public bool SmartHrefHandling { get; set; } = false; 32 | 33 | public TableWithoutHeaderRowHandlingOption TableWithoutHeaderRowHandling { get; set; } = 34 | TableWithoutHeaderRowHandlingOption.Default; 35 | 36 | private char _listBulletChar = '-'; 37 | 38 | /// 39 | /// Option to set a different bullet character for un-ordered lists 40 | /// 41 | /// 42 | /// This option is ignored when is enabled. 43 | /// 44 | public char ListBulletChar 45 | { 46 | get => SlackFlavored ? '•' : _listBulletChar; 47 | set => _listBulletChar = value; 48 | } 49 | 50 | /// 51 | /// Option to set a default GFM code block language if class based language markers are not available 52 | /// 53 | public string DefaultCodeBlockLanguage { get; set; } 54 | 55 | /// 56 | /// Option to pass a list of tags to pass through as is without any processing 57 | /// 58 | public string[] PassThroughTags { get; set; } = { }; 59 | 60 | public enum UnknownTagsOption 61 | { 62 | /// 63 | /// Include the unknown tag completely into the result. That is, the tag along with the text will be left in output. 64 | /// 65 | PassThrough, 66 | 67 | /// 68 | /// Drop the unknown tag and its content 69 | /// 70 | Drop, 71 | 72 | /// 73 | /// Ignore the unknown tag but try to convert its content 74 | /// 75 | Bypass, 76 | 77 | /// 78 | /// Raise an error to let you know 79 | /// 80 | Raise 81 | } 82 | 83 | public enum TableWithoutHeaderRowHandlingOption 84 | { 85 | /// 86 | /// By default, first row will be used as header row 87 | /// 88 | Default, 89 | 90 | /// 91 | /// An empty row will be added as the header row 92 | /// 93 | EmptyRow 94 | } 95 | 96 | /// 97 | /// Set this flag to handle table header column with column spans 98 | /// 99 | public bool TableHeaderColumnSpanHandling { get; set; } = true; 100 | 101 | public bool CleanupUnnecessarySpaces { get; set; } = true; 102 | 103 | 104 | /// 105 | /// Determines whether url is allowed: WhitelistUriSchemes contains no elements or contains passed url. 106 | /// 107 | /// Scheme name without trailing colon 108 | internal bool IsSchemeWhitelisted(string scheme) 109 | { 110 | if (scheme == null) throw new ArgumentNullException(nameof(scheme)); 111 | var isSchemeAllowed = WhitelistUriSchemes == null || WhitelistUriSchemes.Length == 0 || 112 | WhitelistUriSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase); 113 | return isSchemeAllowed; 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text.RegularExpressions; 6 | using HtmlAgilityPack; 7 | using ReverseMarkdown.Converters; 8 | 9 | namespace ReverseMarkdown 10 | { 11 | public class Converter 12 | { 13 | protected readonly IDictionary Converters = new Dictionary(); 14 | protected readonly IConverter PassThroughTagsConverter; 15 | protected readonly IConverter DropTagsConverter; 16 | protected readonly IConverter ByPassTagsConverter; 17 | 18 | public Converter() : this(new Config()) {} 19 | 20 | public Converter(Config config) : this(config, null) {} 21 | 22 | public Converter(Config config, params Assembly[] additionalAssemblies) 23 | { 24 | Config = config; 25 | 26 | var assemblies = new List() 27 | { 28 | typeof(IConverter).GetTypeInfo().Assembly 29 | }; 30 | 31 | if (!(additionalAssemblies is null)) 32 | assemblies.AddRange(additionalAssemblies); 33 | 34 | var types = new List(); 35 | // instantiate all converters excluding the unknown tags converters 36 | foreach (var assembly in assemblies) 37 | { 38 | foreach (var converterType in assembly.GetTypes() 39 | .Where(t => t.GetTypeInfo().GetInterfaces().Contains(typeof(IConverter)) && 40 | !t.GetTypeInfo().IsAbstract 41 | && t != typeof(PassThrough) 42 | && t != typeof(Drop) 43 | && t != typeof(ByPass))) 44 | { 45 | // Check to see if any existing types are children/equal to 46 | // the type to add. 47 | if (types.Any(e => converterType.IsAssignableFrom(e))) 48 | // If they are, ignore the type. 49 | continue; 50 | 51 | // See if there is a type that is a parent of the 52 | // current type. 53 | var toRemove = types.FirstOrDefault(e => e.IsAssignableFrom(converterType)); 54 | // if there is ... 55 | if (!(toRemove is null)) 56 | // ... remove the parent. 57 | types.Remove(toRemove); 58 | 59 | // finally, add the type. 60 | types.Add(converterType); 61 | } 62 | } 63 | 64 | // For each type to register ... 65 | foreach (var converterType in types) 66 | // ... activate them 67 | Activator.CreateInstance(converterType, this); 68 | 69 | // register the unknown tags converters 70 | PassThroughTagsConverter = new PassThrough(this); 71 | DropTagsConverter = new Drop(this); 72 | ByPassTagsConverter = new ByPass(this); 73 | } 74 | 75 | public Config Config { get; protected set; } 76 | 77 | public virtual string Convert(string html) 78 | { 79 | html = Cleaner.PreTidy(html, Config.RemoveComments); 80 | 81 | var doc = new HtmlDocument(); 82 | doc.LoadHtml(html); 83 | 84 | var root = doc.DocumentNode; 85 | 86 | // ensure to start from body and ignore head etc 87 | if (root.Descendants("body").Any()) 88 | { 89 | root = root.SelectSingleNode("//body"); 90 | } 91 | 92 | var result = Lookup(root.Name).Convert(root); 93 | 94 | // cleanup multiple new lines 95 | result = Regex.Replace( result, @"(^\p{Zs}*(\r\n|\n)){2,}", Environment.NewLine, RegexOptions.Multiline); 96 | 97 | if (Config.SlackFlavored) 98 | { 99 | result = Cleaner.SlackTidy(result); 100 | } 101 | 102 | return Config.CleanupUnnecessarySpaces ? result.Trim().FixMultipleNewlines() : result; 103 | } 104 | 105 | public virtual void Register(string tagName, IConverter converter) 106 | { 107 | Converters[tagName] = converter; 108 | } 109 | 110 | public virtual IConverter Lookup(string tagName) 111 | { 112 | // if a tag is in the pass through list then use the pass through tags converter 113 | if (Config.PassThroughTags.Contains(tagName)) 114 | { 115 | return PassThroughTagsConverter; 116 | } 117 | 118 | return Converters.TryGetValue(tagName, out var converter) ? converter : GetDefaultConverter(tagName); 119 | } 120 | 121 | private IConverter GetDefaultConverter(string tagName) 122 | { 123 | switch (Config.UnknownTags) 124 | { 125 | case Config.UnknownTagsOption.PassThrough: 126 | return PassThroughTagsConverter; 127 | case Config.UnknownTagsOption.Drop: 128 | return DropTagsConverter; 129 | case Config.UnknownTagsOption.Bypass: 130 | return ByPassTagsConverter; 131 | default: 132 | throw new UnknownTagException(tagName); 133 | } 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/A.cs: -------------------------------------------------------------------------------- 1 | using HtmlAgilityPack; 2 | using System; 3 | using System.Linq; 4 | 5 | namespace ReverseMarkdown.Converters 6 | { 7 | public class A : ConverterBase 8 | { 9 | public A(Converter converter) 10 | : base(converter) 11 | { 12 | Converter.Register("a", this); 13 | } 14 | 15 | public override string Convert(HtmlNode node) 16 | { 17 | var name = TreatChildren(node).Trim(); 18 | 19 | var hasSingleChildImgNode = node.ChildNodes.Count == 1 && node.ChildNodes.Count(n => n.Name.Contains("img")) == 1; 20 | 21 | var href = node.GetAttributeValue("href", string.Empty).Trim().Replace("(", "%28").Replace(")", "%29").Replace(" ", "%20"); 22 | var title = ExtractTitle(node); 23 | title = title.Length > 0 ? $" \"{title}\"" : ""; 24 | var scheme = StringUtils.GetScheme(href); 25 | 26 | var isRemoveLinkWhenSameName = Converter.Config.SmartHrefHandling 27 | && scheme != string.Empty 28 | && Uri.IsWellFormedUriString(href, UriKind.RelativeOrAbsolute) 29 | && ( 30 | href.Equals(name, StringComparison.OrdinalIgnoreCase) 31 | || href.Equals($"tel:{name}", StringComparison.OrdinalIgnoreCase) 32 | || href.Equals($"mailto:{name}", StringComparison.OrdinalIgnoreCase) 33 | ); 34 | 35 | if (href.StartsWith("#") //anchor link 36 | || !Converter.Config.IsSchemeWhitelisted(scheme) //Not allowed scheme 37 | || isRemoveLinkWhenSameName 38 | || string.IsNullOrEmpty(href)) //We would otherwise print empty () here... 39 | { 40 | return name; 41 | } 42 | 43 | // if (!string.IsNullOrEmpty(href) && string.IsNullOrEmpty(name)) 44 | // { 45 | // name = href; 46 | // } 47 | 48 | var useHrefWithHttpWhenNameHasNoScheme = Converter.Config.SmartHrefHandling && 49 | (scheme.Equals("http", StringComparison.OrdinalIgnoreCase) || scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) 50 | && string.Equals(href, $"{scheme}://{name}", StringComparison.OrdinalIgnoreCase); 51 | 52 | // if the anchor tag contains a single child image node don't escape the link text 53 | var linkText = hasSingleChildImgNode ? name : StringUtils.EscapeLinkText(name); 54 | 55 | if (string.IsNullOrEmpty(linkText)) 56 | { 57 | return href; 58 | } 59 | 60 | return useHrefWithHttpWhenNameHasNoScheme ? href : $"[{linkText}]({href}{title})"; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/Aside.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using HtmlAgilityPack; 3 | 4 | namespace ReverseMarkdown.Converters 5 | { 6 | public class Aside : ConverterBase 7 | { 8 | public Aside(Converter converter) 9 | : base(converter) 10 | { 11 | Converter.Register("aside", this); 12 | } 13 | 14 | public override string Convert(HtmlNode node) 15 | { 16 | return $"{Environment.NewLine}{TreatChildren(node)}{Environment.NewLine}"; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/Blockquote.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | using HtmlAgilityPack; 5 | 6 | namespace ReverseMarkdown.Converters 7 | { 8 | public class Blockquote : ConverterBase 9 | { 10 | public Blockquote(Converter converter) : base(converter) 11 | { 12 | Converter.Register("blockquote", this); 13 | } 14 | 15 | public override string Convert(HtmlNode node) 16 | { 17 | var content = TreatChildren(node); 18 | 19 | // get the lines based on carriage return and prefix "> " to each line 20 | var lines = content.ReadLines().Select(item => "> " + item + Environment.NewLine); 21 | 22 | // join all the lines to a single line 23 | var result = lines.Aggregate(string.Empty, (current, next) => current + next); 24 | 25 | return $"{Environment.NewLine}{Environment.NewLine}{result}{Environment.NewLine}"; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/Br.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using HtmlAgilityPack; 4 | 5 | namespace ReverseMarkdown.Converters 6 | { 7 | public class Br : ConverterBase 8 | { 9 | public Br(Converter converter) : base(converter) 10 | { 11 | Converter.Register("br", this); 12 | } 13 | 14 | public override string Convert(HtmlNode node) 15 | { 16 | var parentName = node.ParentNode.Name.ToLowerInvariant(); 17 | var parentList = new string[] {"strong", "b", "em", "i"}; 18 | if (parentList.Contains(parentName)) 19 | { 20 | return ""; 21 | } 22 | 23 | return Converter.Config.GithubFlavored ? Environment.NewLine : $" {Environment.NewLine}"; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/ByPass.cs: -------------------------------------------------------------------------------- 1 | using HtmlAgilityPack; 2 | 3 | namespace ReverseMarkdown.Converters 4 | { 5 | public class ByPass : ConverterBase 6 | { 7 | public ByPass(Converter converter) : base(converter) 8 | { 9 | Converter.Register("#document", this); 10 | Converter.Register("html", this); 11 | Converter.Register("body", this); 12 | Converter.Register("span", this); 13 | Converter.Register("thead", this); 14 | Converter.Register("tbody", this); 15 | } 16 | 17 | public override string Convert(HtmlNode node) 18 | { 19 | return TreatChildren(node); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/Code.cs: -------------------------------------------------------------------------------- 1 | using HtmlAgilityPack; 2 | using System.Net; 3 | using System.Text; 4 | 5 | namespace ReverseMarkdown.Converters 6 | { 7 | public class Code : ConverterBase 8 | { 9 | public Code(Converter converter) : base(converter) 10 | { 11 | Converter.Register("code", this); 12 | } 13 | 14 | public override string Convert(HtmlNode node) 15 | { 16 | // Depending on the content "surrounding" the element, 17 | // leading/trailing whitespace is significant. For example, the 18 | // following HTML renders as expected in a browser (meaning there is 19 | // proper spacing between words): 20 | // 21 | //

    The JavaScript function keyword...

    22 | // 23 | // However, if we simply trim the contents of the element, 24 | // then the Markdown becomes: 25 | // 26 | // The JavaScript`function`keyword... 27 | // 28 | // To avoid this scenario, do *not* trim the inner text of the 29 | // element. 30 | // 31 | // For the HTML example above, the Markdown will be: 32 | // 33 | // The JavaScript` function `keyword... 34 | // 35 | // While it might seem preferable to try to "fix" this by trimming 36 | // the element and insert leading/trailing spaces as 37 | // necessary, things become complicated rather quickly depending 38 | // on the particular content. For example, what would be the 39 | // "correct" conversion of the following HTML? 40 | // 41 | //

    The JavaScript function keyword...

    42 | // 43 | // The simplest conversion to Markdown: 44 | // 45 | // The JavaScript**` function `**keyword... 46 | // 47 | // Some other, arguably "better", alternatives (that would require 48 | // substantially more conversion logic): 49 | // 50 | // The JavaScript** `function` **keyword... 51 | // 52 | // The JavaScript **`function`** keyword... 53 | 54 | var sb = new StringBuilder(); 55 | 56 | sb.Append('`'); 57 | sb.Append(WebUtility.HtmlDecode(node.InnerText)); 58 | sb.Append('`'); 59 | 60 | return sb.ToString(); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/ConverterBase.cs: -------------------------------------------------------------------------------- 1 |  2 | using System.Linq; 3 | using HtmlAgilityPack; 4 | 5 | namespace ReverseMarkdown.Converters 6 | { 7 | public abstract class ConverterBase : IConverter 8 | { 9 | protected ConverterBase(Converter converter) 10 | { 11 | Converter = converter; 12 | } 13 | 14 | protected Converter Converter { get; } 15 | 16 | protected string TreatChildren(HtmlNode node) 17 | { 18 | var result = string.Empty; 19 | 20 | return !node.HasChildNodes 21 | ? result 22 | : node.ChildNodes.Aggregate(result, (current, nd) => current + Treat(nd)); 23 | } 24 | 25 | private string Treat(HtmlNode node) { 26 | // TrimNewLine(node); 27 | var converter = Converter.Lookup(node.Name); 28 | return converter.Convert(node); 29 | } 30 | 31 | private static void TrimNewLine(HtmlNode node) 32 | { 33 | if (!node.HasChildNodes) return; 34 | 35 | if (node.FirstChild.Name == "#text" && (node.FirstChild.InnerText.StartsWith("\r\n") || node.FirstChild.InnerText.StartsWith("\n"))) 36 | { 37 | node.FirstChild.InnerHtml = node.FirstChild.InnerHtml.TrimStart('\r').TrimStart('\n'); 38 | } 39 | 40 | if (node.LastChild.Name == "#text" && (node.LastChild.InnerText.EndsWith("\r\n") || node.LastChild.InnerText.EndsWith("\n"))) 41 | { 42 | node.LastChild.InnerHtml = node.LastChild.InnerHtml.TrimEnd('\r').TrimEnd('\n'); 43 | } 44 | } 45 | 46 | protected static string ExtractTitle(HtmlNode node) 47 | { 48 | return node.GetAttributeValue("title", ""); 49 | } 50 | 51 | protected static string DecodeHtml(string html) 52 | { 53 | return System.Net.WebUtility.HtmlDecode(html); 54 | } 55 | 56 | protected static string IndentationFor(HtmlNode node, bool zeroIndex=false) 57 | { 58 | var length = node.Ancestors("ol").Count() + node.Ancestors("ul").Count(); 59 | 60 | // li not required to have a parent ol/ul 61 | if (length == 0) 62 | { 63 | return string.Empty; 64 | } 65 | 66 | if (zeroIndex) 67 | { 68 | length -= 1; 69 | } 70 | 71 | return new string(' ', length*4); 72 | } 73 | 74 | public abstract string Convert(HtmlNode node); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/Dd.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using HtmlAgilityPack; 3 | 4 | namespace ReverseMarkdown.Converters 5 | { 6 | public class Dd : ConverterBase 7 | { 8 | public Dd(Converter converter) : base(converter) 9 | { 10 | Converter.Register("dd", this); 11 | } 12 | 13 | public override string Convert(HtmlNode node) 14 | { 15 | var indent = new string(' ', 4); 16 | var prefix = $"{Converter.Config.ListBulletChar} "; 17 | var content = TreatChildren(node); 18 | return $"{indent}{prefix}{content.Chomp()}{Environment.NewLine}"; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/Div.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using HtmlAgilityPack; 4 | 5 | namespace ReverseMarkdown.Converters 6 | { 7 | public class Div : ConverterBase 8 | { 9 | public Div(Converter converter) : base(converter) 10 | { 11 | Converter.Register("div", this); 12 | } 13 | 14 | public override string Convert(HtmlNode node) 15 | { 16 | string content; 17 | 18 | do 19 | { 20 | if (node.ChildNodes.Count == 1 && node.FirstChild.Name == "div") 21 | { 22 | node = node.FirstChild; 23 | continue; 24 | } 25 | 26 | content = TreatChildren(node); 27 | break; 28 | } while (true); 29 | 30 | var blockTags = new List 31 | { 32 | "pre", 33 | "p", 34 | "ol", 35 | "oi", 36 | "table" 37 | }; 38 | 39 | content = Converter.Config.CleanupUnnecessarySpaces ? content.Trim() : content; 40 | 41 | // if there is a block child then ignore adding the newlines for div 42 | if ((node.ChildNodes.Count == 1 && blockTags.Contains(node.FirstChild.Name))) 43 | { 44 | return content; 45 | } 46 | 47 | var prefix = Environment.NewLine; 48 | 49 | if (Td.FirstNodeWithinCell(node)) 50 | { 51 | prefix = string.Empty; 52 | } 53 | else if (Converter.Config.SuppressDivNewlines) 54 | { 55 | prefix = string.Empty; 56 | } 57 | 58 | return $"{prefix}{content}{(Td.LastNodeWithinCell(node) ? "" : Environment.NewLine)}"; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/Dl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using HtmlAgilityPack; 3 | 4 | namespace ReverseMarkdown.Converters 5 | { 6 | public class Dl : ConverterBase 7 | { 8 | public Dl(Converter converter) : base(converter) 9 | { 10 | Converter.Register("dl", this); 11 | } 12 | 13 | public override string Convert(HtmlNode node) 14 | { 15 | var prefixSuffix = Environment.NewLine; 16 | return $"{prefixSuffix}{TreatChildren(node)}{prefixSuffix}"; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/Drop.cs: -------------------------------------------------------------------------------- 1 | using HtmlAgilityPack; 2 | 3 | namespace ReverseMarkdown.Converters 4 | { 5 | public class Drop : ConverterBase 6 | { 7 | public Drop(Converter converter) : base(converter) 8 | { 9 | Converter.Register("style", this); 10 | Converter.Register("script", this); 11 | if (Converter.Config.RemoveComments) { 12 | converter.Register("#comment", this); 13 | } 14 | } 15 | 16 | public override string Convert(HtmlNode node) 17 | { 18 | return ""; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/Dt.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using HtmlAgilityPack; 3 | 4 | namespace ReverseMarkdown.Converters 5 | { 6 | public class Dt : ConverterBase 7 | { 8 | public Dt(Converter converter) : base(converter) 9 | { 10 | Converter.Register("dt", this); 11 | } 12 | 13 | public override string Convert(HtmlNode node) 14 | { 15 | var prefix = $"{Converter.Config.ListBulletChar} "; 16 | var content = TreatChildren(node); 17 | return $"{prefix}{content.Chomp()}{Environment.NewLine}"; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/Em.cs: -------------------------------------------------------------------------------- 1 |  2 | using System.Linq; 3 | 4 | using HtmlAgilityPack; 5 | 6 | namespace ReverseMarkdown.Converters 7 | { 8 | public class Em : ConverterBase 9 | { 10 | public Em(Converter converter) : base(converter) 11 | { 12 | var elements = new [] { "em", "i" }; 13 | 14 | foreach (var element in elements) 15 | { 16 | Converter.Register(element, this); 17 | } 18 | } 19 | 20 | public override string Convert(HtmlNode node) 21 | { 22 | var content = TreatChildren(node); 23 | 24 | if (string.IsNullOrEmpty(content.Trim()) || AlreadyItalic(node)) 25 | { 26 | return content; 27 | } 28 | 29 | var spaceSuffix = (node.NextSibling?.Name == "i" || node.NextSibling?.Name == "em") 30 | ? " " 31 | : ""; 32 | 33 | var emphasis = Converter.Config.SlackFlavored ? "_" : "*"; 34 | return content.EmphasizeContentWhitespaceGuard(emphasis, spaceSuffix); 35 | } 36 | 37 | private static bool AlreadyItalic(HtmlNode node) 38 | { 39 | return node.Ancestors("i").Any() || node.Ancestors("em").Any(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/H.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using HtmlAgilityPack; 4 | 5 | namespace ReverseMarkdown.Converters 6 | { 7 | public class H : ConverterBase 8 | { 9 | public H(Converter converter) : base(converter) 10 | { 11 | var elements = new [] { "h1", "h2", "h3", "h4", "h5", "h6" }; 12 | foreach (var element in elements) 13 | { 14 | Converter.Register(element, this); 15 | } 16 | } 17 | 18 | public override string Convert(HtmlNode node) 19 | { 20 | // Headings inside tables are not supported as markdown, so just ignore the heading and convert children 21 | if (node.Ancestors("table").Any()) 22 | { 23 | return TreatChildren(node); 24 | } 25 | 26 | var prefix = new string('#', System.Convert.ToInt32(node.Name.Substring(1))); 27 | 28 | return $"{Environment.NewLine}{prefix} {TreatChildren(node)}{Environment.NewLine}"; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/Hr.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using HtmlAgilityPack; 3 | 4 | namespace ReverseMarkdown.Converters 5 | { 6 | public class Hr : ConverterBase 7 | { 8 | public Hr(Converter converter) : base(converter) 9 | { 10 | Converter.Register("hr", this); 11 | } 12 | 13 | public override string Convert(HtmlNode node) 14 | { 15 | if (Converter.Config.SlackFlavored) 16 | { 17 | throw new SlackUnsupportedTagException(node.Name); 18 | } 19 | 20 | return $"{Environment.NewLine}* * *{Environment.NewLine}"; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/IConverter.cs: -------------------------------------------------------------------------------- 1 | using HtmlAgilityPack; 2 | 3 | namespace ReverseMarkdown.Converters 4 | { 5 | public interface IConverter 6 | { 7 | string Convert(HtmlNode node); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/Ignore.cs: -------------------------------------------------------------------------------- 1 | using HtmlAgilityPack; 2 | 3 | namespace ReverseMarkdown.Converters 4 | { 5 | public class Ignore : ConverterBase 6 | { 7 | public Ignore(Converter converter) : base(converter) 8 | { 9 | var elements = new [] { "colgroup", "col" }; 10 | 11 | foreach (var element in elements) 12 | { 13 | Converter.Register(element, this); 14 | } 15 | } 16 | 17 | public override string Convert(HtmlNode node) 18 | { 19 | return ""; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/Img.cs: -------------------------------------------------------------------------------- 1 | using HtmlAgilityPack; 2 | 3 | namespace ReverseMarkdown.Converters 4 | { 5 | public class Img : ConverterBase 6 | { 7 | public Img(Converter converter) : base(converter) 8 | { 9 | Converter.Register("img", this); 10 | } 11 | 12 | public override string Convert(HtmlNode node) 13 | { 14 | if (Converter.Config.SlackFlavored) 15 | { 16 | throw new SlackUnsupportedTagException(node.Name); 17 | } 18 | 19 | var alt = node.GetAttributeValue("alt", string.Empty); 20 | var src = node.GetAttributeValue("src", string.Empty); 21 | 22 | if (!Converter.Config.IsSchemeWhitelisted(StringUtils.GetScheme(src))) 23 | { 24 | return ""; 25 | } 26 | 27 | var title = ExtractTitle(node); 28 | title = title.Length > 0 ? $" \"{title}\"" : ""; 29 | 30 | return $"![{StringUtils.EscapeLinkText(alt)}]({src}{title})"; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/Li.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | using HtmlAgilityPack; 5 | 6 | namespace ReverseMarkdown.Converters 7 | { 8 | public class Li : ConverterBase 9 | { 10 | public Li(Converter converter) : base(converter) 11 | { 12 | Converter.Register("li", this); 13 | } 14 | 15 | public override string Convert(HtmlNode node) 16 | { 17 | // Standardize whitespace before inner lists so that the following are equivalent 18 | //
  • Foo
    • ... 19 | //
    • Foo\n
      • ... 20 | foreach (var innerList in node.SelectNodes("//ul|//ol") ?? Enumerable.Empty()) 21 | { 22 | if (innerList.PreviousSibling?.NodeType == HtmlNodeType.Text) 23 | { 24 | innerList.PreviousSibling.InnerHtml = innerList.PreviousSibling.InnerHtml.Chomp(); 25 | } 26 | } 27 | 28 | var content = TreatChildren(node); 29 | var indentation = IndentationFor(node, true); 30 | var prefix = PrefixFor(node); 31 | 32 | return $"{indentation}{prefix}{content.Chomp()}{Environment.NewLine}"; 33 | } 34 | 35 | private string PrefixFor(HtmlNode node) 36 | { 37 | if (node.ParentNode != null && node.ParentNode.Name == "ol") 38 | { 39 | // index are zero based hence add one 40 | var index = node.ParentNode.SelectNodes("./li").IndexOf(node) + 1; 41 | return $"{index}. "; 42 | } 43 | else 44 | { 45 | return $"{Converter.Config.ListBulletChar} "; 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/Ol.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using HtmlAgilityPack; 4 | 5 | namespace ReverseMarkdown.Converters 6 | { 7 | public class Ol : ConverterBase 8 | { 9 | public Ol(Converter converter) : base(converter) 10 | { 11 | var elements = new[] { "ol", "ul" }; 12 | 13 | foreach (var element in elements) 14 | { 15 | Converter.Register(element, this); 16 | } 17 | } 18 | 19 | public override string Convert(HtmlNode node) 20 | { 21 | // Lists inside tables are not supported as markdown, so leave as HTML 22 | if (node.Ancestors("table").Any()) 23 | { 24 | return node.OuterHtml; 25 | } 26 | 27 | string prefixSuffix = Environment.NewLine; 28 | 29 | // Prevent blank lines being inserted in nested lists 30 | string parentName = node.ParentNode.Name.ToLowerInvariant(); 31 | if (parentName == "ol" || parentName == "ul") 32 | { 33 | prefixSuffix = ""; 34 | } 35 | 36 | return $"{prefixSuffix}{TreatChildren(node)}{prefixSuffix}"; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/P.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using HtmlAgilityPack; 4 | 5 | namespace ReverseMarkdown.Converters 6 | { 7 | public class P : ConverterBase 8 | { 9 | public P(Converter converter) : base(converter) 10 | { 11 | Converter.Register("p", this); 12 | } 13 | 14 | public override string Convert(HtmlNode node) 15 | { 16 | var indentation = IndentationFor(node); 17 | var newlineAfter = NewlineAfter(node); 18 | 19 | var content = Converter.Config.CleanupUnnecessarySpaces ? TreatChildren(node).Trim() : TreatChildren(node); 20 | 21 | return $"{indentation}{TreatChildren(node)}{newlineAfter}"; 22 | } 23 | 24 | private static string IndentationFor(HtmlNode node) 25 | { 26 | string parentName = node.ParentNode.Name.ToLowerInvariant(); 27 | 28 | // If p follows a list item, add newline and indent it 29 | var length = node.Ancestors("ol").Count() + node.Ancestors("ul").Count(); 30 | bool parentIsList = parentName == "li" || parentName == "ol" || parentName == "ul"; 31 | if (parentIsList && node.ParentNode.FirstChild != node) 32 | return Environment.NewLine + (new string(' ', length * 4)); 33 | 34 | // If p is at the start of a table cell, no leading newline 35 | return Td.FirstNodeWithinCell(node) ? "" : Environment.NewLine; 36 | } 37 | 38 | private static string NewlineAfter(HtmlNode node) 39 | { 40 | return Td.LastNodeWithinCell(node) ? "" : Environment.NewLine; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/PassThrough.cs: -------------------------------------------------------------------------------- 1 | using HtmlAgilityPack; 2 | 3 | namespace ReverseMarkdown.Converters 4 | { 5 | public class PassThrough : ConverterBase 6 | { 7 | public PassThrough(Converter converter) 8 | : base(converter) 9 | { 10 | } 11 | 12 | public override string Convert(HtmlNode node) 13 | { 14 | return node.OuterHtml; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/Pre.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text.RegularExpressions; 4 | using HtmlAgilityPack; 5 | 6 | namespace ReverseMarkdown.Converters 7 | { 8 | public class Pre : ConverterBase 9 | { 10 | public Pre(Converter converter) : base(converter) 11 | { 12 | Converter.Register("pre", this); 13 | } 14 | 15 | public override string Convert(HtmlNode node) 16 | { 17 | var content = DecodeHtml(node.InnerText); 18 | 19 | // check if indentation need to be added if it is under a ordered or unordered list 20 | var indentation = IndentationFor(node); 21 | 22 | var fencedCodeStartBlock = string.Empty; 23 | var fencedCodeEndBlock = string.Empty; 24 | 25 | if (Converter.Config.GithubFlavored) 26 | { 27 | var lang = GetLanguage(node); 28 | fencedCodeStartBlock = $"{indentation}```{lang}{Environment.NewLine}"; 29 | fencedCodeEndBlock = $"{indentation}```"; 30 | } 31 | else 32 | { 33 | // 4 space indent for code if it is not fenced code block 34 | indentation += " "; 35 | } 36 | 37 | var lines = content.ReadLines().Select(item => indentation + item); 38 | content = string.Join(Environment.NewLine, lines); 39 | 40 | if (string.IsNullOrEmpty(content) 41 | && Converter.Config.GithubFlavored == false) 42 | { 43 | content = indentation; 44 | } 45 | 46 | return $"{Environment.NewLine}{Environment.NewLine}{fencedCodeStartBlock}{content}{Environment.NewLine}{fencedCodeEndBlock}{Environment.NewLine}"; 47 | } 48 | 49 | private string GetLanguage(HtmlNode node) 50 | { 51 | var language = GetLanguageFromHighlightClassAttribute(node); 52 | 53 | return !string.IsNullOrEmpty(language) 54 | ? language 55 | : Converter.Config.DefaultCodeBlockLanguage; 56 | } 57 | 58 | 59 | private static string GetLanguageFromHighlightClassAttribute(HtmlNode node) 60 | { 61 | var res = ClassMatch(node); 62 | 63 | // check parent node: 64 | // GitHub:
         65 |             // BitBucket: 
         66 |             if (!res.Success && node.ParentNode != null)
         67 |             {
         68 |                 res = ClassMatch(node.ParentNode);
         69 |             }
         70 | 
         71 |             // check child  node:
         72 |             // HighlightJs: 
        
         73 |             if (!res.Success)
         74 |             {
         75 |                 var cnode = node.ChildNodes["code"];
         76 |                 if (cnode != null)
         77 |                 {
         78 |                     res = ClassMatch(cnode);
         79 |                 }
         80 |             }
         81 | 
         82 |             return res.Success && res.Groups.Count == 3 ? res.Groups[2].Value : string.Empty;
         83 |         }
         84 | 
         85 |         /// 
         86 |         /// Extracts class attribute syntax using: highlight-json, highlight-source-json, language-json, brush: language
         87 |         /// Returns the Language in Match.Groups[2]
         88 |         /// 
         89 |         private static readonly Regex ClassRegex = new Regex(@"(highlight-source-|language-|highlight-|brush:\s)([a-zA-Z0-9]+)");
         90 | 
         91 |         /// 
         92 |         /// Checks class attribute for language class identifiers for various
         93 |         /// common highlighters
         94 |         /// 
         95 |         /// Node with class attribute
         96 |         /// Match.Success and Match.Group[2] set to the language
         97 |         private static Match ClassMatch(HtmlNode node)
         98 |         {
         99 |             var val = node.GetAttributeValue("class", "");
        100 |             if (!string.IsNullOrEmpty(val))
        101 |             {
        102 |                 return ClassRegex.Match(val);
        103 |             }
        104 | 
        105 |             return Match.Empty;
        106 |         }
        107 |     }
        108 | }
        109 | 
        
        
        --------------------------------------------------------------------------------
        /src/ReverseMarkdown/Converters/S.cs:
        --------------------------------------------------------------------------------
         1 | using System.Linq;
         2 | using HtmlAgilityPack;
         3 | 
         4 | namespace ReverseMarkdown.Converters
         5 | {
         6 |     public class S : ConverterBase
         7 |     {
         8 |         public S(Converter converter) : base(converter)
         9 |         {
        10 |             Converter.Register("s", this);
        11 |             Converter.Register("del", this);
        12 |             Converter.Register("strike", this);
        13 |         }
        14 | 
        15 |         public override string Convert(HtmlNode node)
        16 |         {
        17 |             var content = TreatChildren(node);
        18 |             if (string.IsNullOrEmpty(content) || AlreadyStrikethrough(node))
        19 |             {
        20 |                 return content;
        21 |             }
        22 | 
        23 |             var emphasis = Converter.Config.SlackFlavored ? "~" : "~~";
        24 |             return content.EmphasizeContentWhitespaceGuard(emphasis);
        25 |         }
        26 | 
        27 |         private static bool AlreadyStrikethrough(HtmlNode node)
        28 |         {
        29 |             return node.Ancestors("s").Any() || node.Ancestors("del").Any() || node.Ancestors("strike").Any();
        30 |         }
        31 |     }
        32 | }
        33 | 
        
        
        --------------------------------------------------------------------------------
        /src/ReverseMarkdown/Converters/Strong.cs:
        --------------------------------------------------------------------------------
         1 | using System.Linq;
         2 | using HtmlAgilityPack;
         3 | 
         4 | namespace ReverseMarkdown.Converters
         5 | {
         6 |     public class Strong : ConverterBase
         7 |     {
         8 |         public Strong(Converter converter) : base(converter)
         9 |         {
        10 |             var elements = new [] { "strong", "b" };
        11 | 
        12 |             foreach (var element in elements)
        13 |             {
        14 |                 Converter.Register(element, this);
        15 |             }
        16 |         }
        17 | 
        18 |         public override string Convert(HtmlNode node)
        19 |         {
        20 |             var content = TreatChildren(node);
        21 |             if (string.IsNullOrEmpty(content) || AlreadyBold(node))
        22 |             {
        23 |                 return content;
        24 |             }
        25 |             
        26 |             var spaceSuffix = (node.NextSibling?.Name == "strong" || node.NextSibling?.Name == "b")
        27 |                 ? " "
        28 |                 : "";
        29 | 
        30 |             var emphasis = Converter.Config.SlackFlavored ? "*" : "**";
        31 |             return content.EmphasizeContentWhitespaceGuard(emphasis, spaceSuffix);
        32 |         }
        33 | 
        34 |         private static bool AlreadyBold(HtmlNode node)
        35 |         {
        36 |             return node.Ancestors("strong").Any() || node.Ancestors("b").Any();
        37 |         }
        38 |     }
        39 | }
        40 | 
        
        
        --------------------------------------------------------------------------------
        /src/ReverseMarkdown/Converters/Sup.cs:
        --------------------------------------------------------------------------------
         1 | using System.Linq;
         2 | using HtmlAgilityPack;
         3 | 
         4 | namespace ReverseMarkdown.Converters
         5 | {
         6 |     public class Sup : ConverterBase
         7 |     {
         8 |         public Sup(Converter converter) : base(converter)
         9 |         {
        10 |             Converter.Register("sup", this);   
        11 |         }
        12 | 
        13 |         public override string Convert(HtmlNode node)
        14 |         {
        15 |             if (Converter.Config.SlackFlavored)
        16 |             {
        17 |                 throw new SlackUnsupportedTagException(node.Name);
        18 |             }
        19 |             
        20 |             var content = TreatChildren(node);
        21 |             if (string.IsNullOrEmpty(content) || AlreadySup(node))
        22 |             {
        23 |                 return content;
        24 |             }
        25 | 
        26 |             return $"^{content.Chomp(all:true)}^";
        27 |         }
        28 | 
        29 |         private static bool AlreadySup(HtmlNode node)
        30 |         {
        31 |             return node.Ancestors("sup").Any();
        32 |         }
        33 |     }
        34 | }
        35 | 
        
        
        --------------------------------------------------------------------------------
        /src/ReverseMarkdown/Converters/Table.cs:
        --------------------------------------------------------------------------------
         1 | using System;
         2 | using System.Collections.Generic;
         3 | using System.Linq;
         4 | using HtmlAgilityPack;
         5 | 
         6 | namespace ReverseMarkdown.Converters
         7 | {
         8 |     public class Table : ConverterBase
         9 |     {
        10 |         public Table(Converter converter) : base(converter)
        11 |         {
        12 |             Converter.Register("table", this);
        13 |         }
        14 | 
        15 |         public override string Convert(HtmlNode node)
        16 |         {
        17 |             if (Converter.Config.SlackFlavored)
        18 |             {
        19 |                 throw new SlackUnsupportedTagException(node.Name);
        20 |             }
        21 |             
        22 |             // if table does not have a header row , add empty header row if set in config
        23 |             var useEmptyRowForHeader = this.Converter.Config.TableWithoutHeaderRowHandling ==
        24 |                                        Config.TableWithoutHeaderRowHandlingOption.EmptyRow;
        25 | 
        26 |             var emptyHeaderRow = HasNoTableHeaderRow(node) && useEmptyRowForHeader
        27 |                 ? EmptyHeader(node)
        28 |                 : string.Empty;
        29 | 
        30 |             return $"{Environment.NewLine}{Environment.NewLine}{emptyHeaderRow}{TreatChildren(node)}{Environment.NewLine}";
        31 |         }
        32 | 
        33 |         private static bool HasNoTableHeaderRow(HtmlNode node)
        34 |         {
        35 |             var thNode = node.SelectNodes("//th")?.FirstOrDefault();
        36 |             return thNode == null;
        37 |         }
        38 | 
        39 |         private static string EmptyHeader(HtmlNode node)
        40 |         {
        41 |             var firstRow = node.SelectNodes("//tr")?.FirstOrDefault();
        42 | 
        43 |             if (firstRow == null)
        44 |             {
        45 |                 return string.Empty;
        46 |             }
        47 | 
        48 |             var colCount = firstRow.ChildNodes.Count(n => n.Name.Contains("td"));
        49 | 
        50 |             var headerRowItems = new List();
        51 |             var underlineRowItems = new List();
        52 | 
        53 |             for (var i = 0; i < colCount; i++ )
        54 |             {
        55 |                 headerRowItems.Add("");
        56 |                 underlineRowItems.Add("---");
        57 |             }
        58 | 
        59 |             var headerRow = $"| {string.Join(" | ", headerRowItems)} |{Environment.NewLine}";
        60 |             var underlineRow = $"| {string.Join(" | ", underlineRowItems)} |{Environment.NewLine}";
        61 | 
        62 |             return headerRow + underlineRow;
        63 |         }
        64 |     }
        65 | }
        66 | 
        
        
        --------------------------------------------------------------------------------
        /src/ReverseMarkdown/Converters/Td.cs:
        --------------------------------------------------------------------------------
         1 | using HtmlAgilityPack;
         2 | using System;
         3 | using System.Linq;
         4 | using System.Runtime.CompilerServices;
         5 | using System.Text.RegularExpressions;
         6 | 
         7 | namespace ReverseMarkdown.Converters
         8 | {
         9 |     public class Td : ConverterBase
        10 |     {
        11 |         public Td(Converter converter) : base(converter)
        12 |         {
        13 |             var elements = new [] { "td", "th" };
        14 | 
        15 |             foreach (var element in elements)
        16 |             {
        17 |                 Converter.Register(element, this);
        18 |             }
        19 |         }
        20 | 
        21 |         public override string Convert(HtmlNode node)
        22 |         {
        23 |             if (Converter.Config.SlackFlavored)
        24 |             {
        25 |                 throw new SlackUnsupportedTagException(node.Name);
        26 |             }
        27 |             
        28 |             var content = TreatChildren(node)
        29 |                 .Chomp()
        30 |                 .Replace(Environment.NewLine, "
        "); 31 | 32 | var colSpan = GetColSpan(node); 33 | return string.Concat(Enumerable.Repeat($" {content} |", colSpan)); 34 | } 35 | 36 | /// 37 | /// Given node within td tag, checks if newline should be prepended. Will not prepend if this is the first node after any whitespace 38 | /// 39 | /// 40 | /// 41 | public static bool FirstNodeWithinCell(HtmlNode node) { 42 | var parentName = node.ParentNode.Name; 43 | // If p is at the start of a table cell, no leading newline 44 | if (parentName == "td" || parentName == "th") { 45 | var pNodeIndex = node.ParentNode.ChildNodes.GetNodeIndex(node); 46 | var firstNodeIsWhitespace = node.ParentNode.FirstChild.Name == "#text" && Regex.IsMatch(node.ParentNode.FirstChild.InnerText, @"^\s*$"); 47 | if (pNodeIndex == 0 || (firstNodeIsWhitespace && pNodeIndex == 1)) return true; 48 | } 49 | return false; 50 | } 51 | /// 52 | /// Given node within td tag, checks if newline should be appended. Will not append if this is the last node before any whitespace 53 | /// 54 | /// 55 | /// 56 | public static bool LastNodeWithinCell(HtmlNode node) { 57 | var parentName = node.ParentNode.Name; 58 | if (parentName == "td" || parentName == "th") { 59 | var pNodeIndex = node.ParentNode.ChildNodes.GetNodeIndex(node); 60 | var cellNodeCount = node.ParentNode.ChildNodes.Count; 61 | var lastNodeIsWhitespace = node.ParentNode.LastChild.Name == "#text" && Regex.IsMatch(node.ParentNode.LastChild.InnerText, @"^\s*$"); 62 | if (pNodeIndex == cellNodeCount - 1 || (lastNodeIsWhitespace && pNodeIndex == cellNodeCount - 2)) return true; 63 | } 64 | return false; 65 | } 66 | 67 | private int GetColSpan(HtmlNode node) 68 | { 69 | var colSpan = 1; 70 | 71 | if (Converter.Config.TableHeaderColumnSpanHandling && node.Name == "th") 72 | { 73 | colSpan = node.GetAttributeValue("colspan", 1); 74 | } 75 | return colSpan; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/Text.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text.RegularExpressions; 4 | 5 | using HtmlAgilityPack; 6 | 7 | namespace ReverseMarkdown.Converters 8 | { 9 | public class Text : ConverterBase 10 | { 11 | private readonly Dictionary _escapedKeyChars = new Dictionary(); 12 | 13 | public Text(Converter converter) : base(converter) 14 | { 15 | _escapedKeyChars.Add("*", @"\*"); 16 | _escapedKeyChars.Add("_", @"\_"); 17 | 18 | Converter.Register("#text", this); 19 | } 20 | 21 | public override string Convert(HtmlNode node) 22 | { 23 | return node.InnerText == string.Empty ? TreatEmpty(node) : TreatText(node); 24 | } 25 | 26 | private string TreatText(HtmlNode node) 27 | { 28 | // Prevent < and > from being converted to < and > as this will be interpreted as HTML by markdown 29 | string content = node.InnerText 30 | .Replace("<", "%3C") 31 | .Replace(">", "%3E"); 32 | 33 | content = DecodeHtml(content); 34 | 35 | // Not all renderers support hex encoded characters, so convert back to escaped HTML 36 | content = content 37 | .Replace("%3C", "<") 38 | .Replace("%3E", ">"); 39 | 40 | //strip leading spaces and tabs for text within list item 41 | var parent = node.ParentNode; 42 | 43 | switch (parent.Name) 44 | { 45 | case "table": 46 | case "thead": 47 | case "tbody": 48 | case "ol": 49 | case "ul": 50 | case "th": 51 | case "tr": 52 | content = content.Trim(); 53 | break; 54 | } 55 | 56 | if (parent.Ancestors("th").Any() || parent.Ancestors("td").Any()) 57 | { 58 | content = ReplaceNewlineChars(parent, content); 59 | } 60 | 61 | if (parent.Name != "a" && !Converter.Config.SlackFlavored) 62 | { 63 | content = EscapeKeyChars(content); 64 | } 65 | 66 | content = PreserveKeyCharsWithinBackTicks(content); 67 | 68 | return content; 69 | } 70 | 71 | private string EscapeKeyChars(string content) 72 | { 73 | foreach(var item in _escapedKeyChars) 74 | { 75 | content = content.Replace(item.Key, item.Value); 76 | } 77 | 78 | return content; 79 | } 80 | 81 | private static string TreatEmpty(HtmlNode node) 82 | { 83 | var content = ""; 84 | 85 | var parent = node.ParentNode; 86 | 87 | if (parent.Name == "ol" || parent.Name == "ul") 88 | { 89 | content = ""; 90 | } 91 | else if(node.InnerText == " ") 92 | { 93 | content = " "; 94 | } 95 | 96 | return content; 97 | } 98 | 99 | private static string PreserveKeyCharsWithinBackTicks(string content) 100 | { 101 | var rx = new Regex("`.*?`"); 102 | 103 | content = rx.Replace(content, p => p.Value.Replace(@"\*", "*").Replace(@"\_", "_")); 104 | 105 | return content; 106 | } 107 | 108 | private static string ReplaceNewlineChars(HtmlNode parentNode, string content) 109 | { 110 | if (parentNode.Name != "p" && parentNode.Name != "#document") return content; 111 | 112 | content = content.Replace("\r\n", "
        "); 113 | content = content.Replace("\n", "
        "); 114 | 115 | return content; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/Converters/Tr.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using HtmlAgilityPack; 5 | 6 | namespace ReverseMarkdown.Converters 7 | { 8 | public class Tr : ConverterBase 9 | { 10 | public Tr(Converter converter) : base(converter) 11 | { 12 | Converter.Register("tr", this); 13 | } 14 | 15 | public override string Convert(HtmlNode node) 16 | { 17 | if (Converter.Config.SlackFlavored) 18 | { 19 | throw new SlackUnsupportedTagException(node.Name); 20 | } 21 | 22 | var content = TreatChildren(node).TrimEnd(); 23 | var underline = ""; 24 | 25 | if (string.IsNullOrWhiteSpace(content)) 26 | { 27 | return ""; 28 | } 29 | 30 | // if parent is an ordered or unordered list 31 | // then table need to be indented as well 32 | var indent = IndentationFor(node); 33 | 34 | if (IsTableHeaderRow(node) || UseFirstRowAsHeaderRow(node)) 35 | { 36 | underline = UnderlineFor(node, indent, Converter.Config.TableHeaderColumnSpanHandling); 37 | } 38 | 39 | return $"{indent}|{content}{Environment.NewLine}{underline}"; 40 | } 41 | 42 | private bool UseFirstRowAsHeaderRow(HtmlNode node) 43 | { 44 | var tableNode = node.Ancestors("table").FirstOrDefault(); 45 | var firstRow = tableNode?.SelectSingleNode(".//tr"); 46 | 47 | if (firstRow == null) 48 | { 49 | return false; 50 | } 51 | 52 | var isFirstRow = firstRow == node; 53 | var hasNoHeaderRow = tableNode.SelectNodes(".//th")?.FirstOrDefault() == null; 54 | 55 | return isFirstRow 56 | && hasNoHeaderRow 57 | && Converter.Config.TableWithoutHeaderRowHandling == 58 | Config.TableWithoutHeaderRowHandlingOption.Default; 59 | } 60 | 61 | private static bool IsTableHeaderRow(HtmlNode node) 62 | { 63 | return node.ChildNodes.FindFirst("th") != null; 64 | } 65 | 66 | private static string UnderlineFor(HtmlNode node, string indent, bool tableHeaderColumnSpanHandling) 67 | { 68 | var nodes = node.ChildNodes.Where(x => x.Name == "th" || x.Name == "td").ToList(); 69 | 70 | var cols = new List(); 71 | foreach (var nd in nodes) 72 | { 73 | var colSpan = GetColSpan(nd, tableHeaderColumnSpanHandling); 74 | var styles = StringUtils.ParseStyle(nd.GetAttributeValue("style", "")); 75 | styles.TryGetValue("text-align", out var align); 76 | 77 | string content; 78 | switch (align?.Trim()) 79 | { 80 | case "left": 81 | content = ":---"; 82 | break; 83 | case "right": 84 | content ="---:"; 85 | break; 86 | case "center": 87 | content = ":---:"; 88 | break; 89 | default: 90 | content ="---"; 91 | break; 92 | } 93 | 94 | for (var i = 0; i < colSpan; i++) { 95 | cols.Add(content); 96 | } 97 | } 98 | 99 | var colsAggregated = string.Join(" | ", cols); 100 | 101 | return $"{indent}| {colsAggregated} |{Environment.NewLine}"; 102 | } 103 | 104 | private static int GetColSpan(HtmlNode node, bool tableHeaderColumnSpanHandling) 105 | { 106 | var colSpan = 1; 107 | 108 | if (tableHeaderColumnSpanHandling && node.Name == "th") 109 | { 110 | colSpan = node.GetAttributeValue("colspan", 1); 111 | } 112 | return colSpan; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/ReverseMarkdown.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | ReverseMarkdown is a Html to Markdown converter library in c# 4 | 4.7.0 5 | Babu Annamalai 6 | net46;netstandard2.0;net6.0;net7.0;net8.0;net9.0 7 | https://github.com/mysticmind/reversemarkdown-net 8 | MIT 9 | htmltomarkdown;html2markdown;converthtml;htmlconverter;html;converter;markdown 10 | 11 | 12 | 13 | 14 | 15 | 16 | https://github.com/mysticmind/reversemarkdown-net.git 17 | git 18 | true 19 | true 20 | snupkg 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/StringUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | 7 | namespace ReverseMarkdown 8 | { 9 | public static class StringUtils 10 | { 11 | public static string Chomp(this string content, bool all=false) 12 | { 13 | if (all) 14 | { 15 | return content 16 | .Replace("\r", "") 17 | .Replace("\n", "") 18 | .Trim(); 19 | } 20 | 21 | return content.Trim().TrimEnd('\r', '\n'); 22 | } 23 | 24 | public static IEnumerable ReadLines(this string content) 25 | { 26 | string line; 27 | using (var sr = new StringReader(content)) 28 | while ((line = sr.ReadLine()) != null) 29 | yield return line; 30 | } 31 | 32 | /// 33 | /// Gets scheme for provided uri string to overcome different behavior between windows/linux. https://github.com/dotnet/corefx/issues/1745 34 | /// Assume http for url starting with // 35 | /// Assume file for url starting with / 36 | /// Otherwise give what gives us. 37 | /// If non parseable by Uri, return empty string. Will never return null 38 | /// 39 | /// 40 | public static string GetScheme(string url) { 41 | var isValidUri = Uri.TryCreate(url, UriKind.Absolute, out Uri uri); 42 | //IETF RFC 3986 43 | if (Regex.IsMatch(url, "^//[^/]")) { 44 | return "http"; 45 | } 46 | //Unix style path 47 | else if (Regex.IsMatch(url, "^/[^/]")) { 48 | return "file"; 49 | } 50 | else if (isValidUri) { 51 | return uri.Scheme; 52 | } 53 | else { 54 | return String.Empty; 55 | } 56 | } 57 | 58 | /// 59 | /// Escape/clean characters which would break the [] section of a markdown []() link 60 | /// 61 | public static string EscapeLinkText(string rawText) 62 | { 63 | return Regex.Replace(rawText, @"\r?\n\s*\r?\n", Environment.NewLine, RegexOptions.Singleline) 64 | .Replace("[", @"\[") 65 | .Replace("]", @"\]"); 66 | } 67 | 68 | public static Dictionary ParseStyle(string style) 69 | { 70 | if (string.IsNullOrEmpty(style)) 71 | { 72 | return new Dictionary(); 73 | } 74 | 75 | var styles = style.Split(';'); 76 | return styles.Select(styleItem => styleItem.Split(':')) 77 | .Where(styleParts => styleParts.Length == 2) 78 | .DistinctBy(styleParts => styleParts[0]) 79 | .ToDictionary(styleParts => styleParts[0], styleParts => styleParts[1]); 80 | } 81 | 82 | public static int LeadingSpaceCount(this string content) 83 | { 84 | var leadingSpaces = 0; 85 | foreach (var c in content) 86 | { 87 | if (c == ' ') 88 | { 89 | leadingSpaces++; 90 | } 91 | else 92 | { 93 | break; 94 | } 95 | } 96 | return leadingSpaces; 97 | } 98 | 99 | public static int TrailingSpaceCount(this string content) 100 | { 101 | var trailingSpaces = 0; 102 | for (var i = content.Length - 1; i >= 0; i--) 103 | { 104 | if (content[i] == ' ') 105 | { 106 | trailingSpaces++; 107 | } 108 | else 109 | { 110 | break; 111 | } 112 | } 113 | return trailingSpaces; 114 | } 115 | 116 | public static string EmphasizeContentWhitespaceGuard(this string content, string emphasis, string nextSiblingSpaceSuffix="") 117 | { 118 | var leadingSpaces = new string(' ', content.LeadingSpaceCount()); 119 | var trailingSpaces = new string(' ', content.TrailingSpaceCount()); 120 | 121 | return $"{leadingSpaces}{emphasis}{content.Chomp(all:true)}{emphasis}{(trailingSpaces.Length > 0 ? trailingSpaces : nextSiblingSpaceSuffix)}"; 122 | } 123 | 124 | public static string FixMultipleNewlines(this string markdown) 125 | { 126 | var normalizedMarkdown = Regex.Replace(markdown, @"\r\n|\r|\n", Environment.NewLine); 127 | var pattern = $"{Environment.NewLine}{{2,}}"; 128 | return Regex.Replace(normalizedMarkdown, pattern, Environment.NewLine + Environment.NewLine); 129 | } 130 | 131 | private static IEnumerable DistinctBy(this IEnumerable enumerable, Func keySelector) 132 | { 133 | return enumerable.GroupBy(keySelector).Select(grp => grp.First()); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/UnknownTagException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ReverseMarkdown 4 | { 5 | public class UnknownTagException : Exception 6 | { 7 | public UnknownTagException(string tagName): base($"Unknown tag: {tagName}") 8 | { 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/ReverseMarkdown/UnsupportedTagExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ReverseMarkdown 4 | { 5 | public class UnsupportedTagException : Exception 6 | { 7 | internal UnsupportedTagException(string message) : base(message) 8 | { 9 | } 10 | } 11 | 12 | public class SlackUnsupportedTagException : UnsupportedTagException 13 | { 14 | internal SlackUnsupportedTagException(string tagName) 15 | : base($"<{tagName}> tags cannot be converted to Slack-flavored markdown") 16 | { 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/mdsnippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "Convention": "InPlaceOverwrite", 3 | "ExcludeMarkdownDirectories": [ "ReverseMarkdown.Test" ] 4 | } --------------------------------------------------------------------------------