├── .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 | [](https://github.com/mysticmind/reversemarkdown-net/actions/workflows/ci.yaml) [](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 |
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 | [](https://github.com/mysticmind/reversemarkdown-net/actions/workflows/ci.yaml) [](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 |
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://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 | 
--------------------------------------------------------------------------------
/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 | 
--------------------------------------------------------------------------------
/src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagWithMultilineAltText_ThenEnsureNoBlankLinesInMarkdownAltText.verified.md:
--------------------------------------------------------------------------------
1 | This text has image . Next line of text
--------------------------------------------------------------------------------
/src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagWithRelativeUrl_NotWhitelisted_ThenConvertToMarkdown.verified.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagWithUnixUrl_ConfigHasWhitelist_ThenConvertToMarkdown.verified.md:
--------------------------------------------------------------------------------
1 | 
--------------------------------------------------------------------------------
/src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagWithoutAltText_ThenConvertToMarkdownImageWithoutAltText.verified.md:
--------------------------------------------------------------------------------
1 | This text has image . Next line of text
--------------------------------------------------------------------------------
/src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagWithoutTitle_ThenConvertToMarkdownImageWithoutTitle.verified.md:
--------------------------------------------------------------------------------
1 | This text has image . Next line of text
--------------------------------------------------------------------------------
/src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTag_SchemeIsWhitelisted_ThenConvertToMarkdown.verified.md:
--------------------------------------------------------------------------------
1 | 
--------------------------------------------------------------------------------
/src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTag_SchemeNotWhitelisted_ThenEmptyOutput.verified.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTag_ThenConvertToMarkdownImage.verified.md:
--------------------------------------------------------------------------------
1 | This text has image . 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 | | 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 . 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 | | |
--------------------------------------------------------------------------------
/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 is in an aside tag. 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}Heading text {Environment.NewLine}Content {Environment.NewLine}
";
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.Item1 Item2 ";
548 | return CheckConversion(html);
549 | }
550 |
551 | [Fact]
552 | public Task WhenThereIsOrderedListWithNestedUnorderedList_ThenConvertToMarkdownListWithNestedList()
553 | {
554 | var html =
555 | "This text has ordered list.OuterItem1 Item2 ";
556 | return CheckConversion(html);
557 | }
558 |
559 | [Fact]
560 | public Task WhenThereIsUnorderedListWithNestedOrderedList_ThenConvertToMarkdownListWithNestedList()
561 | {
562 | var html =
563 | "This text has ordered list.OuterItem1InnerItem1 InnerItem2 Item2 ";
564 | return CheckConversion(html);
565 | }
566 |
567 | [Fact]
568 | public Task WhenThereIsWhitespaceAroundNestedLists_PreventBlankLinesWhenConvertingToMarkdownList()
569 | {
570 | var html = $"{Environment.NewLine} ";
571 | html +=
572 | $" OuterItem1{Environment.NewLine} {Environment.NewLine} InnerItem1 {Environment.NewLine} {Environment.NewLine} {Environment.NewLine}";
573 | html += $" Item2 {Environment.NewLine}";
574 | html +=
575 | $" {Environment.NewLine} InnerItem2 {Environment.NewLine} {Environment.NewLine}";
576 | html += $" Item3 {Environment.NewLine}";
577 | html += " ";
578 |
579 | return CheckConversion(html);
580 | }
581 |
582 | [Fact]
583 | public Task
584 | WhenListItemTextContainsLeadingAndTrailingSpacesAndTabs_ThenConvertToMarkdownListItemWithSpacesAndTabsStripped()
585 | {
586 | var html = @" 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{Environment.NewLine}\t\tItem1 {Environment.NewLine}\t{Environment.NewLine}\t\tItem2 ";
595 | return CheckConversion(html);
596 | }
597 |
598 | [Fact]
599 | public Task WhenListContainsMultipleParagraphs_ConvertToMarkdownAndIndentSiblings()
600 | {
601 | var html =
602 | @"
603 |
604 | Paragraph 1
605 | Paragraph 1.1
606 | Paragraph 1.2
607 |
608 | Paragraph 3
";
609 |
610 | return CheckConversion(html);
611 | }
612 |
613 | [Fact]
614 | public Task WhenListContainsParagraphsOutsideItems_ConvertToMarkdownAndIndentSiblings()
615 | {
616 | var html =
617 | @"
618 | Item1
619 | Item 1 additional info
620 | Item2
621 | ";
622 |
623 | return CheckConversion(html);
624 | }
625 |
626 | [Fact]
627 | public Task When_OrderedListIsInTable_LeaveListAsHtml()
628 | {
629 | var html = "";
630 | return CheckConversion(html);
631 | }
632 |
633 | [Fact]
634 | public Task When_UnorderedListIsInTable_LeaveListAsHtml()
635 | {
636 | var html = "";
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 | "col1 col2 col3 data1 data2 data3
";
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 | "data1 data2 data3 data4 data5 data6
";
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 | "data1 data2 data3 data4 data5 data6
";
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 | $"col1 col2 col3 data line1{Environment.NewLine}line2 data2 data3
";
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 = "";
754 | return CheckConversion(html);
755 | }
756 |
757 | [Fact]
758 | public Task WhenTable_ContainsTheadTh_ConvertToGFMTable()
759 | {
760 | var html =
761 | "";
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 | "";
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 = "";
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 = "";
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 = "";
1104 | return CheckConversion(html);
1105 | }
1106 |
1107 | [Fact]
1108 | public Task When_SingleChild_BlockTag_With_Parent_DIV_Ignore_Processing_DIV()
1109 | {
1110 | var html = "";
1111 | return CheckConversion(html);
1112 | }
1113 |
1114 | [Fact]
1115 | public Task When_Table_Within_List_Should_Be_Indented()
1116 | {
1117 | var html =
1118 | "Item1 Item2col1 col2 col3 data1 data2 data3
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 | $"Item1 Item2 test {Environment.NewLine} test 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 | $"Item1 Item2 test {Environment.NewLine} test 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 1line 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 1line 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 = "block1 block2 block3 block4 ";
1217 | return CheckConversion(html);
1218 | }
1219 |
1220 | [Fact]
1221 | public Task When_Consecutive_Em_Tags_Should_Convert_Properly()
1222 | {
1223 | var html = "block1 block2 block3 block4 ";
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 | $"";
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 | wordpress
1273 | hexo & hugo
1274 |
1275 |
1276 |
1277 |
1278 | 搭建要求
1279 | 一台服务器以及运行环境
1280 | 静态生成页面,无需服务器。
1281 |
1282 |
1283 | 性能
1284 | 由于是动态生成页面,可以通过自行配置提高性能,但是仍然无法媲美静态页面
1285 | 几乎无需考虑性能问题
1286 |
1287 |
1288 | 访问速度
1289 | 依赖于服务器配置以及cdn加速。
1290 | 只需考虑cdn加速
1291 |
1292 |
1293 | 功能完善
1294 | 作为强大的cms功能很完善,需要的功能基本可以插件下载直接实现。
1295 | 额外功能也可以通过插件实现,不过稍微需要自行查找以及diy
1296 |
1297 |
1298 | 后台管理
1299 | 现成的后台管理功能,开箱即用
1300 | 由于静态博客,本身没有后台管理,有需求需要自行搜索实现
1301 |
1302 |
1303 |
";
1304 |
1305 | return CheckConversion(html);
1306 | }
1307 |
1308 | [Fact]
1309 | public Task WhenTableHeadingWithAlignmentStyles_ThenTableHeaderShouldHaveProperAlignment()
1310 | {
1311 | var html =
1312 | $"";
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 tes t 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 tes t 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 | "col1 col2 col3 data1 data2.1 data2.2 data3
";
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\nThis 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 = "\nItem 1 \nItem 2 \nItem 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 $"";
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 | }
--------------------------------------------------------------------------------