master cc103aaf8efc cached
246 files
20.9 MB
1.2M tokens
312 symbols
1 requests
Download .txt
Showing preview only (4,851K chars total). Download the full file or copy to clipboard to get everything.
Repository: mysticmind/reversemarkdown-net
Branch: master
Commit: cc103aaf8efc
Files: 246
Total size: 20.9 MB

Directory structure:
gitextract_l7_nm_6w/

├── .github/
│   ├── FUNDING.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── ci.yaml
│       ├── nuget-publish.yaml
│       └── on-push-do-docs.yml
├── .gitignore
├── LICENSE
├── README.md
├── dotnet
├── mdsnippets.json
└── src/
    ├── .editorconfig
    ├── ReverseMarkdown/
    │   ├── Cleaner.cs
    │   ├── Config.cs
    │   ├── Converter.cs
    │   ├── ConverterContext.cs
    │   ├── Converters/
    │   │   ├── A.cs
    │   │   ├── AliasConverter.cs
    │   │   ├── AliasTagConverter.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
    │   │   └── UnknownTagReplacer.cs
    │   ├── Helpers/
    │   │   ├── LineSplitEnumerator.cs
    │   │   ├── StringExtensions.cs
    │   │   ├── StringReplaceValues.cs
    │   │   └── StringUtils.cs
    │   ├── ImageUtils.cs
    │   ├── ReverseMarkdown.csproj
    │   ├── UnknownTagException.cs
    │   └── UnsupportedTagExtension.cs
    ├── ReverseMarkdown.Benchmark/
    │   ├── Benchmark.md
    │   ├── CompareBenchmark.cs
    │   ├── FileHelper.cs
    │   ├── Files/
    │   │   ├── 1000-paragraphs.html
    │   │   ├── 10k-paragraphs.html
    │   │   └── huge.html
    │   ├── Program.cs
    │   └── ReverseMarkdown.Benchmark.csproj
    ├── ReverseMarkdown.Test/
    │   ├── ChildConverterTests.cs
    │   ├── Children/
    │   │   └── IgnoreAWhenHasClass.cs
    │   ├── CommonMarkSpecTests.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.Bug400_MissingSpanSpaceWithItalics.verified.md
    │   ├── ConverterTests.Bug403_unexpectedBehaviourWhenTableBodyRowsWithTHCells.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.EscapeMarkdownCharsInTextProperly.verified.md
    │   ├── 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.WhenTableRowWithDuplicateStyleKeysAfterTrimming_ThenConvertWithoutException.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_WithCaptionAndGithubFlavored_ThenCaptionAppearsAboveTable.verified.md
    │   ├── ConverterTests.WhenTable_WithCaptionAndNoHeaderRow_EmptyRowHandling_ThenCaptionAppearsAboveTable.verified.md
    │   ├── ConverterTests.WhenTable_WithCaptionContainingMarkdownChars_ThenHandleProperly.verified.md
    │   ├── ConverterTests.WhenTable_WithCaptionContainingNestedTags_ThenExtractTextOnly.verified.md
    │   ├── ConverterTests.WhenTable_WithCaptionContainingNewlines_ThenHandleNewlines.verified.md
    │   ├── ConverterTests.WhenTable_WithCaptionContainingWhitespace_ThenTrimWhitespace.verified.md
    │   ├── ConverterTests.WhenTable_WithCaption_ThenCaptionAppearsAboveTable.verified.md
    │   ├── ConverterTests.WhenTable_WithColSpan_TableHeaderColumnSpansHandling_ThenConvertToGFMTable.verified.md
    │   ├── ConverterTests.WhenTable_WithEmptyCaption_ThenConvertNormally.verified.md
    │   ├── ConverterTests.WhenTable_WithoutCaption_ThenConvertNormally.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.WhenThereAreSemanticContainerTags.verified.md
    │   ├── ConverterTests.WhenThereAreStrongTag_ThenConvertToMarkdownDoubleAsterisks.verified.md
    │   ├── ConverterTests.WhenThereHtmlWithHrefAndNoSchema_NotWhitelisted_ThenConvertToPlain.verified.md
    │   ├── ConverterTests.WhenThereHtmlWithHrefAndNoSchema_WhitelistedEmptyString_ThenConvertToMarkdown.verified.md
    │   ├── ConverterTests.WhenThereIsAsideTag.verified.md
    │   ├── ConverterTests.WhenThereIsBase64ImgTag_WithDefaultConfig_ThenIncludeInMarkdown.verified.md
    │   ├── ConverterTests.WhenThereIsBase64ImgTag_WithSaveToFileConfigButNoDirectory_ThenSkipImage.verified.md
    │   ├── ConverterTests.WhenThereIsBase64ImgTag_WithSkipConfig_ThenSkipImage.verified.md
    │   ├── ConverterTests.WhenThereIsBase64JpegImgTag_WithSkipConfig_ThenSkipImage.verified.md
    │   ├── ConverterTests.WhenThereIsBlockquoteTag_ThenConvertToMarkdownBlockquote.verified.md
    │   ├── ConverterTests.WhenThereIsBreakTag_ThenConvertToMarkdownDoubleSpacesCarriageReturn.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.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.WhenThereIsInputListWithGithubFlavoredDisabled_ThenConvertToTypicalMarkdownList.verified.md
    │   ├── ConverterTests.WhenThereIsInputListWithGithubFlavoredEnabled_ThenConvertToMarkdownCheckList.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.WhenUnclosedScriptTag_WithBypassUnknownTags_ThenConvertToMarkdown.verified.md
    │   ├── ConverterTests.WhenUnclosedStyleTag_WithBypassUnknownTags_ThenConvertToMarkdown.verified.md
    │   ├── ConverterTests.When_AlternatingEmptyAndFilledNestedParagraphs_ThenConvertCorrectly.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_ComplexNestedTableIsInTable_LeaveNestedTableAsHtml.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_DeeplyNestedParagraphs_WithMalformedHTML_ThenConvertWithoutHanging.verified.md
    │   ├── ConverterTests.When_DescriptionListTag_ThenConvertToMarkdown_List.verified.md
    │   ├── ConverterTests.When_EmptyNestedParagraphs_ThenConvertCorrectly.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_InterleavedParagraphsAndSpans_ThenConvertCorrectly.verified.md
    │   ├── ConverterTests.When_ManySequentialUnclosedParagraphs_ThenConvertCorrectly.verified.md
    │   ├── ConverterTests.When_MultipleNestedTablesInTable_LeaveAllNestedTablesAsHtml.verified.md
    │   ├── ConverterTests.When_NestedParagraphs_FiveLevelsDeep_ThenConvertCorrectly.verified.md
    │   ├── ConverterTests.When_NestedParagraphs_TenLevelsDeep_ThenConvertCorrectly.verified.md
    │   ├── ConverterTests.When_NestedSpans_FiveLevelsDeep_ThenConvertCorrectly.verified.md
    │   ├── ConverterTests.When_NestedTableIsInTable_LeaveNestedTableAsHtml.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.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_TextContainsBacktickInlineCode_DecodeAngleEntities.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_UnclosedParagraphsWithSpansAndTextNodes_ThenConvertCorrectly.verified.md
    │   ├── ConverterTests.When_Underline_Tag_With_AliasConverter_Register_ThenConvertToItalics.verified.md
    │   ├── ConverterTests.When_Underline_Tag_With_TagAlias_ThenConvertToItalics.verified.md
    │   ├── ConverterTests.When_Underline_Tag_With_UnknownTagsReplacer_ThenConvertToItalics.verified.md
    │   ├── ConverterTests.When_UnorderedListIsInTable_LeaveListAsHtml.verified.md
    │   ├── ConverterTests.cs
    │   ├── ReverseMarkdown.Test.csproj
    │   ├── Snippets.Usage.verified.txt
    │   ├── Snippets.cs
    │   └── TestData/
    │       ├── README.md
    │       ├── cases.json
    │       └── commonmark.json
    └── ReverseMarkdown.sln

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: mysticmind
# ko_fi: 'babuannamalai'
# custom: ['https://www.buymeacoffee.com/babuannamalai']


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: "nuget"
    directory: "/"
    schedule:
      interval: "daily"


================================================
FILE: .github/workflows/ci.yaml
================================================
name: Build
on:
  push:
    branches:
      - master
  pull_request:
    branches:
      - master

env:
  DOTNET_CLI_TELEMETRY_OPTOUT: 1
  DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1

jobs:
  job:
    strategy:
      matrix:
        include:
          - os: ubuntu-latest
            artifact-name: Linux
          #- os: macos-11
          # artifact-name: Darwin
          #- os: windows-2022
          #  artifact-name: Win64 
    runs-on: ${{ matrix.os }} 
    continue-on-error: true
    steps:
      - name: checkout repo
        uses: actions/checkout@v4
      - name: Install .NET 9.0.x
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: 9.0.x
      - name: Display dotnet info
        run: dotnet --list-sdks  
      - name: Run tests
        run: dotnet test src/ReverseMarkdown.Test/ReverseMarkdown.Test.csproj --framework net9.0



================================================
FILE: .github/workflows/nuget-publish.yaml
================================================
name: NuGet Manual Publish

on: [workflow_dispatch]

env:
  config: Release
  DOTNET_CLI_TELEMETRY_OPTOUT: 1
  DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1

jobs:
  publish_job:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Install .NET 9.0.x
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: 9.0.x

      - name: Run Pack
        run: dotnet pack src/ReverseMarkdown/ReverseMarkdown.csproj -c Release
        shell: bash

      - name: Publish to NuGet
        run: |
          find . -name '*.nupkg' -exec dotnet nuget push "{}" -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_API_KEY }} --skip-duplicate \;
          # find . -name '*.snupkg' -exec dotnet nuget push "{}" -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_API_KEY }} \;
        shell: bash




================================================
FILE: .github/workflows/on-push-do-docs.yml
================================================
name: on-push-do-docs
on:
  push:
jobs:
  docs:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Run MarkdownSnippets
      run: |
        dotnet tool install --global MarkdownSnippets.Tool --version 27.0.2
        mdsnippets ${GITHUB_WORKSPACE}
      shell: bash
    - name: Push changes
      run: |
        git config --local user.email "action@github.com"
        git config --local user.name "GitHub Action"
        git commit -m "Docs changes [skip ci]" -a  || echo "nothing to commit"
        remote="https://${GITHUB_ACTOR}:${{secrets.GITHUB_TOKEN}}@github.com/${GITHUB_REPOSITORY}.git"
        branch="${GITHUB_REF:11}"
        git push "${remote}" ${branch} || echo "nothing to push"
      shell: bash


================================================
FILE: .gitignore
================================================
.vs
obj
bin
bin/*
deploy
deploy/*
_ReSharper.*
*.user
*.suo
*.cache
*.Cache
Thumbs.db
**/packages
.idea
*.received.*
.DS_Store


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2015 Babu Annamalai

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.



================================================
FILE: README.md
================================================
# Meet ReverseMarkdown

[![Build status](https://github.com/mysticmind/reversemarkdown-net/actions/workflows/ci.yaml/badge.svg)](https://github.com/mysticmind/reversemarkdown-net/actions/workflows/ci.yaml) [![NuGet Version](https://badgen.net/nuget/v/reversemarkdown)](https://www.nuget.org/packages/ReverseMarkdown/)

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.

If you have used and benefitted from this library. Please feel free to sponsor me!<br>
<a href="https://github.com/sponsors/mysticmind" target="_blank"><img height="30" style="border:0px;height:36px;" src="https://img.shields.io/static/v1?label=GitHub Sponsor&message=%E2%9D%A4&logo=GitHub" border="0" alt="GitHub Sponsor" /></a>

## Features

**Core conversion**
- Supports common HTML tags like h1-h6, p, em, strong, i, b, blockquote, code, img, a, hr, li, ol, ul, table, tr, th, td, br, pre, del, strike, sup, dl, dt, dd, div, and span
- Supports nested lists
- Improved performance with optimized text writer approach and O(1) ancestor lookups

**Markdown flavors**
- GitHub Flavoured Markdown conversion for br, pre, tasklists, and table. Use `var config = new ReverseMarkdown.Config(githubFlavoured:true);`. By default the table will always be converted to Github flavored markdown immaterial of this flag
- Slack Flavoured Markdown conversion. Use `var config = new ReverseMarkdown.Config { SlackFlavored = true };`
- Telegram MarkdownV2 conversion. Use `var config = new ReverseMarkdown.Config { TelegramMarkdownV2 = true };`
- CommonMark-focused output with opt-in flags to preserve compatibility. Use `var config = new ReverseMarkdown.Config { CommonMark = true };` This mode may emit inline HTML for tricky emphasis/link cases unless you disable `CommonMarkUseHtmlInlineTags`.

**Tables**
- Support for nested tables (converted as HTML inside markdown)
- Support for table captions (rendered as paragraph above table)
- Configurable table header handling

**Links and images**
- Smart link handling and URI scheme whitelisting for links and images
- Base64-encoded image handling with options to include as-is, skip, or save to disk

**Extensibility and safety**
- Tag aliasing and unknown tag replacement options for custom conversion behavior
- Pass-through, bypass, drop, or raise strategies for unknown tags
- Pre-tidy handling for malformed unclosed script/style tags

**Formatting controls**
- Configurable list bullets and default code block language
- Comment removal and optional whitespace cleanup

## Usage

Install the package from NuGet using `Install-Package ReverseMarkdown` or clone the repository and build it yourself.

<!-- snippet: Usage -->
<a id='snippet-Usage'></a>
```cs
var converter = new ReverseMarkdown.Converter();

string html = "This a sample <strong>paragraph</strong> from <a href=\"http://test.com\">my site</a>";

string result = converter.Convert(html);
```
<sup><a href='/src/ReverseMarkdown.Test/Snippets.cs#L12-L20' title='Snippet source file'>snippet source</a> | <a href='#snippet-Usage' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Will result in:

<!-- snippet: Snippets.Usage.verified.txt -->
<a id='snippet-Snippets.Usage.verified.txt'></a>
```txt
This a sample **paragraph** from [my site](http://test.com)
```
<sup><a href='/src/ReverseMarkdown.Test/Snippets.Usage.verified.txt#L1-L1' title='Snippet source file'>snippet source</a> | <a href='#snippet-Snippets.Usage.verified.txt' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

The conversion can also be customized:

<!-- snippet: UsageWithConfig -->
<a id='snippet-UsageWithConfig'></a>
```cs
var config = new ReverseMarkdown.Config
{
    // Include the unknown tag completely in the result (default as well)
    UnknownTags = Config.UnknownTagsOption.PassThrough,
    // generate GitHub flavoured markdown, supported for BR, PRE and table tags
    GithubFlavored = true,
    // will ignore all comments
    RemoveComments = true,
    // remove markdown output for links where appropriate
    SmartHrefHandling = true
};

var converter = new ReverseMarkdown.Converter(config);
```
<sup><a href='/src/ReverseMarkdown.Test/Snippets.cs#L28-L44' title='Snippet source file'>snippet source</a> | <a href='#snippet-UsageWithConfig' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

To treat `<pre>` (and `<pre><code>`) content as normal HTML instead of code blocks:

```cs
var config = new ReverseMarkdown.Config
{
    ConvertPreContentAsHtml = true
};

var converter = new ReverseMarkdown.Converter(config);
```

If you need to preserve markdown-like text as literal content (for example `# Heading` or `- Item`), either enable `EscapeMarkdownLineStarts` or use `CommonMark`:

```cs
var config = new ReverseMarkdown.Config
{
    EscapeMarkdownLineStarts = true,
    // or CommonMark = true
};

var converter = new ReverseMarkdown.Converter(config);
```

### Telegram MarkdownV2 mode

When `TelegramMarkdownV2` is enabled, ReverseMarkdown applies Telegram-compatible formatting and escaping rules:

```cs
var converter = new ReverseMarkdown.Converter(new ReverseMarkdown.Config
{
    TelegramMarkdownV2 = true
});

var html = "This is <strong>bold</strong>, <em>italic</em>, <del>strikethrough</del> and <a href=\"https://example.com/path_(one)?q=1)2\">a_b[c]</a>";
var result = converter.Convert(html);
// This is *bold*, _italic_, ~strikethrough~ and [a\_b\[c\]](https://example.com/path_(one\)?q=1\)2)
```

Notes:

- Text and link labels escape Telegram-reserved characters.
- Ordered and unordered list markers are escaped (`1\.` and `\-`).
- `<img>` falls back to a link label (for example `[Image: alt](url)`).
- `<table>` falls back to a preformatted code block representation.
- `<sup>` falls back to caret notation (for example `x^2`).

## Configuration options

* `DefaultCodeBlockLanguage` - Option to set the default code block language for Github style markdown if class based language markers are not available
* `GithubFlavored` - Github style markdown for br, pre and table. Default is false
* `SlackFlavored` - Slack style markdown formatting. When enabled, uses `*` for bold, `_` for italic, `~` for strikethrough, and `•` for list bullets. Default is false
* `TelegramMarkdownV2` - Telegram MarkdownV2 formatting and escaping rules. When enabled, output escapes Telegram-reserved characters and uses Telegram-compatible emphasis and link syntax. For unsupported Telegram constructs, ReverseMarkdown falls back to readable text (`<img>` to link label, `<table>` to preformatted block, `<sup>` to caret notation).
* `CommonMark` - Enable CommonMark-focused output rules. Default is false
* `CommonMarkUseHtmlInlineTags` - When CommonMark is enabled, emit HTML for inline tags (`em`, `strong`, `a`, `img`) to avoid delimiter edge cases. Default is true
* `CommonMarkIntrawordEmphasisSpacing` - When CommonMark is enabled, insert spaces to avoid intraword emphasis. Default is false
  * Note: CommonMark is best used on its own. Combining `CommonMark` with `GithubFlavored` can produce mixed output; keep them separate unless you explicitly want that behavior.
* `EscapeMarkdownLineStarts` - Escape markdown line starts (headings, lists, block markers) in plain text output. Default is false
  * Note: If you need to preserve markdown-like text as literal content, enable `EscapeMarkdownLineStarts` or use `CommonMark`.
* `OutputLineEnding` - Output line endings used in generated markdown. Default is `Environment.NewLine`
* `CleanupUnnecessarySpaces` - Cleanup unnecessary spaces in the output. Default is true
* `SuppressDivNewlines` - Removes prefixed newlines from `div` tags. Default is false
* `ConvertPreContentAsHtml` - Treat `<pre>` (and `<pre><code>`) content as normal HTML instead of a code block. Default is false
* `ListBulletChar` - Allows you to change the bullet character. Default value is `-`. Some systems expect the bullet character to be `*` rather than `-`, this config allows you to change it. Note: This option is ignored when `SlackFlavored` is enabled
* `RemoveComments` - Remove comment tags with text. Default is false
* `SmartHrefHandling` - How to handle `<a>` tag href attribute
  * `false` - Outputs `[{name}]({href}{title})` even if the name and href is identical. This is the default option.
  * `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.

    If `href` contains `http/https` protocol, and `name` doesn't but otherwise are the same, output `href` only

    If `tel:` or `mailto:` scheme, but afterwards identical with name, output `name` only.
* `UnknownTags` - handle unknown tags.
  * `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
  * `UnknownTagsOption.Drop` - Drop the unknown tag and its content
  * `UnknownTagsOption.Bypass` - Ignore the unknown tag but try to convert its content
  * `UnknownTagsOption.Raise` - Raise an error to let you know
* `UnknownTagsReplacer` - Optional replacements for unknown tags. Key is tag name and value is the markdown wrapper used as prefix/suffix around converted content (example: `{ ["u"] = "*" }`).
* `TagAliases` - Optional alias map to treat a tag as another tag during conversion (example: `{ ["u"] = "em" }`).
* `PassThroughTags` - Pass a list of tags to pass through as-is without any processing.
* `WhitelistUriSchemes` - Specify which schemes (without trailing colon) are to be allowed for `<a>` and `<img>` tags. Others will be bypassed (output text or nothing). By default allows everything.

  If `string.Empty` provided and when `href` or `src` schema couldn't be determined - whitelists

  Schema is determined by `Uri` class, with exception when url begins with `/` (file schema) and `//` (http schema)
* `TableWithoutHeaderRowHandling` - handle table without header rows
  * `TableWithoutHeaderRowHandlingOption.Default` - First row will be used as header row (default)
  * `TableWithoutHeaderRowHandlingOption.EmptyRow` - An empty row will be added as the header row
* `TableHeaderColumnSpanHandling` - Set this flag to handle or process table header column with column spans. Default is true
* `Base64Images` - Control how base64-encoded images (inline data URIs) are handled during conversion
  * `Base64ImageHandling.Include` - Include base64-encoded images in the markdown output as-is (default behavior)
  * `Base64ImageHandling.Skip` - Skip/ignore base64-encoded images entirely
  * `Base64ImageHandling.SaveToFile` - Save base64-encoded images to disk and reference the saved file path in markdown. Requires `Base64ImageSaveDirectory` to be set
* `Base64ImageSaveDirectory` - When `Base64Images` is set to `SaveToFile`, specifies the directory path where images should be saved
* `Base64ImageFileNameGenerator` - When `Base64Images` is set to `SaveToFile`, this function generates a filename for each saved image. The function receives the image index (int) and MIME type (string), and should return a filename without extension. If not specified, images will be named as `image_0`, `image_1`, etc.

### Custom converter alias

You can also register a tag to reuse another tag's converter directly:

```cs
var converter = new ReverseMarkdown.Converter();
converter.Register("u", new ReverseMarkdown.Converters.AliasConverter(converter, "em"));
```

### Base64 Image Handling Examples

ReverseMarkdown provides flexible options for handling base64-encoded images (inline data URIs) during HTML to Markdown conversion.

**Include Base64 Images (Default)**

By default, base64-encoded images are included in the markdown output as-is:

<!-- snippet: Base64ImageInclude -->
<a id='snippet-Base64ImageInclude'></a>
```cs
var converter = new ReverseMarkdown.Converter();
string html = "<img src=\"data:image/png;base64,iVBORw0KGg...\" alt=\"Sample Image\"/>";
string result = converter.Convert(html);
// Output: ![Sample Image](data:image/png;base64,iVBORw0KGg...)
```
<sup><a href='/src/ReverseMarkdown.Test/Snippets.cs#L50-L57' title='Snippet source file'>snippet source</a> | <a href='#snippet-Base64ImageInclude' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

**Skip Base64 Images**

To ignore base64-encoded images entirely:

<!-- snippet: Base64ImageSkip -->
<a id='snippet-Base64ImageSkip'></a>
```cs
var config = new ReverseMarkdown.Config
{
    Base64Images = Config.Base64ImageHandling.Skip
};
var converter = new ReverseMarkdown.Converter(config);
string html = "<img src=\"data:image/png;base64,iVBORw0KGg...\" alt=\"Sample Image\"/>";
string result = converter.Convert(html);
// Output: (empty - image is skipped)
```
<sup><a href='/src/ReverseMarkdown.Test/Snippets.cs#L63-L74' title='Snippet source file'>snippet source</a> | <a href='#snippet-Base64ImageSkip' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

**Save Base64 Images to Disk**

To extract and save base64-encoded images to disk:

<!-- snippet: Base64ImageSaveToFile -->
<a id='snippet-Base64ImageSaveToFile'></a>
```cs
var config = new ReverseMarkdown.Config
{
    Base64Images = Config.Base64ImageHandling.SaveToFile,
    Base64ImageSaveDirectory = "/path/to/images"
};
var converter = new ReverseMarkdown.Converter(config);
string html = "<img src=\"data:image/png;base64,iVBORw0KGg...\" alt=\"Sample Image\"/>";
string result = converter.Convert(html);
// Output: ![Sample Image](/path/to/images/image_0.png)
// Image file saved to: /path/to/images/image_0.png
```
<sup><a href='/src/ReverseMarkdown.Test/Snippets.cs#L80-L93' title='Snippet source file'>snippet source</a> | <a href='#snippet-Base64ImageSaveToFile' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

**Custom Filename Generator**

You can provide a custom filename generator for saved images:

<!-- snippet: Base64ImageCustomFilename -->
<a id='snippet-Base64ImageCustomFilename'></a>
```cs
var config = new ReverseMarkdown.Config
{
    Base64Images = Config.Base64ImageHandling.SaveToFile,
    Base64ImageSaveDirectory = "/path/to/images",
    Base64ImageFileNameGenerator = (index, mimeType) => 
    {
        var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
        return $"converted_{timestamp}_{index}";
    }
};
var converter = new ReverseMarkdown.Converter(config);
// Images will be saved as: converted_20260108_143022_0.png, converted_20260108_143022_1.jpg, etc.
```
<sup><a href='/src/ReverseMarkdown.Test/Snippets.cs#L99-L114' title='Snippet source file'>snippet source</a> | <a href='#snippet-Base64ImageCustomFilename' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

**Supported Image Formats:**
- PNG (`image/png`)
- JPEG (`image/jpeg`, `image/jpg`)
- GIF (`image/gif`)
- BMP (`image/bmp`)
- TIFF (`image/tiff`)
- WebP (`image/webp`)
- SVG (`image/svg+xml`)

## Breaking Changes

### v5.0.0

**Configuration Changes:**
* `WhitelistUriSchemes` - Changed from `string[]` to `HashSet<string>` (read-only property). Use `.Add()` method to add schemes instead of array assignment
* `PassThroughTags` - Changed from `string[]` to `HashSet<string>`

**API Changes:**
* `IConverter` interface signature changed from `string Convert(HtmlNode node)` to `void Convert(TextWriter writer, HtmlNode node)`. If you have custom converters, you'll need to update them to write to the TextWriter instead of returning a string

**Target Framework Changes:**

* Removed support for legacy and end-of-life .NET versions. Only actively supported .NET versions are now targeted i.e. .NET 8, .NET 9 and .NET 10.

### v2.0.0

* `UnknownTags` config has been changed to an enumeration

## Acknowledgements

This library's initial implementation ideas from the Ruby based Html to Markdown converter [xijo/reverse_markdown](https://github.com/xijo/reverse_markdown).

## Copyright

Copyright © Babu Annamalai

## License

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.


================================================
FILE: dotnet
================================================


================================================
FILE: mdsnippets.json
================================================
{
  "Convention": "InPlaceOverwrite",
  "ExcludeMarkdownDirectories": [ "ReverseMarkdown.Test" ]
}

================================================
FILE: src/.editorconfig
================================================
root = true

[*.cs]
indent_style = space
indent_size = 4


================================================
FILE: src/ReverseMarkdown/Cleaner.cs
================================================
using System;
using System.Text.RegularExpressions;
using ReverseMarkdown.Helpers;


namespace ReverseMarkdown;

public static partial class Cleaner {
    [GeneratedRegex(@"\*(\s\*)+")]
    private static partial Regex SlackBoldCleaner();

    [GeneratedRegex(@"_(\s_)+")]
    private static partial Regex SlackItalicCleaner();

    [GeneratedRegex(@"[\u0020\u00A0]")]
    private static partial Regex NonBreakingSpaces();

    private static readonly StringReplaceValues TagBorders = new() {
        ["\n\t"] = string.Empty,
        ["\r\n\t"] = string.Empty,
    };

    private static string CleanTagBorders(string content)
    {
        // content from some htl editors such as CKEditor emits newline and tab between tags, clean that up
        return content.Replace(TagBorders);
    }

    private static string NormalizeSpaceChars(string content)
    {
        // replace unicode and non-breaking spaces to normal space
        content = NonBreakingSpaces().Replace(content, " ");
        return content;
    }

    public static string PreTidy(string content, bool removeComments)
    {
        content = NormalizeSpaceChars(content);
        content = FixUnclosedTag(content, "script");
        content = FixUnclosedTag(content, "style");
        content = CleanTagBorders(content);

        return content;
    }

    private static string FixUnclosedTag(string content, string tagName)
    {
        if (string.IsNullOrWhiteSpace(content)) {
            return content;
        }

        var openRegex = new Regex($@"<\s*{Regex.Escape(tagName)}\b[^>]*>", RegexOptions.IgnoreCase);
        var closeRegex = new Regex($@"</\s*{Regex.Escape(tagName)}\s*>", RegexOptions.IgnoreCase);

        return openRegex.Replace(content, match =>
        {
            var remaining = content.Substring(match.Index);
            return closeRegex.IsMatch(remaining) ? match.Value : string.Empty;
        });
    }

    public static string SlackTidy(string content)
    {
        // Slack's escaping rules depend on whether the key characters appear in
        // next to word characters or not.
        content = SlackBoldCleaner().Replace(content, "*");
        content = SlackItalicCleaner().Replace(content, "_");

        return content;
    }
}


================================================
FILE: src/ReverseMarkdown/Config.cs
================================================
using System;
using System.Collections.Generic;

namespace ReverseMarkdown
{
    public class Config
    {
        public UnknownTagsOption UnknownTags { get; set; } = UnknownTagsOption.PassThrough;

        public bool GithubFlavored { get; set; } = false;

        public bool SlackFlavored { get; set; } = false;

        /// <summary>
        /// Telegram MarkdownV2 conversion.
        /// </summary>
        public bool TelegramMarkdownV2 { get; set; } = false;

        /// <summary>
        /// Enable CommonMark compatible emphasis handling (avoid intraword emphasis by inserting spaces).
        /// </summary>
        public bool CommonMark { get; set; } = false;

        /// <summary>
        /// When CommonMark is enabled, insert spaces to avoid intraword emphasis.
        /// </summary>
        public bool CommonMarkIntrawordEmphasisSpacing { get; set; } = false;

        /// <summary>
        /// When CommonMark is enabled, emit HTML for inline tags (em/strong/a/img) to avoid delimiter edge cases.
        /// </summary>
        public bool CommonMarkUseHtmlInlineTags { get; set; } = true;

        /// <summary>
        /// Escape markdown line starts (headings, lists, block markers) in plain text output.
        /// </summary>
        public bool EscapeMarkdownLineStarts { get; set; } = false;

        /// <summary>
        /// Output line endings to use for the generated markdown.
        /// Defaults to <see cref="Environment.NewLine" />.
        /// </summary>
        public string OutputLineEnding { get; set; } = Environment.NewLine;

        public bool SuppressDivNewlines { get; set; } = false;

        public bool RemoveComments { get; set; } = false;

        /// <summary>
        /// When enabled, treat &lt;pre&gt; (and &lt;pre&gt;&lt;code&gt;) content as normal HTML
        /// instead of converting it to a code block.
        /// </summary>
        public bool ConvertPreContentAsHtml { get; set; } = false;

        /// <summary>
        /// Specify which schemes (without trailing colon) are to be allowed for &lt;a&gt; and &lt;img&gt; tags. Others will be bypassed. By default, allows everything.
        /// <para>If <see cref="string.Empty" /> provided and when href schema couldn't be determined - whitelists</para>
        /// </summary>
        public HashSet<string> WhitelistUriSchemes { get; } = new (StringComparer.OrdinalIgnoreCase);

        /// <summary>
        /// How to handle &lt;a&gt; tag href attribute
        /// <para>false - Outputs [{name}]({href}{title}) even if name and href is identical. This is the default option.</para>
        /// true - If name and href equals, outputs just the `name`. Note that if Uri is not well formed as per <see cref="Uri.IsWellFormedUriString"/> (i.e. string is not correctly escaped like `http://example.com/path/file name.docx`) then markdown syntax will be used anyway.
        /// <para>If href contains http/https protocol, and name doesn't but otherwise are the same, output href only</para>
        /// If tel: or mailto: scheme, but afterward identical with name, output name only.
        /// </summary>
        public bool SmartHrefHandling { get; set; } = false;

        public TableWithoutHeaderRowHandlingOption TableWithoutHeaderRowHandling { get; set; } =
            TableWithoutHeaderRowHandlingOption.Default;

        private char _listBulletChar = '-';

        /// <summary>
        /// Option to set a different bullet character for un-ordered lists
        /// </summary>
        /// <remarks>
        /// This option is ignored when <see cref="SlackFlavored"/> is enabled.
        /// </remarks>
        public char ListBulletChar
        {
            get => SlackFlavored ? '•' : _listBulletChar;
            set => _listBulletChar = value;
        }

        /// <summary>
        /// Option to set a default GFM code block language if class based language markers are not available
        /// </summary>
        public string? DefaultCodeBlockLanguage { get; set; }

        /// <summary>
        /// Option to pass a list of tags to pass through as is without any processing
        /// </summary>
        public HashSet<string> PassThroughTags { get; set; } = [];

        /// <summary>
        /// Optional replacements for unknown tags. The key is the tag name and the value is the
        /// markdown wrapper to use as both prefix and suffix around converted content.
        /// </summary>
        public Dictionary<string, string> UnknownTagsReplacer { get; } = new (StringComparer.OrdinalIgnoreCase);

        /// <summary>
        /// Optional alias map to treat a tag as another tag during conversion.
        /// </summary>
        public Dictionary<string, string> TagAliases { get; } = new (StringComparer.OrdinalIgnoreCase);

        public enum UnknownTagsOption
        {
            /// <summary>
            /// Include the unknown tag completely into the result. That is, the tag along with the text will be left in output.
            /// </summary>
            PassThrough,

            /// <summary>
            ///  Drop the unknown tag and its content
            /// </summary>
            Drop,

            /// <summary>
            /// Ignore the unknown tag but try to convert its content
            /// </summary>
            Bypass,

            /// <summary>
            /// Raise an error to let you know
            /// </summary>
            Raise
        }

        public enum TableWithoutHeaderRowHandlingOption
        {
            /// <summary>
            /// By default, first row will be used as header row
            /// </summary>
            Default,

            /// <summary>
            /// An empty row will be added as the header row
            /// </summary>
            EmptyRow
        }

        public enum Base64ImageHandling
        {
            /// <summary>
            /// Include base64-encoded images in the markdown output (default behavior)
            /// </summary>
            Include,

            /// <summary>
            /// Skip/ignore base64-encoded images entirely
            /// </summary>
            Skip,

            /// <summary>
            /// Save base64-encoded images to disk and reference the saved file path in markdown
            /// Requires Base64ImageSaveDirectory to be set
            /// </summary>
            SaveToFile
        }

        /// <summary>
        /// Set this flag to handle table header column with column spans
        /// </summary>
        public bool TableHeaderColumnSpanHandling { get; set; } = true;

        public bool CleanupUnnecessarySpaces { get; set; } = true;

        /// <summary>
        /// Option to control how base64-encoded images are handled during conversion
        /// </summary>
        public Base64ImageHandling Base64Images { get; set; } = Base64ImageHandling.Include;

        /// <summary>
        /// When Base64Images is set to SaveToFile, this specifies the directory path where images should be saved
        /// </summary>
        public string? Base64ImageSaveDirectory { get; set; }

        /// <summary>
        /// When Base64Images is set to SaveToFile, this function generates a filename for each saved image
        /// The function receives the image index and MIME type, and should return a filename without extension
        /// </summary>
        public Func<int, string, string>? Base64ImageFileNameGenerator { get; set; }


        /// <summary>
        /// Determines whether url is allowed: WhitelistUriSchemes contains no elements or contains passed url.
        /// </summary>
        /// <param name="scheme">Scheme name without trailing colon</param>
        internal bool IsSchemeWhitelisted(string scheme)
        {
            if (scheme == null) throw new ArgumentNullException(nameof(scheme));
            var isSchemeAllowed = WhitelistUriSchemes.Count == 0 ||
                                  WhitelistUriSchemes.Contains(scheme);
            return isSchemeAllowed;
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converter.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using HtmlAgilityPack;
using ReverseMarkdown.Converters;
using ReverseMarkdown.Helpers;


namespace ReverseMarkdown {
    /// <summary>
    /// Converts HTML to Markdown. Thread-safe for concurrent use.
    /// </summary>
    public class Converter {
        protected readonly IDictionary<string, IConverter> Converters = new Dictionary<string, IConverter>();
        protected readonly IConverter PassThroughTagsConverter;
        protected readonly IConverter DropTagsConverter;
        protected readonly IConverter ByPassTagsConverter;
        private readonly Dictionary<string, UnknownTagReplacer> _unknownTagReplacerConverters = new(StringComparer.OrdinalIgnoreCase);
        private readonly Dictionary<string, AliasTagConverter> _tagAliasConverters = new(StringComparer.OrdinalIgnoreCase);

        private readonly System.Threading.AsyncLocal<ConverterContext?> _context = new();

        public ConverterContext Context => _context.Value ??= new ConverterContext();

        public Converter() : this(new Config())
        {
        }

        public Converter(Config config) : this(config, null)
        {
        }

        public Converter(Config config, params Assembly[]? additionalAssemblies)
        {
            Config = config;

            var assemblies = new List<Assembly>() {
                typeof(IConverter).GetTypeInfo().Assembly
            };

            if (!(additionalAssemblies is null))
                assemblies.AddRange(additionalAssemblies);

            var types = new List<Type>();
            // instantiate all converters excluding the unknown tags converters
            foreach (var assembly in assemblies) {
                foreach (var converterType in assembly.GetTypes()
                             .Where(t => t.GetTypeInfo().GetInterfaces().Contains(typeof(IConverter)) &&
                                         !t.GetTypeInfo().IsAbstract
                                         && t != typeof(PassThrough)
                                         && t != typeof(Drop)
                                         && t != typeof(ByPass))) {
                    // Check to see if any existing types are children/equal to
                    // the type to add.
                    if (types.Any(e => converterType.IsAssignableFrom(e)))
                        // If they are, ignore the type.
                        continue;

                    // See if there is a type that is a parent of the
                    // current type.
                    var toRemove = types.FirstOrDefault(e => e.IsAssignableFrom(converterType));
                    // if there is ...
                    if (!(toRemove is null))
                        // ... remove the parent.
                        types.Remove(toRemove);

                    // finally, add the type.
                    types.Add(converterType);
                }
            }

            // For each type to register ...
            foreach (var converterType in types) {
                var ctor = converterType.GetConstructor(new[] { typeof(Converter) });
                if (ctor is null) {
                    continue;
                }

                // ... activate them
                Activator.CreateInstance(converterType, this);
            }

            // register the unknown tags converters
            PassThroughTagsConverter = new PassThrough(this);
            DropTagsConverter = new Drop(this);
            ByPassTagsConverter = new ByPass(this);
        }

        public Config Config { get; protected set; }

        public virtual string Convert(string html)
        {
            using var _ = EnsureContext();

            html = html.ReplaceLineEndings("\n");

            if (Config.CommonMark && LooksLikeCommonMarkHtmlBlock(html)) {
                return ApplyOutputLineEndings(html);
            }

            if (Config.CommonMark) {
                var trimmed = html.TrimStart('\uFEFF', ' ', '\t', '\r', '\n');
                if (trimmed.StartsWith("</", StringComparison.Ordinal) ||
                    html.Contains("<!--", StringComparison.Ordinal) ||
                    html.Contains("<![CDATA[", StringComparison.Ordinal)) {
                    return ApplyOutputLineEndings(html);
                }

                var paragraphTrimmed = html.Trim();
                if (paragraphTrimmed.StartsWith("<p>", StringComparison.OrdinalIgnoreCase) &&
                    paragraphTrimmed.EndsWith("</p>", StringComparison.OrdinalIgnoreCase)) {
                    var inner = paragraphTrimmed.Substring(3, paragraphTrimmed.Length - 7);
                    if (inner.TrimStart().StartsWith("</", StringComparison.Ordinal)) {
                        return ApplyOutputLineEndings(inner);
                    }
                }
            }

            if (Config.CommonMark) {
                html = html.Replace("\u00A0", "&nbsp;");
            }

            html = Cleaner.PreTidy(html, Config.RemoveComments);

            var doc = new HtmlDocument();
            doc.LoadHtml(html);

            var root = doc.DocumentNode;

            // ensure to start from body and ignore head etc
            if (root.Descendants("body").Any()) {
                root = root.SelectSingleNode("//body");
            }

            var result = ConvertNode(root);

            if (!Config.CommonMark) {
                // cleanup multiple new lines
                result = Regex.Replace(result, @"(^\p{Zs}*(\r\n|\n)){2,}", Environment.NewLine, RegexOptions.Multiline);
            }

            if (Config.SlackFlavored) {
                result = Cleaner.SlackTidy(result);
            }

            if (!Config.CleanupUnnecessarySpaces) {
                return ApplyOutputLineEndings(result);
            }

            if (Config.CommonMark) {
                result = result.TrimEnd();
                result = result.TrimStart('\r', '\n');
                return ApplyOutputLineEndings(result);
            }

            return ApplyOutputLineEndings(result.Trim().FixMultipleNewlines());
        }

        private string ApplyOutputLineEndings(string content)
        {
            var lineEnding = string.IsNullOrEmpty(Config.OutputLineEnding)
                ? Environment.NewLine
                : Config.OutputLineEnding;
            return content.ReplaceLineEndings(lineEnding);
        }

        private static bool LooksLikeCommonMarkHtmlBlock(string html)
        {
            if (string.IsNullOrWhiteSpace(html)) {
                return false;
            }

            var trimmed = html.TrimStart('\uFEFF', ' ', '\t', '\r', '\n');
            if (trimmed.StartsWith("<!--", StringComparison.Ordinal) ||
                trimmed.StartsWith("<?", StringComparison.Ordinal) ||
                trimmed.StartsWith("<!", StringComparison.Ordinal)) {
                return true;
            }

            return HtmlBlockStart.IsMatch(trimmed);
        }

        private static readonly Regex HtmlBlockStart = new(
            @"^\s*<\/?(div|table|pre|script|style|iframe|article|section|header|footer|nav|aside|blockquote|h[1-6]|hr|details|summary|figure|figcaption|main|form|center|address|body|html|head|link|meta|title|tbody|thead|tfoot|tr|td|th)\b",
            RegexOptions.IgnoreCase | RegexOptions.Compiled
        );

        public virtual void Register(string tagName, IConverter converter)
        {
            Converters[tagName] = converter;
        }

        internal int MesureCapacity(HtmlNode node)
        {
            var startIndex = Math.Max(0, node.InnerStartIndex);
            var endNode = (node.EndNode ?? node.LastChild);
            var endIndex = Math.Max(startIndex, endNode.OuterStartIndex + endNode.OuterLength);
            var length = endIndex - startIndex;
            if (length < 10) length = 100;

            var capacity = (int) (length * 0.8);
            return capacity;
        }

        internal StringWriter CreateWriter(HtmlNode node)
        {
            var capacity = MesureCapacity(node);
            // TODO : use a pooled StringBuilder to further cut down memory allocations
            // important: find a way to select the best instance form pool based on the capacity needed
            var sb = new StringBuilder(capacity);
            var writer = new StringWriter(sb);
            return writer;
        }

        public virtual string ConvertNode(HtmlNode node)
        {
            using var _ = EnsureContext();
            using var writer = CreateWriter(node);
            ConvertNode(writer, node);
            return writer.GetStringBuilder().ToString();
        }

        public virtual void ConvertNode(TextWriter writer, HtmlNode node)
        {
            using var _ = EnsureContext();
            var converter = Lookup(node.Name);
            Context.Enter(node);
            converter.Convert(writer, node);
            Context.Leave(node);
        }

        public virtual IConverter Lookup(string tagName)
        {
            // if a tag is in the pass through list then use the pass through tags converter
            if (Config.PassThroughTags.Contains(tagName)) {
                return PassThroughTagsConverter;
            }

            if (Converters.TryGetValue(tagName, out var converter)) {
                return converter;
            }

            var aliasTargetTag = ResolveAliasTarget(tagName);
            if (aliasTargetTag is not null) {
                return GetAliasConverter(tagName, aliasTargetTag);
            }

            if (Config.UnknownTagsReplacer.TryGetValue(tagName, out var replacement)) {
                return GetUnknownTagReplacer(tagName, replacement);
            }

            return GetDefaultConverter(tagName);
        }

        private IConverter GetUnknownTagReplacer(string tagName, string replacement)
        {
            if (_unknownTagReplacerConverters.TryGetValue(tagName, out var converter) &&
                string.Equals(converter.Replacement, replacement, StringComparison.Ordinal)) {
                return converter;
            }

            var replacer = new UnknownTagReplacer(this, tagName, replacement);
            _unknownTagReplacerConverters[tagName] = replacer;
            return replacer;
        }

        private IConverter GetAliasConverter(string tagName, string targetTag)
        {
            if (_tagAliasConverters.TryGetValue(tagName, out var converter) &&
                string.Equals(converter.TargetTag, targetTag, StringComparison.OrdinalIgnoreCase)) {
                return converter;
            }

            var alias = new AliasTagConverter(this, tagName, targetTag);
            _tagAliasConverters[tagName] = alias;
            return alias;
        }

        private string? ResolveAliasTarget(string tagName)
        {
            if (!Config.TagAliases.TryGetValue(tagName, out var targetTag) || string.IsNullOrWhiteSpace(targetTag)) {
                return null;
            }

            var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { tagName };

            while (true) {
                if (string.Equals(targetTag, tagName, StringComparison.OrdinalIgnoreCase)) {
                    return null;
                }

                if (!visited.Add(targetTag)) {
                    return null;
                }

                if (!Config.TagAliases.TryGetValue(targetTag, out var nextTag) || string.IsNullOrWhiteSpace(nextTag)) {
                    return targetTag;
                }

                targetTag = nextTag;
            }
        }

        private IConverter GetDefaultConverter(string tagName)
        {
            return Config.UnknownTags switch {
                Config.UnknownTagsOption.PassThrough => PassThroughTagsConverter,
                Config.UnknownTagsOption.Drop => DropTagsConverter,
                Config.UnknownTagsOption.Bypass => ByPassTagsConverter,
                _ => throw new UnknownTagException(tagName)
            };
        }

        private IDisposable EnsureContext()
        {
            if (_context.Value is not null) {
                return NoopDisposable.Instance;
            }

            _context.Value = new ConverterContext();
            return new ContextScope(_context);
        }

        private sealed class ContextScope : IDisposable {
            private readonly System.Threading.AsyncLocal<ConverterContext?> _scope;

            public ContextScope(System.Threading.AsyncLocal<ConverterContext?> scope)
            {
                _scope = scope;
            }

            public void Dispose()
            {
                _scope.Value = null;
            }
        }

        private sealed class NoopDisposable : IDisposable {
            public static readonly NoopDisposable Instance = new();

            private NoopDisposable()
            {
            }

            public void Dispose()
            {
            }
        }
    }
}


================================================
FILE: src/ReverseMarkdown/ConverterContext.cs
================================================
using System;
using System.Collections.Generic;
using HtmlAgilityPack;


namespace ReverseMarkdown;

public class ConverterContext {
    private readonly Dictionary<string, List<HtmlNode>> _ancestors = new();

    /// <summary>
    /// Enter a node to track ancestors with time complexity O(1)
    /// </summary>
    public void Enter(HtmlNode node)
    {
        var parent = node.ParentNode;
        if (parent == null!) return;

        if (_ancestors.TryGetValue(parent.Name, out var list)) {
            list.Add(parent);
        }
        else {
            _ancestors[parent.Name] = [parent];
        }
    }

    /// <summary>
    /// Leave a node to track ancestors with time complexity O(1)
    /// </summary>
    public void Leave(HtmlNode node)
    {
        var parent = node.ParentNode;
        if (parent == null!) return;

        if (_ancestors.TryGetValue(parent.Name, out var list)) {
            list.Remove(parent);
        }
        else {
            throw new InvalidOperationException("Node was not entered");
        }
    }

    /// <summary>
    /// Ancestors lookup with time complexity O(1)
    /// </summary>
    public bool AncestorsAny(string nodeName)
    {
        return _ancestors.GetValueOrDefault(nodeName)?.Count > 0;
    }

    /// <summary>
    /// Ancestors count with time complexity O(1)
    /// </summary>
    public int AncestorsCount(string nodeName)
    {
        return _ancestors.GetValueOrDefault(nodeName)?.Count ?? 0;
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/A.cs
================================================
using System;
using System.IO;
using System.Linq;
using HtmlAgilityPack;
using ReverseMarkdown.Helpers;


namespace ReverseMarkdown.Converters {
    public class A : ConverterBase {
        public A(Converter converter) : base(converter)
        {
            Converter.Register("a", this);
        }

        private readonly StringReplaceValues _escapeValues = new() {
            [" "] = "%20",
            ["("] = "%28",
            [")"] = "%29",
        };

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            var isCommonMark = Converter.Config.CommonMark;
            var isTelegram = Converter.Config.TelegramMarkdownV2;
            var name = TreatChildrenAsString(node);
            if (!isCommonMark && !isTelegram) {
                name = name.Trim();
            }
            else if (isCommonMark) {
                name = name.ReplaceLineEndings("&#10;");
            }

            if (isTelegram) {
                ConvertTelegramMarkdownV2(writer, node, name);
                return;
            }

            if (isCommonMark &&
                node.FirstChild?.NodeType == HtmlNodeType.Text &&
                (node.InnerText.Contains("\\", StringComparison.Ordinal) ||
                 node.InnerText.Contains("<", StringComparison.Ordinal) ||
                 node.InnerText.Contains(">", StringComparison.Ordinal))) {
                WriteRawHtmlAnchor(writer, node, EncodeAnchorText(node.InnerText));
                return;
            }



            if (isCommonMark && (name.Contains('[') || name.Contains(']') || name.Contains('\n'))) {
                writer.Write(node.OuterHtml);
                return;
            }

            var hrefAttribute = node.Attributes["href"];
            var hasHrefAttribute = hrefAttribute != null;
            var href = node.GetAttributeValue("href", string.Empty).Trim();
            if (!isCommonMark) {
                href = href.Replace(_escapeValues);
            }
            else {
                href = href.Replace("\\", "\\\\");
                href = href.Replace("*", "\\*").Replace("_", "\\_");
                if (href.Contains('\n') || href.Contains('\r')) {
                    writer.Write(node.OuterHtml);
                    return;
                }
                var openCount = href.Count(c => c == '(');
                var closeCount = href.Count(c => c == ')');
                if (closeCount > openCount) {
                    href = href.Replace(")", "\\)");
                }
                if (href.Contains(' ') || href.Contains('(') || href.Contains(')')) {
                    href = $"<{href.Replace("<", "%3C").Replace(">", "%3E")}>";
                }
            }

            if (isCommonMark && !hasHrefAttribute) {
                writer.Write(node.OuterHtml);
                return;
            }

            if (isCommonMark &&
                node.OuterHtml.IndexOf("href=", StringComparison.OrdinalIgnoreCase) < 0) {
                writer.Write(node.OuterHtml);
                return;
            }

            if (isCommonMark && hrefAttribute != null) {
                if (hrefAttribute.Value.Contains("&", StringComparison.Ordinal) ||
                    hrefAttribute.Value.Contains("\\", StringComparison.Ordinal)) {
                    writer.Write(node.OuterHtml);
                    return;
                }

                var outerHtml = node.OuterHtml;
                if (outerHtml.Contains("href=\"&", StringComparison.OrdinalIgnoreCase) ||
                    outerHtml.Contains("href='&", StringComparison.OrdinalIgnoreCase) ||
                    outerHtml.Contains("href=\"\\", StringComparison.OrdinalIgnoreCase) ||
                    outerHtml.Contains("href='\\", StringComparison.OrdinalIgnoreCase)) {
                    writer.Write(outerHtml);
                    return;
                }

                var text = node.InnerText;
                if (text.Contains("\\", StringComparison.Ordinal) ||
                    text.Contains("[", StringComparison.Ordinal) ||
                    text.Contains("]", StringComparison.Ordinal) ||
                    text.Contains("<", StringComparison.Ordinal) ||
                    text.Contains("*", StringComparison.Ordinal) ||
                    text.Contains("_", StringComparison.Ordinal)) {
                    writer.Write(node.OuterHtml);
                    return;
                }
            }

            if (isCommonMark && string.IsNullOrEmpty(name)) {
                if (!hasHrefAttribute) {
                    writer.Write(node.OuterHtml);
                }
                else if (string.IsNullOrEmpty(href)) {
                    writer.Write("[]()");
                }
                else {
                    writer.Write("[](");
                    writer.Write(href);
                    writer.Write(")");
                }

                return;
            }

            if (isCommonMark && hrefAttribute != null &&
                hrefAttribute.Value.Contains("&", StringComparison.Ordinal)) {
                writer.Write(node.OuterHtml);
                return;
            }

            if (isCommonMark && href.Contains('`')) {
                writer.Write(node.OuterHtml);
                return;
            }
            var scheme = StringUtils.GetScheme(href);

            var isRemoveLinkWhenSameName = (
                Converter.Config.SmartHrefHandling &&
                scheme != string.Empty &&
                Uri.IsWellFormedUriString(href, UriKind.RelativeOrAbsolute) && (
                    href.Equals(name, StringComparison.OrdinalIgnoreCase) ||
                    href.Equals($"tel:{name}", StringComparison.OrdinalIgnoreCase) ||
                    href.Equals($"mailto:{name}", StringComparison.OrdinalIgnoreCase)
                )
            );

            if ((!Converter.Config.CommonMark && href.StartsWith("#")) //anchor link
                || !Converter.Config.IsSchemeWhitelisted(scheme) //Not allowed scheme
                || isRemoveLinkWhenSameName
                || (string.IsNullOrEmpty(href) && !Converter.Config.CommonMark) //We would otherwise print empty () here...
               ) {
                writer.Write(name);
                return;
            }

            var useHrefWithHttpWhenNameHasNoScheme = (
                Converter.Config.SmartHrefHandling && (
                    scheme.Equals("http", StringComparison.OrdinalIgnoreCase) ||
                    scheme.Equals("https", StringComparison.OrdinalIgnoreCase)
                ) &&
                string.Equals(href, $"{scheme}://{name}", StringComparison.OrdinalIgnoreCase)
            );

            var hasSingleChildImgNode = (
                node.ChildNodes.Count == 1 && // TODO handle whitespace text nodes?
                node.ChildNodes.Count(n => n.Name.Contains("img")) == 1
            );

            // if the anchor tag contains a single child image node don't escape the link text
            var linkText = hasSingleChildImgNode ? name : StringUtils.EscapeLinkText(name);

            if (string.IsNullOrEmpty(linkText)) {
                if (Converter.Config.CommonMark && string.IsNullOrEmpty(href)) {
                    writer.Write("[]()");
                }
                else {
                    writer.Write(href);
                }

                return;
            }

            if (useHrefWithHttpWhenNameHasNoScheme) {
                writer.Write(href);
            }
            else {
                writer.Write("[");
                writer.Write(linkText);
                writer.Write("](");
                writer.Write(href);

                if (ExtractTitle(node) is { Length: > 0 } title) {
                    writer.Write(" \"");
                    writer.Write(title);
                    writer.Write("\"");
                }

                writer.Write(")");
            }
        }

        private void ConvertTelegramMarkdownV2(TextWriter writer, HtmlNode node, string name)
        {
            var href = node.GetAttributeValue("href", string.Empty).Trim();
            var hasHrefAttribute = node.Attributes["href"] != null;
            var escapedName = StringUtils.EscapeTelegramMarkdownV2(name);

            if (!hasHrefAttribute) {
                writer.Write(escapedName);
                return;
            }

            var scheme = StringUtils.GetScheme(href);

            var isRemoveLinkWhenSameName = (
                Converter.Config.SmartHrefHandling &&
                scheme != string.Empty &&
                Uri.IsWellFormedUriString(href, UriKind.RelativeOrAbsolute) && (
                    href.Equals(name, StringComparison.OrdinalIgnoreCase) ||
                    href.Equals($"tel:{name}", StringComparison.OrdinalIgnoreCase) ||
                    href.Equals($"mailto:{name}", StringComparison.OrdinalIgnoreCase)
                )
            );

            if (href.StartsWith("#", StringComparison.Ordinal)
                || !Converter.Config.IsSchemeWhitelisted(scheme)
                || isRemoveLinkWhenSameName
                || string.IsNullOrEmpty(href)) {
                writer.Write(escapedName);
                return;
            }

            var escapedHref = StringUtils.EscapeTelegramMarkdownV2LinkUrl(href);
            writer.Write('[');
            writer.Write(escapedName);
            writer.Write("](");
            writer.Write(escapedHref);
            writer.Write(')');
        }

        private static void WriteRawHtmlAnchor(TextWriter writer, HtmlNode node, string text)
        {
            writer.Write("<a");
            foreach (var attribute in node.Attributes) {
                writer.Write(' ');
                writer.Write(attribute.Name);
                writer.Write("=\"");
                writer.Write(attribute.Value);
                writer.Write('"');
            }

            writer.Write('>');
            writer.Write(text);
            writer.Write("</a>");
        }

        private static string EncodeAnchorText(string text)
        {
            return text.Replace("\\", "&#92;").Replace("<", "&lt;").Replace(">", "&gt;");
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/AliasConverter.cs
================================================
using System.IO;
using HtmlAgilityPack;

namespace ReverseMarkdown.Converters {
    public sealed class AliasConverter : ConverterBase {
        private readonly string _targetTag;

        public AliasConverter(Converter converter, string targetTag) : base(converter)
        {
            _targetTag = targetTag;
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            if (string.IsNullOrWhiteSpace(_targetTag)) {
                writer.Write(TreatChildrenAsString(node));
                return;
            }

            if (string.Equals(node.Name, _targetTag, System.StringComparison.OrdinalIgnoreCase) ||
                Context.AncestorsAny(node.Name)) {
                writer.Write(TreatChildrenAsString(node));
                return;
            }

            var target = Converter.Lookup(_targetTag);
            target.Convert(writer, node);
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/AliasTagConverter.cs
================================================
using System.IO;
using HtmlAgilityPack;

namespace ReverseMarkdown.Converters {
    internal sealed class AliasTagConverter : ConverterBase {
        private readonly string _sourceTag;
        private readonly string _targetTag;

        public AliasTagConverter(Converter converter, string sourceTag, string targetTag) : base(converter)
        {
            _sourceTag = sourceTag;
            _targetTag = targetTag;
        }

        internal string TargetTag => _targetTag;

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            if (Context.AncestorsAny(_sourceTag)) {
                writer.Write(TreatChildrenAsString(node));
                return;
            }

            var target = Converter.Lookup(_targetTag);
            target.Convert(writer, node);
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/Aside.cs
================================================
using System.IO;
using HtmlAgilityPack;


namespace ReverseMarkdown.Converters {
    public class Aside : ConverterBase {
        public Aside(Converter converter)
            : base(converter)
        {
            Converter.Register("aside", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            writer.WriteLine();
            TreatChildren(writer, node);
            writer.WriteLine();
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/Blockquote.cs
================================================
using System.IO;
using HtmlAgilityPack;
using ReverseMarkdown.Helpers;


namespace ReverseMarkdown.Converters {
    public class Blockquote : ConverterBase {
        public Blockquote(Converter converter) : base(converter)
        {
            Converter.Register("blockquote", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            writer.WriteLine();
            writer.WriteLine();

            var content = TreatChildrenAsString(node);
            foreach (var line in content.ReadLines()) {
                writer.Write('>');
                writer.Write(' ');
                writer.WriteLine(line);
            }

            writer.WriteLine();
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/Br.cs
================================================
using System.IO;
using HtmlAgilityPack;


namespace ReverseMarkdown.Converters {
    public class Br : ConverterBase {
        public Br(Converter converter) : base(converter)
        {
            Converter.Register("br", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            if (node.ParentNode.Name is "strong" or "b" or "em" or "i") {
                if (!Converter.Config.CommonMark) {
                    return;
                }
            }

            if (Converter.Config.CommonMark) {
                writer.WriteLine("\\");
            }
            else if (Converter.Config.TelegramMarkdownV2) {
                writer.WriteLine();
            }
            else if (Converter.Config.GithubFlavored) {
                writer.WriteLine();
            }
            else {
                writer.WriteLine("  ");
            }
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/ByPass.cs
================================================
using System.IO;
using HtmlAgilityPack;


namespace ReverseMarkdown.Converters {
    public class ByPass : ConverterBase {
        public ByPass(Converter converter) : base(converter)
        {
            Converter.Register("#document", this);
            Converter.Register("html", this);
            Converter.Register("body", this);
            Converter.Register("span", this);
            Converter.Register("thead", this);
            Converter.Register("tbody", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            TreatChildren(writer, node);
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/Code.cs
================================================
using System;
using System.IO;
using HtmlAgilityPack;
using ReverseMarkdown.Helpers;


namespace ReverseMarkdown.Converters {
    public class Code : ConverterBase {
        public Code(Converter converter) : base(converter)
        {
            Converter.Register("code", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            if (Converter.Config.TelegramMarkdownV2) {
                writer.Write('`');
                writer.Write(StringUtils.EscapeTelegramMarkdownV2Code(DecodeHtml(node.InnerText)));
                writer.Write('`');
                return;
            }

            if (Converter.Config.CommonMark) {
                var content = node.InnerHtml;
                var fence = CreateCommonMarkCodeFence(content);
                writer.Write(fence);
                var needsBacktickPadding = content.Length > 0 && (content[0] == '`' || content[^1] == '`');
                var needsWhitespacePadding = content.Length > 0 &&
                                            (char.IsWhiteSpace(content[0]) || char.IsWhiteSpace(content[^1]));
                var needsPadding = needsBacktickPadding || needsWhitespacePadding;

                if (needsPadding) {
                    writer.Write(' ');
                }

                DecodeHtml(writer, content);

                if (needsPadding) {
                    writer.Write(' ');
                }

                writer.Write(fence);
                return;
            }

            // Depending on the content "surrounding" the <code> element,
            // leading/trailing whitespace is significant. For example, the
            // following HTML renders as expected in a browser (meaning there is
            // proper spacing between words):
            //
            //   <p>The JavaScript<code> function </code>keyword...</p>
            //
            // However, if we simply trim the contents of the <code> element,
            // then the Markdown becomes:
            //
            //   The JavaScript`function`keyword...
            //
            // To avoid this scenario, do *not* trim the inner text of the
            // <code> element.
            //
            // For the HTML example above, the Markdown will be:
            //
            //   The JavaScript` function `keyword...
            //
            // While it might seem preferable to try to "fix" this by trimming
            // the <code> element and insert leading/trailing spaces as
            // necessary, things become complicated rather quickly depending
            // on the particular content. For example, what would be the
            // "correct" conversion of the following HTML?
            //
            //   <p>The JavaScript<b><code> function </code></b>keyword...</p>
            //
            // The simplest conversion to Markdown:
            //
            //   The JavaScript**` function `**keyword...
            //
            // Some other, arguably "better", alternatives (that would require
            // substantially more conversion logic):
            //
            //   The JavaScript** `function` **keyword...
            //
            //   The JavaScript **`function`** keyword...

            writer.Write('`');
            DecodeHtml(writer, node.InnerText);
            writer.Write('`');
        }

        private static string CreateCommonMarkCodeFence(string content)
        {
            var maxRun = 0;
            var currentRun = 0;
            foreach (var c in content) {
                if (c == '`') {
                    currentRun++;
                    if (currentRun > maxRun) {
                        maxRun = currentRun;
                    }
                }
                else {
                    currentRun = 0;
                }
            }

            var fenceLength = Math.Max(1, maxRun + 1);
            return new string('`', fenceLength);
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/ConverterBase.cs
================================================
using System.IO;
using HtmlAgilityPack;
using ReverseMarkdown.Helpers;


namespace ReverseMarkdown.Converters {
    public abstract class ConverterBase(Converter converter) : IConverter {
        protected Converter Converter { get; } = converter;
        protected ConverterContext Context => Converter.Context;


        protected void TreatChildren(TextWriter writer, HtmlNode node)
        {
            if (node.HasChildNodes) {
                foreach (var child in node.ChildNodes) {
                    Converter.ConvertNode(writer, child);
                }
            }
        }

        protected string TreatChildrenAsString(HtmlNode node)
        {
            if (node.HasChildNodes) {
                using var writer = Converter.CreateWriter(node);
                foreach (var child in node.ChildNodes) {
                    Converter.ConvertNode(writer, child);
                }

                return writer.ToString();
            }

            return string.Empty;
        }


        protected static string ExtractTitle(HtmlNode node)
        {
            return node.GetAttributeValue("title", string.Empty);
        }


        protected static string DecodeHtml(string html)
        {
            return System.Net.WebUtility.HtmlDecode(html);
        }

        protected static void DecodeHtml(TextWriter writer, string html)
        {
            System.Net.WebUtility.HtmlDecode(html, writer);
        }


        protected string IndentationFor(HtmlNode node, bool zeroIndex = false)
        {
            var length = Context.AncestorsCount("ol") + Context.AncestorsCount("ul");
            var indentSize = Converter.Config.CommonMark ? 2 : 4;

            // li not required to have a parent ol/ul
            if (length == 0) {
                return string.Empty;
            }

            if (zeroIndex) {
                length -= 1;
            }

            return new string(' ', length * indentSize);
        }


        public static void TreatEmphasizeContentWhitespaceGuard(
            TextWriter writer,
            string content,
            string emphasis,
            string nextSiblingSpaceSuffix = "",
            bool preserveLineEndings = false
        )
        {
            WriteLeadingSpace(writer, content);
            writer.Write(emphasis);
            writer.Write(preserveLineEndings ? content.Trim(' ') : content.Chomp());
            writer.Write(emphasis);
            WriteTrailingSpace(writer, content, nextSiblingSpaceSuffix);

            return;

            static void WriteLeadingSpace(TextWriter writer, string content)
            {
                foreach (var c in content) {
                    if (c != ' ') break;
                    writer.Write(c);
                }
            }

            static void WriteTrailingSpace(TextWriter writer, string content, string nextSiblingSpaceSuffix)
            {
                var start = content.Length - 1;
                var i = start;
                for (; i >= 0; i--) {
                    var c = content[i];
                    if (c != ' ') break;
                    writer.Write(c);
                }

                if (i == start) {
                    writer.Write(nextSiblingSpaceSuffix);
                }
            }
        }


        public abstract void Convert(TextWriter writer, HtmlNode node);
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/Dd.cs
================================================
using System.IO;
using HtmlAgilityPack;


namespace ReverseMarkdown.Converters {
    public class Dd : ConverterBase {
        public Dd(Converter converter) : base(converter)
        {
            Converter.Register("dd", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            writer.Write(new string(' ', 4));
            writer.Write(Converter.Config.ListBulletChar);
            writer.Write(' ');
            var content = TreatChildrenAsString(node).Trim();
            writer.Write(content);
            writer.WriteLine();
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/Div.cs
================================================
using System.IO;
using HtmlAgilityPack;


namespace ReverseMarkdown.Converters {
    public class Div : ConverterBase {
        public Div(Converter converter) : base(converter)
        {
            Converter.Register("div", this);
            Converter.Register("header", this);
            Converter.Register("main", this);
            Converter.Register("footer", this);
            Converter.Register("section", this);
            Converter.Register("article", this);
            Converter.Register("nav", this);
            Converter.Register("figure", this);
            Converter.Register("figcaption", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            if (Converter.Config.CommonMark) {
                writer.Write(node.OuterHtml);
                return;
            }

            while (node.ChildNodes.Count == 1 && node.FirstChild.Name == "div") {
                node = node.FirstChild;
            }

            var content = TreatChildrenAsString(node);

            content = Converter.Config.CleanupUnnecessarySpaces
                ? content.Trim()
                : content;

            // if there is a block child then ignore adding the newlines for div
            if (
                node.ChildNodes.Count == 1 &&
                node.FirstChild.Name
                    is "pre"
                    or "p"
                    or "ol"
                    or "oi"
                    or "table"
            ) {
                writer.Write(content);
                return;
            }

            if (
                !Td.FirstNodeWithinCell(node) &&
                !Converter.Config.SuppressDivNewlines
            ) {
                writer.WriteLine();
            }

            writer.Write(content);

            if (!Td.LastNodeWithinCell(node)) {
                writer.WriteLine();
            }
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/Dl.cs
================================================
using System.IO;
using HtmlAgilityPack;


namespace ReverseMarkdown.Converters {
    public class Dl : ConverterBase {
        public Dl(Converter converter) : base(converter)
        {
            Converter.Register("dl", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            writer.WriteLine();
            TreatChildren(writer, node);
            writer.WriteLine();
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/Drop.cs
================================================
using System.IO;
using HtmlAgilityPack;


namespace ReverseMarkdown.Converters {
    public class Drop : ConverterBase {
        public Drop(Converter converter) : base(converter)
        {
            Converter.Register("style", this);
            Converter.Register("script", this);
            if (Converter.Config.RemoveComments) {
                converter.Register("#comment", this);
            }
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            // Do nothing, effectively dropping the node and its children
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/Dt.cs
================================================
using System.IO;
using HtmlAgilityPack;


namespace ReverseMarkdown.Converters {
    public class Dt : ConverterBase {
        public Dt(Converter converter) : base(converter)
        {
            Converter.Register("dt", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            writer.Write(Converter.Config.ListBulletChar);
            writer.Write(' ');
            var content = TreatChildrenAsString(node).Trim();
            writer.Write(content);
            writer.WriteLine();
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/Em.cs
================================================
using System.IO;
using HtmlAgilityPack;


namespace ReverseMarkdown.Converters {
    public class Em : ConverterBase {
        public Em(Converter converter) : base(converter)
        {
            Converter.Register("em", this);
            Converter.Register("i", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            var isCommonMark = Converter.Config.CommonMark;
            if (isCommonMark && Converter.Config.CommonMarkUseHtmlInlineTags) {
                var innerHtml = node.InnerHtml;
                if (innerHtml.Contains('[') || innerHtml.Contains(']')) {
                    writer.Write('<');
                    writer.Write(node.Name);
                    foreach (var attribute in node.Attributes) {
                        writer.Write(' ');
                        writer.Write(attribute.Name);
                        writer.Write("=\"");
                        writer.Write(attribute.Value);
                        writer.Write('"');
                    }

                    writer.Write('>');
                    writer.Write(innerHtml.Replace("[", "&#91;").Replace("]", "&#93;"));
                    writer.Write("</");
                    writer.Write(node.Name);
                    writer.Write('>');
                }
                else {
                    writer.Write(node.OuterHtml);
                }

                return;
            }

            var content = TreatChildrenAsString(node);

            if (string.IsNullOrWhiteSpace(content) || (!isCommonMark && AlreadyItalic())) {
                writer.Write(content);
                return;
            }

            var commonMarkPrefix = string.Empty;
            var commonMarkSuffix = string.Empty;
            if (isCommonMark && Converter.Config.CommonMarkIntrawordEmphasisSpacing) {
                var contentFirst = FirstNonWhitespaceChar(content);
                var contentLast = LastNonWhitespaceChar(content);
                var contentHasLeadingWhitespace = content.Length > 0 && char.IsWhiteSpace(content[0]);
                var contentHasTrailingWhitespace = content.Length > 0 && char.IsWhiteSpace(content[content.Length - 1]);

                if (IsWordChar(contentFirst) && IsWordChar(contentLast)) {
                    var hasPrevWord = !contentHasLeadingWhitespace &&
                                      IsAdjacentWordChar(node.PreviousSibling, checkEnd: true);
                    var hasNextWord = !contentHasTrailingWhitespace &&
                                      IsAdjacentWordChar(node.NextSibling, checkEnd: false);
                    if (hasPrevWord && hasNextWord) {
                        commonMarkPrefix = " ";
                        commonMarkSuffix = " ";
                    }
                }
            }

            var spaceSuffix = node.NextSibling?.Name is "i" or "em"
                ? " "
                : string.Empty;

            var emphasis = Converter.Config.SlackFlavored
                || Converter.Config.TelegramMarkdownV2
                ? "_"
                : isCommonMark && Context.AncestorsAny("i")
                    ? "_"
                    : "*";
            var suffix = commonMarkSuffix.Length > 0 || spaceSuffix.Length > 0 ? " " : string.Empty;
            if (commonMarkPrefix.Length > 0) {
                writer.Write(commonMarkPrefix);
            }

            TreatEmphasizeContentWhitespaceGuard(
                writer,
                content,
                emphasis,
                suffix,
                preserveLineEndings: isCommonMark
            );
        }

        private bool AlreadyItalic()
        {
            return Context.AncestorsAny("i") || Context.AncestorsAny("em");
        }

        private static bool IsWordChar(char? value)
        {
            return value.HasValue && char.IsLetterOrDigit(value.Value);
        }

        private static char? FirstNonWhitespaceChar(string content)
        {
            foreach (var c in content) {
                if (!char.IsWhiteSpace(c)) {
                    return c;
                }
            }

            return null;
        }

        private static char? LastNonWhitespaceChar(string content)
        {
            for (var i = content.Length - 1; i >= 0; i--) {
                var c = content[i];
                if (!char.IsWhiteSpace(c)) {
                    return c;
                }
            }

            return null;
        }

        private static bool IsAdjacentWordChar(HtmlNode? sibling, bool checkEnd)
        {
            if (sibling == null) {
                return false;
            }

            var text = sibling.InnerText;
            if (string.IsNullOrEmpty(text)) {
                return false;
            }

            var index = checkEnd ? text.Length - 1 : 0;
            var c = text[index];
            if (char.IsWhiteSpace(c)) {
                return false;
            }

            return char.IsLetterOrDigit(c);
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/H.cs
================================================
using System.IO;
using HtmlAgilityPack;


namespace ReverseMarkdown.Converters {
    public class H : ConverterBase {
        public H(Converter converter) : base(converter)
        {
            Converter.Register("h1", this);
            Converter.Register("h2", this);
            Converter.Register("h3", this);
            Converter.Register("h4", this);
            Converter.Register("h5", this);
            Converter.Register("h6", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            // Headings inside tables are not supported as markdown, so just ignore the heading and convert children
            if (Context.AncestorsAny("table")) {
                TreatChildren(writer, node);
                return;
            }

            var level = node.Name[1] - '0'; // 'h1' -> 1, 'h2' -> 2, etc.

            var content = TreatChildrenAsString(node);
            if (Converter.Config.TelegramMarkdownV2) {
                writer.WriteLine();
                for (var i = 0; i < level; i++) {
                    writer.Write("\\#");
                }

                writer.Write(' ');
                writer.Write(content);
                writer.WriteLine();
                return;
            }

            if (Converter.Config.CommonMark) {
                content = content.ReplaceLineEndings("&#10;");
                content = EscapeTrailingHashes(content);
            }

            writer.WriteLine();
            writer.Write(new string('#', level));
            writer.Write(' ');
            writer.Write(content);
            writer.WriteLine();
        }

        private static string EscapeTrailingHashes(string content)
        {
            if (string.IsNullOrEmpty(content)) {
                return content;
            }

            var index = content.Length - 1;
            while (index >= 0 && content[index] == '#') {
                index--;
            }

            if (index == content.Length - 1) {
                return content;
            }

            var hashCount = content.Length - 1 - index;
            var escapedHashes = new string('#', hashCount);
            escapedHashes = escapedHashes.Replace("#", "\\#");
            return content[..(index + 1)] + escapedHashes;
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/Hr.cs
================================================
using System.IO;
using HtmlAgilityPack;


namespace ReverseMarkdown.Converters {
    public class Hr : ConverterBase {
        public Hr(Converter converter) : base(converter)
        {
            Converter.Register("hr", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            if (Converter.Config.SlackFlavored) {
                throw new SlackUnsupportedTagException(node.Name);
            }

            if (Converter.Config.TelegramMarkdownV2) {
                writer.WriteLine();
                writer.Write("\\-\\-\\-");
                writer.WriteLine();
                return;
            }

            writer.WriteLine();
            writer.Write("* * *");
            writer.WriteLine();
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/IConverter.cs
================================================
using System.IO;
using HtmlAgilityPack;


namespace ReverseMarkdown.Converters {
    public interface IConverter {
        void Convert(TextWriter writer, HtmlNode node);
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/Ignore.cs
================================================
using System.IO;
using HtmlAgilityPack;


namespace ReverseMarkdown.Converters {
    public class Ignore : ConverterBase {
        public Ignore(Converter converter) : base(converter)
        {
            Converter.Register("colgroup", this);
            Converter.Register("col", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            // Do nothing, ignore the node
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/Img.cs
================================================
using System.IO;
using HtmlAgilityPack;
using ReverseMarkdown.Helpers;


namespace ReverseMarkdown.Converters {
    public class Img : ConverterBase {
        private int _base64ImageCounter = 0;

        public Img(Converter converter) : base(converter)
        {
            Converter.Register("img", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            if (Converter.Config.SlackFlavored) {
                throw new SlackUnsupportedTagException(node.Name);
            }

            var altAttribute = node.Attributes["alt"];
            var alt = node.GetAttributeValue("alt", string.Empty);
            var src = node.GetAttributeValue("src", string.Empty);

            // Check if this is a base64-encoded image
            bool isBase64Image = ImageUtils.IsValidBase64ImageData(src);

            if (isBase64Image)
            {
                // Handle base64 images according to configuration
                switch (Converter.Config.Base64Images)
                {
                    case Config.Base64ImageHandling.Skip:
                        // Skip this image entirely
                        return;

                    case Config.Base64ImageHandling.SaveToFile:
                        // Save to file and update src
                        src = SaveBase64ImageToFile(src);
                        if (string.IsNullOrEmpty(src))
                        {
                            // If saving failed, skip the image
                            return;
                        }
                        break;

                    case Config.Base64ImageHandling.Include:
                    default:
                        // Include as-is (default behavior)
                        break;
                }
            }
            else
            {
                // For non-base64 images, check scheme whitelist
                var scheme = StringUtils.GetScheme(src);
                if (!Converter.Config.IsSchemeWhitelisted(scheme)) {
                    return;
                }
            }

            if (Converter.Config.TelegramMarkdownV2) {
                WriteTelegramFallback(writer, alt, src, isBase64Image);
                return;
            }

            if (Converter.Config.CommonMark && altAttribute == null) {
                writer.Write(node.OuterHtml);
                return;
            }

            writer.Write("![");
            writer.Write(StringUtils.EscapeLinkText(alt));
            writer.Write("](");
            writer.Write(src);

            if (ExtractTitle(node) is { Length: > 0 } title) {
                writer.Write(" \"");
                writer.Write(title);
                writer.Write("\"");
            }

            writer.Write(')');
        }

        private void WriteTelegramFallback(TextWriter writer, string alt, string src, bool isBase64Image)
        {
            var label = string.IsNullOrWhiteSpace(alt)
                ? "Image"
                : $"Image: {alt}";

            var escapedLabel = StringUtils.EscapeTelegramMarkdownV2(label);
            var canRenderAsLink = !string.IsNullOrEmpty(src) && (
                !isBase64Image ||
                Converter.Config.Base64Images == Config.Base64ImageHandling.SaveToFile
            );

            if (!canRenderAsLink) {
                writer.Write(escapedLabel);
                return;
            }

            writer.Write('[');
            writer.Write(escapedLabel);
            writer.Write("](");
            writer.Write(StringUtils.EscapeTelegramMarkdownV2LinkUrl(src));
            writer.Write(')');
        }

        private string SaveBase64ImageToFile(string base64Src)
        {
            try
            {
                var saveDir = Converter.Config.Base64ImageSaveDirectory;
                if (string.IsNullOrWhiteSpace(saveDir))
                {
                    // If no save directory is configured, skip the image
                    return string.Empty;
                }

                // Ensure directory exists
                if (!Directory.Exists(saveDir))
                {
                    Directory.CreateDirectory(saveDir);
                }

                // Generate filename
                string fileName;
                if (Converter.Config.Base64ImageFileNameGenerator != null)
                {
                    // Extract MIME type for the generator
                    var mimeMatch = System.Text.RegularExpressions.Regex.Match(base64Src, @"^data:(?<mime>[\w/\-\.]+);base64,");
                    var mimeType = mimeMatch.Success ? mimeMatch.Groups["mime"].Value : "image/png";
                    fileName = Converter.Config.Base64ImageFileNameGenerator(_base64ImageCounter++, mimeType);
                }
                else
                {
                    fileName = $"image_{_base64ImageCounter++}";
                }

                // Save the image and get the file path
                var filePath = ImageUtils.SaveBase64Image(base64Src, saveDir, fileName);
                return filePath;
            }
            catch
            {
                // If saving fails for any reason, skip the image
                return string.Empty;
            }
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/Li.cs
================================================
using System;
using System.IO;
using System.Linq;
using HtmlAgilityPack;
using ReverseMarkdown.Helpers;


namespace ReverseMarkdown.Converters {
    public class Li : ConverterBase {
        public Li(Converter converter) : base(converter)
        {
            Converter.Register("li", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            // Standardize whitespace before inner lists so that the following are equivalent
            //   <li>Foo<ul><li>...
            //   <li>Foo\n    <ul><li>...
            foreach (var innerList in node.SelectNodes("//ul|//ol") ?? Enumerable.Empty<HtmlNode>()) {
                if (innerList.PreviousSibling?.NodeType == HtmlNodeType.Text) {
                    innerList.PreviousSibling.InnerHtml = innerList.PreviousSibling.InnerHtml.Trim(); // TODO optimize
                }
            }

            var baseIndentation = IndentationFor(node, true);
            writer.Write(baseIndentation);

            if (node.ParentNode is { Name: "ol" }) {
                // index are zero based hence add one
                var start = node.ParentNode.GetAttributeValue("start", 1);
                var index = node.ParentNode.SelectNodes("./li").IndexOf(node) + start;
                writer.Write(index);
                writer.Write(Converter.Config.TelegramMarkdownV2 ? "\\. " : ". ");
            }
            else {
                if (Converter.Config.TelegramMarkdownV2) {
                    writer.Write(StringUtils.EscapeTelegramMarkdownV2(Converter.Config.ListBulletChar.ToString()));
                }
                else {
                    writer.Write(Converter.Config.ListBulletChar);
                }

                writer.Write(' ');
            }

            var content = ContentFor(node);

            if (!Converter.Config.CommonMark) {
                content = content.Trim();
                writer.Write(content);
                writer.WriteLine();
                return;
            }

            var markerLength = node.ParentNode is { Name: "ol" }
                ? ($"{node.ParentNode.GetAttributeValue("start", 1) + node.ParentNode.SelectNodes("./li").IndexOf(node)}. ").Length
                : 2;

            var indentation = baseIndentation + new string(' ', markerLength);
            var lines = content.ReplaceLineEndings("\n").Split('\n');

            if (lines.Length == 0) {
                writer.WriteLine();
                return;
            }

            writer.WriteLine(lines[0].TrimEnd());

            for (var i = 1; i < lines.Length; i++) {
                var line = lines[i];
                if (line.Length == 0) {
                    writer.WriteLine();
                    continue;
                }

                if (line[0] == ' ' || line[0] == '\t') {
                    writer.WriteLine(line);
                    continue;
                }

                writer.Write(indentation);
                writer.WriteLine(line);
            }
        }

        private string ContentFor(HtmlNode node)
        {
            using var writer = Converter.CreateWriter(node);

            if (Converter.Config.GithubFlavored) {
                if (
                    node.FirstChild is { Name: "input" } childNode &&
                    childNode.GetAttributeValue("type", string.Empty).Equals("checkbox", StringComparison.OrdinalIgnoreCase)
                ) {
                    writer.Write(childNode.Attributes.Contains("checked")
                        ? "[x]"
                        : "[ ]");

                    node.RemoveChild(childNode);
                }
            }

            TreatChildren(writer, node);

            return writer.ToString();
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/Ol.cs
================================================
using System.IO;
using HtmlAgilityPack;
using ReverseMarkdown.Helpers;


namespace ReverseMarkdown.Converters {
    public class Ol : ConverterBase {
        public Ol(Converter converter) : base(converter)
        {
            Converter.Register("ol", this);
            Converter.Register("ul", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            if (Converter.Config.CommonMark) {
                writer.Write(node.OuterHtml.CompactHtmlForCommonMarkBlock());
                return;
            }

            // Lists inside tables are not supported as markdown, so leave as HTML
            if (Context.AncestorsAny("table")) {
                writer.Write(node.OuterHtml);
                return;
            }

            // Prevent blank lines being inserted in nested lists
            var block = node.ParentNode.Name is not ("ol" or "ul");

            if (block) writer.WriteLine();
            TreatChildren(writer, node);
            if (block) {
                writer.WriteLine();
                if (Converter.Config.CommonMark && NextElementIsList(node)) {
                    writer.WriteLine();
                }
            }
        }

        private static bool NextElementIsList(HtmlNode node)
        {
            var sibling = node.NextSibling;
            while (sibling != null) {
                if (sibling.NodeType == HtmlNodeType.Element) {
                    return sibling.Name is "ul" or "ol";
                }

                sibling = sibling.NextSibling;
            }

            return false;
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/P.cs
================================================
using System.IO;
using HtmlAgilityPack;


namespace ReverseMarkdown.Converters {
    public class P : ConverterBase {
        public P(Converter converter) : base(converter)
        {
            Converter.Register("p", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            TreatIndentation(writer, node);

            var content = TreatChildrenAsString(node);
            if (Converter.Config.CleanupUnnecessarySpaces) {
                content = content.Trim();
            }

            writer.Write(content);

            if (!Td.LastNodeWithinCell(node)) {
                writer.WriteLine();
            }
        }

        private void TreatIndentation(TextWriter writer, HtmlNode node)
        {
            // If p follows a list item, add newline and indent it
            var parentIsList = node.ParentNode.Name is "li" or "ol" or "ul";
            if (Converter.Config.CommonMark && parentIsList) {
                if (node.ParentNode.FirstChild != node) {
                    writer.WriteLine();
                    writer.WriteLine();
                }

                return;
            }

            if (parentIsList && node.ParentNode.FirstChild != node) {
                var length = Context.AncestorsCount("ol") + Context.AncestorsCount("ul");
                var indentSize = Converter.Config.CommonMark ? 2 : 4;
                writer.WriteLine();
                writer.Write(new string(' ', length * indentSize));
                return;
            }

            // If p is at the start of a table cell, no leading newline
            if (!Td.FirstNodeWithinCell(node)) {
                writer.WriteLine();
            }
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/PassThrough.cs
================================================
using System.IO;
using HtmlAgilityPack;


namespace ReverseMarkdown.Converters {
    public class PassThrough : ConverterBase {
        public PassThrough(Converter converter) : base(converter)
        {
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            writer.Write(node.OuterHtml);
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/Pre.cs
================================================
using System;
using System.IO;
using System.Text.RegularExpressions;
using HtmlAgilityPack;
using ReverseMarkdown.Helpers;


namespace ReverseMarkdown.Converters {
    public partial class Pre : ConverterBase {
        public Pre(Converter converter) : base(converter)
        {
            Converter.Register("pre", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            if (Converter.Config.ConvertPreContentAsHtml) {
                ConvertHtmlContent(writer, node);
                return;
            }

            var isTelegram = Converter.Config.TelegramMarkdownV2;
            var isFencedCodeBlock = Converter.Config.GithubFlavored || Converter.Config.CommonMark || isTelegram;

            var indentation = (Converter.Config.CommonMark || isTelegram)
                ? string.Empty
                : IndentationFor(node);
            var contentIndentation = indentation;

            // 4 space indent for code if it is not fenced code block
            if (!isFencedCodeBlock) {
                indentation += "    ";
                contentIndentation = indentation;
            }

            writer.WriteLine();
            writer.WriteLine();

            // content:
            var content = DecodeHtml(node.InnerText);
            if (isTelegram) {
                content = StringUtils.EscapeTelegramMarkdownV2Code(content);
            }

            if (isFencedCodeBlock) {
                var fence = Converter.Config.CommonMark
                    ? CreateCommonMarkFence(content)
                    : "```";
                var lang = GetLanguage(node);
                writer.Write(indentation);
                writer.Write(fence);
                writer.Write(lang);
                writer.WriteLine();
            }
            foreach (var line in content.ReadLines()) {
                writer.Write(contentIndentation);
                writer.WriteLine(line);
            }

            if (string.IsNullOrEmpty(content)) {
                if (!isFencedCodeBlock) writer.Write(contentIndentation);
                writer.WriteLine();
            }

            if (isFencedCodeBlock) {
                var fence = Converter.Config.CommonMark
                    ? CreateCommonMarkFence(content)
                    : "```";
                writer.Write(indentation);
                writer.Write(fence);
            }

            writer.WriteLine();
        }

        private void ConvertHtmlContent(TextWriter writer, HtmlNode node)
        {
            var contentNode = node.ChildNodes["code"] ?? node;
            if (contentNode.HasChildNodes) {
                foreach (var child in contentNode.ChildNodes) {
                    Converter.ConvertNode(writer, child);
                }

                return;
            }

            TreatChildren(writer, node);
        }


        private string? GetLanguage(HtmlNode node)
        {
            var language = GetLanguageFromHighlightClassAttribute(node);

            if (!Converter.Config.CommonMark && !string.IsNullOrEmpty(language)) {
                language = language.TrimEnd(';');
            }

            return !string.IsNullOrEmpty(language)
                ? language
                : Converter.Config.DefaultCodeBlockLanguage;
        }


        private static string GetLanguageFromHighlightClassAttribute(HtmlNode node)
        {
            var res = ClassMatch(node);

            // check parent node:
            // GitHub: <div class="highlight highlight-source-json"><pre>
            // BitBucket: <div class="codehilite language-json"><pre>
            if (!res.Success && node.ParentNode != null!) {
                res = ClassMatch(node.ParentNode);
            }

            // check child <code> node:
            // HighlightJs: <pre><code class="hljs language-json">
            if (!res.Success) {
                var cnode = node.ChildNodes["code"];
                if (cnode != null!) {
                    res = ClassMatch(cnode);
                }
            }

            return res.Success && res.Groups.Count == 3 ? res.Groups[2].Value : string.Empty;
        }

        /// <summary>
        /// Extracts class attribute syntax using: highlight-json, highlight-source-json, language-json, brush: language
        /// Returns the Language in Match.Groups[2]
        /// </summary>
        [GeneratedRegex(@"(highlight-source-|language-|highlight-|brush:\s)([^\s]+)")]
        private static partial Regex ClassRegex();

        /// <summary>
        /// Checks class attribute for language class identifiers for various
        /// common highlighters
        /// </summary>
        /// <param name="node">Node with class attribute</param>
        /// <returns>Match.Success and Match.Group[2] set to the language</returns>
        private static Match ClassMatch(HtmlNode node)
        {
            var val = node.GetAttributeValue("class", string.Empty);
            if (!string.IsNullOrEmpty(val)) {
                val = System.Net.WebUtility.HtmlDecode(val);
                return ClassRegex().Match(val);
            }

            return Match.Empty;
        }

        private static string CreateCommonMarkFence(string content)
        {
            var maxRun = 0;
            var currentRun = 0;
            foreach (var c in content) {
                if (c == '`') {
                    currentRun++;
                    if (currentRun > maxRun) {
                        maxRun = currentRun;
                    }
                }
                else {
                    currentRun = 0;
                }
            }

            var fenceLength = Math.Max(3, maxRun + 1);
            return new string('`', fenceLength);
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/S.cs
================================================
using System.IO;
using HtmlAgilityPack;


namespace ReverseMarkdown.Converters {
    public class S : ConverterBase {
        public S(Converter converter) : base(converter)
        {
            Converter.Register("s", this);
            Converter.Register("del", this);
            Converter.Register("strike", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            if (Converter.Config.CommonMark) {
                writer.Write(node.OuterHtml);
                return;
            }

            var content = TreatChildrenAsString(node);

            if (string.IsNullOrEmpty(content) || AlreadyStrikethrough()) {
                writer.Write(content);
                return;
            }

            var emphasis = Converter.Config.SlackFlavored || Converter.Config.TelegramMarkdownV2
                ? "~"
                : "~~";
            TreatEmphasizeContentWhitespaceGuard(writer, content, emphasis);
        }

        private bool AlreadyStrikethrough()
        {
            return Context.AncestorsAny("s") || Context.AncestorsAny("del") || Context.AncestorsAny("strike");
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/Strong.cs
================================================
using System.IO;
using HtmlAgilityPack;


namespace ReverseMarkdown.Converters {
    public class Strong : ConverterBase {
        public Strong(Converter converter) : base(converter)
        {
            Converter.Register("strong", this);
            Converter.Register("b", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            var isCommonMark = Converter.Config.CommonMark;
            if (isCommonMark && Converter.Config.CommonMarkUseHtmlInlineTags) {
                writer.Write(node.OuterHtml);
                return;
            }

            var content = TreatChildrenAsString(node);

            if (string.IsNullOrEmpty(content) || (!isCommonMark && AlreadyBold())) {
                writer.Write(content);
                return;
            }

            var commonMarkPrefix = string.Empty;
            var commonMarkSuffix = string.Empty;
            if (isCommonMark && Converter.Config.CommonMarkIntrawordEmphasisSpacing) {
                var contentFirst = FirstNonWhitespaceChar(content);
                var contentLast = LastNonWhitespaceChar(content);
                var contentHasLeadingWhitespace = content.Length > 0 && char.IsWhiteSpace(content[0]);
                var contentHasTrailingWhitespace = content.Length > 0 && char.IsWhiteSpace(content[content.Length - 1]);

                if (IsWordChar(contentFirst) && IsWordChar(contentLast)) {
                    var hasPrevWord = !contentHasLeadingWhitespace &&
                                      IsAdjacentWordChar(node.PreviousSibling, checkEnd: true);
                    var hasNextWord = !contentHasTrailingWhitespace &&
                                      IsAdjacentWordChar(node.NextSibling, checkEnd: false);
                    if (hasPrevWord && hasNextWord) {
                        commonMarkPrefix = " ";
                        commonMarkSuffix = " ";
                    }
                }
            }

            var spaceSuffix = node.NextSibling?.Name is "strong" or "b"
                ? " "
                : "";

            var emphasis = Converter.Config.SlackFlavored
                || Converter.Config.TelegramMarkdownV2
                ? "*"
                : isCommonMark && Context.AncestorsAny("strong")
                    ? "__"
                    : "**";
            var suffix = commonMarkSuffix.Length > 0 || spaceSuffix.Length > 0 ? " " : "";
            if (commonMarkPrefix.Length > 0) {
                writer.Write(commonMarkPrefix);
            }

            TreatEmphasizeContentWhitespaceGuard(
                writer,
                content,
                emphasis,
                suffix,
                preserveLineEndings: isCommonMark
            );
        }

        private bool AlreadyBold()
        {
            return Context.AncestorsAny("strong") || Context.AncestorsAny("b");
        }

        private static bool IsWordChar(char? value)
        {
            return value.HasValue && char.IsLetterOrDigit(value.Value);
        }

        private static char? FirstNonWhitespaceChar(string content)
        {
            foreach (var c in content) {
                if (!char.IsWhiteSpace(c)) {
                    return c;
                }
            }

            return null;
        }

        private static char? LastNonWhitespaceChar(string content)
        {
            for (var i = content.Length - 1; i >= 0; i--) {
                var c = content[i];
                if (!char.IsWhiteSpace(c)) {
                    return c;
                }
            }

            return null;
        }

        private static bool IsAdjacentWordChar(HtmlNode? sibling, bool checkEnd)
        {
            if (sibling == null) {
                return false;
            }

            var text = sibling.InnerText;
            if (string.IsNullOrEmpty(text)) {
                return false;
            }

            var index = checkEnd ? text.Length - 1 : 0;
            var c = text[index];
            if (char.IsWhiteSpace(c)) {
                return false;
            }

            return char.IsLetterOrDigit(c);
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/Sup.cs
================================================
using System.IO;
using HtmlAgilityPack;
using ReverseMarkdown.Helpers;


namespace ReverseMarkdown.Converters {
    public class Sup : ConverterBase {
        public Sup(Converter converter) : base(converter)
        {
            Converter.Register("sup", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            if (Converter.Config.SlackFlavored) {
                throw new SlackUnsupportedTagException(node.Name);
            }

            var content = TreatChildrenAsString(node);

            if (Converter.Config.TelegramMarkdownV2) {
                if (string.IsNullOrEmpty(content)) {
                    return;
                }

                writer.Write('^');
                writer.Write(content.Chomp());
                return;
            }

            if (string.IsNullOrEmpty(content) || AlreadySup()) {
                writer.Write(content);
                return;
            }

            writer.Write('^');
            writer.Write(content.Chomp());
            writer.Write('^');
        }

        private bool AlreadySup()
        {
            return Context.AncestorsAny("sup");
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/Table.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using HtmlAgilityPack;
using ReverseMarkdown.Helpers;


namespace ReverseMarkdown.Converters {
    public class Table : ConverterBase {
        public Table(Converter converter) : base(converter)
        {
            Converter.Register("table", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            if (Converter.Config.CommonMark) {
                writer.Write(node.OuterHtml);
                return;
            }

            if (Converter.Config.SlackFlavored) {
                throw new SlackUnsupportedTagException(node.Name);
            }

            if (Converter.Config.TelegramMarkdownV2) {
                WriteTelegramFallback(writer, node);
                return;
            }

            // Tables inside tables are not supported as markdown, so leave as HTML
            if (Context.AncestorsAny("table")) {
                // Compact the nested table HTML to prevent breaking the markdown table
                writer.Write(node.OuterHtml.CompactHtmlForMarkdown());
                return;
            }
            
            var captionNode = node.SelectSingleNode("caption");
            var captionText = captionNode?.InnerText?.Trim();
            captionNode?.Remove();

            // if table does not have a header row , add empty header row if set in config
            var useEmptyRowForHeader = (
                this.Converter.Config.TableWithoutHeaderRowHandling == Config.TableWithoutHeaderRowHandlingOption.EmptyRow
            );

            var emptyHeaderRow = HasNoTableHeaderRow(node) && useEmptyRowForHeader
                ? EmptyHeader(node)
                : string.Empty;
            
            // add caption text as a paragraph above table
            if (captionText != string.Empty)
            {
                writer.WriteLine();
                writer.WriteLine();
                writer.WriteLine(captionText);
            }

            writer.WriteLine();
            writer.WriteLine();

            writer.Write(emptyHeaderRow);
            TreatChildren(writer, node);
            writer.WriteLine();
        }

        private static void WriteTelegramFallback(TextWriter writer, HtmlNode node)
        {
            var captionText = node.SelectSingleNode("caption")?.InnerText?.Trim();
            if (!string.IsNullOrEmpty(captionText)) {
                writer.WriteLine();
                writer.WriteLine(StringUtils.EscapeTelegramMarkdownV2(captionText));
            }

            var rows = node.SelectNodes(".//tr");
            if (rows == null || rows.Count == 0) {
                var plainText = HtmlEntity.DeEntitize(node.InnerText).Trim();
                if (!string.IsNullOrEmpty(plainText)) {
                    writer.Write(StringUtils.EscapeTelegramMarkdownV2(plainText));
                }

                return;
            }

            var renderedRows = new List<string>(rows.Count);
            foreach (var row in rows) {
                var cells = row.SelectNodes("./th|./td");
                if (cells == null || cells.Count == 0) {
                    var rowText = NormalizeWhitespace(row.InnerText);
                    if (!string.IsNullOrEmpty(rowText)) {
                        renderedRows.Add(rowText);
                    }

                    continue;
                }

                var cellTexts = cells
                    .Select(cell => NormalizeWhitespace(cell.InnerText))
                    .ToArray();
                renderedRows.Add(string.Join(" | ", cellTexts));
            }

            if (renderedRows.Count == 0) {
                return;
            }

            writer.WriteLine();
            writer.WriteLine("```");
            foreach (var row in renderedRows) {
                writer.WriteLine(StringUtils.EscapeTelegramMarkdownV2Code(row));
            }

            writer.Write("```");
            writer.WriteLine();
        }

        private static string NormalizeWhitespace(string value)
        {
            var decoded = HtmlEntity.DeEntitize(value);
            return string.Join(" ", decoded.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries));
        }

        private static bool HasNoTableHeaderRow(HtmlNode node)
        {
            var thNode = node.SelectNodes("//th")?.FirstOrDefault();
            return thNode == null;
        }

        private static string EmptyHeader(HtmlNode node)
        {
            var firstRow = node.SelectNodes("//tr")?.FirstOrDefault();

            if (firstRow == null) {
                return string.Empty;
            }

            var colCount = firstRow.ChildNodes.Count(n => n.Name.Contains("td") || n.Name.Contains("th"));

            var headerRowItems = new List<string>();
            var underlineRowItems = new List<string>();

            for (var i = 0; i < colCount; i++) {
                headerRowItems.Add("<!---->");
                underlineRowItems.Add("---");
            }

            var headerRow = $"| {string.Join(" | ", headerRowItems)} |{Environment.NewLine}";
            var underlineRow = $"| {string.Join(" | ", underlineRowItems)} |{Environment.NewLine}";

            return headerRow + underlineRow;
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/Td.cs
================================================
using System.IO;
using HtmlAgilityPack;


namespace ReverseMarkdown.Converters {
    public class Td : ConverterBase {
        public Td(Converter converter) : base(converter)
        {
            Converter.Register("td", this);
            Converter.Register("th", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            if (Converter.Config.SlackFlavored) {
                throw new SlackUnsupportedTagException(node.Name);
            }

            if (Converter.Config.TelegramMarkdownV2) {
                throw new TelegramUnsupportedTagException(node.Name);
            }

            var colSpan = GetColSpan(node);

            var content = TreatChildrenAsString(node)
                .Trim()
                .ReplaceLineEndings("<br>");

            for (var i = 0; i < colSpan; i++) {
                writer.Write(' ');
                writer.Write(content);
                writer.Write(" |");
            }
        }

        /// <summary>
        /// Given node within td tag, checks if newline should be prepended. Will not prepend if this is the first node after any whitespace
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        public static bool FirstNodeWithinCell(HtmlNode node)
        {
            var parentName = node.ParentNode.Name;
            // If p is at the start of a table cell, no leading newline
            if (parentName is "td" or "th") {
                var pNodeIndex = node.ParentNode.ChildNodes.GetNodeIndex(node);
                var firstNodeIsWhitespace = node.ParentNode.FirstChild.Name == "#text" && string.IsNullOrWhiteSpace(node.ParentNode.FirstChild.InnerText);
                if (pNodeIndex == 0 || (firstNodeIsWhitespace && pNodeIndex == 1)) return true;
            }

            return false;
        }

        /// <summary>
        /// Given node within td tag, checks if newline should be appended. Will not append if this is the last node before any whitespace
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        public static bool LastNodeWithinCell(HtmlNode node)
        {
            var parentName = node.ParentNode.Name;
            if (parentName is "td" or "th") {
                var pNodeIndex = node.ParentNode.ChildNodes.GetNodeIndex(node);
                var cellNodeCount = node.ParentNode.ChildNodes.Count;
                var lastNodeIsWhitespace = node.ParentNode.LastChild.Name == "#text" && string.IsNullOrWhiteSpace(node.ParentNode.LastChild.InnerText);
                if (pNodeIndex == cellNodeCount - 1 || (lastNodeIsWhitespace && pNodeIndex == cellNodeCount - 2)) return true;
            }

            return false;
        }

        private int GetColSpan(HtmlNode node)
        {
            var colSpan = 1;

            if (Converter.Config.TableHeaderColumnSpanHandling && node.Name == "th") {
                colSpan = node.GetAttributeValue("colspan", 1);
            }

            return colSpan;
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/Text.cs
================================================
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using HtmlAgilityPack;
using ReverseMarkdown.Helpers;


namespace ReverseMarkdown.Converters {
    public partial class Text : ConverterBase {
        public Text(Converter converter) : base(converter)
        {
            Converter.Register("#text", this);
        }


        #region values

        private static readonly StringReplaceValues _escapedKeyChars = new() {
            ["*"] = @"\*",
            ["_"] = @"\_",
        };

        private static readonly StringReplaceValues _escapedKeyCharsReverse = new() {
            [@"\*"] = "*",
            [@"\_"] = "_",
        };

        private static readonly StringReplaceValues _preserveAngleBrackets = new() {
            ["&lt;"] = "%3C",
            ["&gt;"] = "%3E",
        };

        private static readonly StringReplaceValues _unPreserveAngleBrackets = new() {
            ["%3C"] = "&lt;",
            ["%3E"] = "&gt;",
        };

        [GeneratedRegex(@"`.*?`")]
        private static partial Regex BackTicks();

        [GeneratedRegex(@"!?\[[^\]\r\n]*\]\([^\)\r\n]*\)")]
        private static partial Regex CommonMarkInlineLinkOrImagePattern();

        [GeneratedRegex(@"\[[^\]\r\n]+\]\[[^\]\r\n]*\]")]
        private static partial Regex CommonMarkReferenceLinkPattern();

        [GeneratedRegex(@"(?m)^ {0,3}\[[^\]\r\n]+\]:")]
        private static partial Regex CommonMarkLinkDefinitionPattern();

        #endregion


        public override void Convert(TextWriter writer, HtmlNode node)
        {
            var isCommonMark = Converter.Config.CommonMark;
            var innerText = node.InnerText;
            if (innerText is " " or "&nbsp;" || innerText == "\u00A0") {
                if (node.ParentNode.Name is not ("ol" or "ul")) {
                    if (isCommonMark && innerText != " ") {
                        writer.Write("&nbsp;");
                    }
                    else {
                        writer.Write(' ');
                    }

                    return;
                }
            }

            if (isCommonMark) {
                if (innerText == "!" && node.NextSibling?.Name == "a") {
                    writer.Write("\\!");
                    return;
                }

                if (innerText == "*" && node.NextSibling?.Name == "img") {
                    writer.Write("\\*");
                    return;
                }
            }

            TreatText(writer, node);
        }


        private void TreatText(TextWriter writer, HtmlNode node)
        {
            var isCommonMark = Converter.Config.CommonMark;
            var isTelegram = Converter.Config.TelegramMarkdownV2;
            var rawText = isCommonMark
                ? node.OuterHtml
                : node.InnerText;
            if (isCommonMark &&
                (rawText.Contains("<!--", StringComparison.Ordinal) ||
                 rawText.Contains("<![CDATA[", StringComparison.Ordinal) ||
                 rawText.Contains("</", StringComparison.Ordinal) ||
                 rawText.Contains("<!", StringComparison.Ordinal))) {
                writer.Write(EscapeSpecialCommonMarkCharacters(rawText));
                return;
            }
            var text = isCommonMark
                ? PreserveCommonMarkAmpersands(rawText)
                : rawText;
            var hasLeadingNbsp = isCommonMark &&
                                 System.Text.RegularExpressions.Regex.IsMatch(
                                     rawText,
                                     @"^\s*(&nbsp;|&#160;)",
                                     System.Text.RegularExpressions.RegexOptions.IgnoreCase
                                 );
            var parent = node.ParentNode;

            if (string.IsNullOrEmpty(text)) {
                return;
            }

            //strip leading spaces and tabs for text within a list item
            var shouldTrim = (
                parent.Name is "table" or "thead" or "tbody" or "ol" or "ul" or "th" or "tr"
            );
            var replaceLineEndings = (
                parent.Name is "p" or "#document" &&
                //(Context.AncestorsAny("th") || Context.AncestorsAny("td"))
                (parent.Ancestors("th").Any() || parent.Ancestors("td").Any())
            );

            // Prevent &lt; and &gt; from being converted to < and > as this will be interpreted as HTML by Markdown
            //var search = SearchValues.Create(["&lt;", "&gt;"], StringComparison.Ordinal);
            //var index = text.IndexOfAny(search);
            //if (index != -1) {
            //}

            // html decode:
            var content = BackTicks().Replace(text, p => DecodeHtml(p.Value));
            content = content.Replace(_preserveAngleBrackets);
            content = DecodeHtml(content);
            content = content.Replace(_unPreserveAngleBrackets);

            if (isCommonMark) {
                content = EscapeCommonMarkBackslashes(content);
                content = RestoreCommonMarkAmpersands(content);
                content = content.Replace("\u00A0", "&nbsp;");
                content = content.Replace("\t", "&#9;");
                if (hasLeadingNbsp && !content.StartsWith("&nbsp;")) {
                    content = "&nbsp;" + content.TrimStart();
                }
                if (!content.Contains("<!--", StringComparison.Ordinal) &&
                    !content.Contains("<![CDATA[", StringComparison.Ordinal) &&
                    !content.Contains("<!", StringComparison.Ordinal) &&
                    !content.Contains("</", StringComparison.Ordinal)) {
                    content = content.Replace("<", "&lt;").Replace(">", "&gt;");
                }
            }

            if (isTelegram && parent.Name != "a") {
                content = StringUtils.EscapeTelegramMarkdownV2(content);
            }

            if (shouldTrim) {
                content = content.Trim();
            }

            if (Converter.Config.CommonMark) {
                content = Regex.Replace(content, "\r?\n\r?\n", "&#10;&#10;");
            }

            if (replaceLineEndings) {
                content = content.ReplaceLineEndings("<br>");
            }

            if (Converter.Config.CommonMark && node.PreviousSibling?.Name == "br") {
                content = content.TrimStart('\r', '\n');
            }

            if (!isTelegram && parent.Name != "a" && !Converter.Config.SlackFlavored) {
                content = content.Replace(_escapedKeyChars);
                // Preserve Key Chars Within BackTicks:
                content = BackTicks().Replace(content, p => p.Value.Replace(_escapedKeyCharsReverse));
            }

            if (isCommonMark) {
                content = EscapeSpecialCommonMarkCharacters(content, node);
                content = content.Replace("`", "\\`");
            }

            if (isCommonMark || Converter.Config.EscapeMarkdownLineStarts) {
                content = EscapeMarkdownLineStarts(content);
            }

            writer.Write(content);
        }


        private const string AmpersandPlaceholder = "__REVERSEMARKDOWN_AMP__";
        private const string NbspPlaceholder = "__REVERSEMARKDOWN_NBSP__";

        private static string EscapeSpecialCommonMarkCharacters(string content, HtmlNode node)
        {
            var escaped = EscapeSpecialCommonMarkCharacters(content);
            return TryGetMarkedDelimiterSequence(node, out var delimiterMarks)
                ? EscapeMarkedDelimiters(escaped, delimiterMarks)
                : escaped;
        }

        private static string EscapeSpecialCommonMarkCharacters(string content)
        {
            return content.StartsWith('`') && content.EndsWith('`')
                ? content
                : EscapeCommonMarkPatternDelimiters(content);
        }

        private static bool TryGetMarkedDelimiterSequence(HtmlNode node, out bool[] delimiterMarks)
        {
            delimiterMarks = Array.Empty<bool>();

            var parent = node.ParentNode;
            if (parent == null || parent.ChildNodes.Count < 2) {
                return false;
            }

            if (!parent.ChildNodes.Any(child => child.NodeType != HtmlNodeType.Text)) {
                return false;
            }

            var parentText = new StringBuilder();
            var nodeInnerText = node.InnerText;
            var nodeStart = -1;

            foreach (var child in parent.ChildNodes) {
                if (child == node) {
                    nodeStart = parentText.Length;
                }

                parentText.Append(child.InnerText);
            }

            if (nodeStart < 0 || string.IsNullOrEmpty(nodeInnerText)) {
                return false;
            }

            var combined = parentText.ToString();
            var shouldEscape = new bool[combined.Length];
            var hasDelimitersToEscape =
                MarkCommonMarkPatternDelimiters(shouldEscape, combined, CommonMarkInlineLinkOrImagePattern()) |
                MarkCommonMarkPatternDelimiters(shouldEscape, combined, CommonMarkReferenceLinkPattern()) |
                MarkCommonMarkPatternDelimiters(shouldEscape, combined, CommonMarkLinkDefinitionPattern());

            if (!hasDelimitersToEscape) {
                return false;
            }

            var marks = new bool[nodeInnerText.Count(IsCommonMarkDelimiter)];
            var delimiterIndex = 0;
            var hasMarkedDelimiterInNode = false;
            for (var i = 0; i < nodeInnerText.Length; i++) {
                var currentChar = nodeInnerText[i];
                if (!IsCommonMarkDelimiter(currentChar)) {
                    continue;
                }

                var marked = shouldEscape[nodeStart + i];
                marks[delimiterIndex++] = marked;
                hasMarkedDelimiterInNode |= marked;
            }

            if (!hasMarkedDelimiterInNode) {
                return false;
            }

            delimiterMarks = marks;
            return true;
        }

        private static string EscapeMarkedDelimiters(string content, bool[] delimiterMarks)
        {
            if (string.IsNullOrEmpty(content) || delimiterMarks.Length == 0) {
                return content;
            }

            var escaped = new StringBuilder(content.Length);
            var delimiterIndex = 0;
            for (var i = 0; i < content.Length; i++) {
                var currentChar = content[i];
                if (!IsCommonMarkDelimiter(currentChar)) {
                    escaped.Append(currentChar);
                    continue;
                }

                var shouldEscape = delimiterIndex < delimiterMarks.Length && delimiterMarks[delimiterIndex];
                delimiterIndex++;
                if (shouldEscape && (i == 0 || content[i - 1] != '\\')) {
                    escaped.Append('\\');
                }

                escaped.Append(currentChar);
            }

            return escaped.ToString();
        }

        private static string EscapeCommonMarkPatternDelimiters(string content)
        {
            if (string.IsNullOrEmpty(content)) {
                return content;
            }

            var shouldEscape = new bool[content.Length];
            var hasDelimitersToEscape =
                MarkCommonMarkPatternDelimiters(shouldEscape, content, CommonMarkInlineLinkOrImagePattern()) |
                MarkCommonMarkPatternDelimiters(shouldEscape, content, CommonMarkReferenceLinkPattern()) |
                MarkCommonMarkPatternDelimiters(shouldEscape, content, CommonMarkLinkDefinitionPattern());

            if (!hasDelimitersToEscape) {
                return content;
            }

            var escaped = new StringBuilder(content.Length);
            for (var i = 0; i < content.Length; i++) {
                if (shouldEscape[i] && (i == 0 || content[i - 1] != '\\')) {
                    escaped.Append('\\');
                }

                escaped.Append(content[i]);
            }

            return escaped.ToString();
        }

        private static bool MarkCommonMarkPatternDelimiters(bool[] shouldEscape, string content, Regex pattern)
        {
            var foundDelimiters = false;

            foreach (Match match in pattern.Matches(content)) {
                var end = match.Index + match.Length;
                for (var i = match.Index; i < end; i++) {
                    if (IsCommonMarkDelimiter(content[i])) {
                        shouldEscape[i] = true;
                        foundDelimiters = true;
                    }
                }
            }

            return foundDelimiters;
        }

        private static bool IsCommonMarkDelimiter(char character)
        {
            return character is '[' or ']' or '(' or ')' or '{' or '}';
        }

        private static string PreserveCommonMarkAmpersands(string rawContent)
        {
            if (string.IsNullOrEmpty(rawContent)) {
                return rawContent;
            }

            var preserved = Regex.Replace(rawContent, "&amp;", AmpersandPlaceholder, RegexOptions.IgnoreCase);
            preserved = Regex.Replace(preserved, "&nbsp;", NbspPlaceholder, RegexOptions.IgnoreCase);
            return preserved;
        }

        private static string RestoreCommonMarkAmpersands(string content)
        {
            if (string.IsNullOrEmpty(content)) {
                return content;
            }

            var restored = Regex.Replace(
                content,
                AmpersandPlaceholder + @"(#[0-9]+|#x[0-9a-fA-F]+|[A-Za-z][A-Za-z0-9]+);",
                "\\&$1;"
            );

            restored = restored.Replace(NbspPlaceholder, "&nbsp;");
            return restored.Replace(AmpersandPlaceholder, "&");
        }

        private static string EscapeCommonMarkBackslashes(string content)
        {
            if (string.IsNullOrEmpty(content)) {
                return content;
            }

            return content.Replace("\\", "\\\\");
        }

        private static string EscapeMarkdownLineStarts(string content)
        {
            if (string.IsNullOrEmpty(content)) {
                return content;
            }

            var normalized = content.ReplaceLineEndings("\n");
            var lines = normalized.Split('\n');
            for (var i = 0; i < lines.Length; i++) {
                lines[i] = EscapeLineStart(lines[i]);
            }

            return string.Join("\n", lines);
        }

        private static string EscapeLineStart(string line)
        {
            if (string.IsNullOrEmpty(line)) {
                return line;
            }

            var index = 0;
            var maxIndent = 3;
            while (index < line.Length && line[index] == ' ' && index < maxIndent) {
                index++;
            }

            if (index >= line.Length || line[index] == '\\') {
                return line;
            }

            var current = line[index];
            if (IsSetextUnderline(line, index)) {
                return line.Insert(index, "\\");
            }

            if (current == '#') {
                return line.Insert(index, "\\");
            }

            if ((current == '-' || current == '*' || current == '+') && IsLineMarker(line, index, 1)) {
                return line.Insert(index, "\\");
            }

            if (char.IsDigit(current)) {
                var digitEnd = index;
                while (digitEnd < line.Length && char.IsDigit(line[digitEnd])) {
                    digitEnd++;
                }

                if (digitEnd < line.Length && (line[digitEnd] == '.' || line[digitEnd] == ')')) {
                    if (IsLineMarker(line, digitEnd, 1)) {
                        return line.Insert(digitEnd, "\\");
                    }
                }
            }

            return line;
        }

        private static bool IsSetextUnderline(string line, int index)
        {
            var trimmed = line[index..].TrimEnd();
            if (trimmed.Length < 3) {
                return false;
            }

            var first = trimmed[0];
            if (first != '-' && first != '=') {
                return false;
            }

            for (var i = 1; i < trimmed.Length; i++) {
                if (trimmed[i] != first) {
                    return false;
                }
            }

            return true;
        }

        private static bool IsLineMarker(string line, int markerIndex, int minTrailingSpaces)
        {
            var nextIndex = markerIndex + 1;
            if (nextIndex >= line.Length) {
                return false;
            }

            var spaceCount = 0;
            while (nextIndex < line.Length && line[nextIndex] == ' ') {
                spaceCount++;
                nextIndex++;
            }

            return spaceCount >= minTrailingSpaces;
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/Tr.cs
================================================
using System.IO;
using System.Linq;
using HtmlAgilityPack;
using ReverseMarkdown.Helpers;


namespace ReverseMarkdown.Converters {
    public class Tr : ConverterBase {
        public Tr(Converter converter) : base(converter)
        {
            Converter.Register("tr", this);
        }

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            if (Converter.Config.SlackFlavored) {
                throw new SlackUnsupportedTagException(node.Name);
            }

            if (Converter.Config.TelegramMarkdownV2) {
                throw new TelegramUnsupportedTagException(node.Name);
            }

            var content = TreatChildrenAsString(node).TrimEnd();

            if (string.IsNullOrWhiteSpace(content)) {
                return;
            }

            // if parent is an ordered or unordered list
            // then table need to be indented as well
            var indent = IndentationFor(node);

            writer.Write(indent);
            writer.Write('|');
            writer.Write(content);
            writer.WriteLine();

            if (ShouldWriteUnderline(node)) {
                writer.Write(indent);
                WriteUnderline(writer, node, Converter.Config.TableHeaderColumnSpanHandling);
            }
        }


        private bool ShouldWriteUnderline(HtmlNode node)
        {
            var tableNode = node.Ancestors("table").FirstOrDefault();
            var firstRow = tableNode?.SelectSingleNode(".//tr");

            if (firstRow == null || tableNode == null) {
                return false;
            }

            var isFirstRow = firstRow == node;

            return (
                isFirstRow && (
                    IsTableHeaderRow(node) ||
                    UseFirstRowAsHeaderRow(node)
                )
            );

            bool UseFirstRowAsHeaderRow(HtmlNode node)
            {
                var hasNoHeaderRow = tableNode.SelectNodes(".//th")?.FirstOrDefault() == null;
                return hasNoHeaderRow
                       && Converter.Config.TableWithoutHeaderRowHandling ==
                       Config.TableWithoutHeaderRowHandlingOption.Default;
            }

            static bool IsTableHeaderRow(HtmlNode node)
            {
                return node.ChildNodes.FindFirst("th") != null!;
            }
        }


        private static void WriteUnderline(TextWriter writer, HtmlNode node, bool tableHeaderColumnSpanHandling)
        {
            var nodes = node.ChildNodes.Where(x => x.Name is "th" or "td");
            foreach (var nd in nodes) {
                var colSpan = GetColSpan(nd, tableHeaderColumnSpanHandling);
                var styles = StringUtils.ParseStyle(nd.GetAttributeValue("style", string.Empty));
                styles.TryGetValue("text-align", out var align);

                var content = align switch {
                    "left" => ":---",
                    "right" => "---:",
                    "center" => ":---:",
                    _ => "---"
                };

                for (var i = 0; i < colSpan; i++) {
                    writer.Write('|');
                    writer.Write(' ');
                    writer.Write(content);
                    writer.Write(' ');
                }
            }

            writer.WriteLine('|');
        }


        private static int GetColSpan(HtmlNode node, bool tableHeaderColumnSpanHandling)
        {
            var colSpan = 1;

            if (tableHeaderColumnSpanHandling && node.Name is "th") {
                colSpan = node.GetAttributeValue("colspan", 1);
            }

            return colSpan;
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Converters/UnknownTagReplacer.cs
================================================
using System.IO;
using HtmlAgilityPack;

namespace ReverseMarkdown.Converters {
    internal sealed class UnknownTagReplacer : ConverterBase {
        private readonly string _replacement;
        private readonly string _tagName;

        public UnknownTagReplacer(Converter converter, string tagName, string replacement) : base(converter)
        {
            _tagName = tagName;
            _replacement = replacement;
        }

        internal string Replacement => _replacement;

        public override void Convert(TextWriter writer, HtmlNode node)
        {
            var content = TreatChildrenAsString(node);

            if (string.IsNullOrWhiteSpace(content) || string.IsNullOrEmpty(_replacement) ||
                Context.AncestorsAny(_tagName)) {
                writer.Write(content);
                return;
            }

            var spaceSuffix = node.NextSibling?.Name == _tagName ? " " : string.Empty;
            TreatEmphasizeContentWhitespaceGuard(writer, content, _replacement, spaceSuffix);
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Helpers/LineSplitEnumerator.cs
================================================
using System;


namespace ReverseMarkdown.Helpers;

/// <summary>
/// Enumerates the lines in a string as ReadOnlySpan&lt;char&gt;
/// </summary>
internal ref struct LineSplitEnumerator(string text) {
    private int _pos = 0;
    private ReadOnlySpan<char> _current;

    public LineSplitEnumerator GetEnumerator() => this;
    public ReadOnlySpan<char> Current => _current;

    public bool MoveNext()
    {
        var pos = _pos;
        if ((uint) pos >= (uint) text.Length) {
            return false;
        }

        _current = ReadNextLine(pos);
        return true;
    }

    private ReadOnlySpan<char> ReadNextLine(int pos)
    {
        var remaining = text.AsSpan(pos);
        var foundLineLength = remaining.IndexOfAny('\r', '\n');
        if (foundLineLength >= 0) {
            var result = text.AsSpan(pos, foundLineLength);

            var ch = remaining[foundLineLength];
            pos += foundLineLength + 1;
            if (ch == '\r') {
                if ((uint) pos < (uint) text.Length && text[pos] == '\n') {
                    pos++;
                }
            }

            _pos = pos;

            return result;
        }
        else {
            var result = text.AsSpan(pos);
            _pos = text.Length;
            return result;
        }
    }
}


================================================
FILE: src/ReverseMarkdown/Helpers/StringExtensions.cs
================================================
using System;
using System.Text.RegularExpressions;


namespace ReverseMarkdown.Helpers;

public static class StringExtensions {
    public static string Chomp(this string content)
    {
        return content
            .ReplaceLineEndings(string.Empty)
            .Trim();
    }

    internal static LineSplitEnumerator ReadLines(this string content)
    {
        return new LineSplitEnumerator(content);
    }

    internal static string Replace(this string content, StringReplaceValues replacements)
    {
        return replacements.Replace(content);
    }

    public static string FixMultipleNewlines(this string markdown)
    {
        var normalizedMarkdown = markdown.ReplaceLineEndings(Environment.NewLine);
        return Regex.Replace(normalizedMarkdown, $"{Environment.NewLine}{{2,}}", Environment.NewLine + Environment.NewLine);
    }

    /// <summary>
    /// Compacts HTML by removing line breaks and collapsing whitespace between HTML tags only,
    /// while preserving spaces within tag content. This is useful for nested tables in markdown.
    /// </summary>
    /// <param name="html">The HTML string to compact</param>
    /// <returns>Compacted HTML string suitable for embedding in markdown tables</returns>
    public static string CompactHtmlForMarkdown(this string html)
    {
        if (string.IsNullOrEmpty(html))
            return html;

        // First remove all line endings
        html = html.ReplaceLineEndings("");

        // Use regex to collapse multiple spaces between tags (>...< patterns)
        // This preserves spaces within tag content
        html = Regex.Replace(html, @">\s+<", "> <");

        // Also trim any leading/trailing whitespace
        return html.Trim();
    }

    public static string CompactHtmlForCommonMarkBlock(this string html)
    {
        if (string.IsNullOrEmpty(html)) {
            return html;
        }

        html = html.ReplaceLineEndings("\n");

        html = Regex.Replace(html, @"<pre><code>(.*?)</code></pre>", match =>
        {
            var content = match.Groups[1].Value.Replace("\n", "&#10;");
            return $"<pre><code>{content}</code></pre>";
        }, RegexOptions.Singleline);

        html = Regex.Replace(html, @">\s+<", "><");

        return html.Trim();
    }
}


================================================
FILE: src/ReverseMarkdown/Helpers/StringReplaceValues.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;


namespace ReverseMarkdown.Helpers;

internal class StringReplaceValues : Dictionary<string, string> {
    private Regex? _regex;

    private Regex Regex => _regex ??= new Regex($"{string.Join("|", this.Keys.Select(Regex.Escape))}", RegexOptions.Compiled);

    public string Replace(string input)
    {
        var offset = 0;
        StringBuilder? sb = null;
        foreach (var match in Regex.EnumerateMatches(input)) {
            sb ??= new StringBuilder(input.Length);
            sb.Append(input.AsSpan(offset, match.Index - offset));
            sb.Append(this[input.AsSpan(match.Index, match.Length).ToString()]);
            offset = match.Index + match.Length;
        }

        if (sb is not null && offset != input.Length) {
            sb.Append(input.AsSpan(offset, input.Length - offset));
        }

        return sb?.ToString() ?? input;
    }
}


================================================
FILE: src/ReverseMarkdown/Helpers/StringUtils.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;


namespace ReverseMarkdown.Helpers;

public static partial class StringUtils {
    private static readonly HashSet<char> TelegramMarkdownV2EscapableChars = new() {
        '_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!', '\\'
    };

    /// <summary>
    /// <para>Gets scheme for provided uri string to overcome different behavior between windows/linux. https://github.com/dotnet/corefx/issues/1745</para>
    /// Assume http for url starting with //
    /// <para>Assume file for url starting with /</para>
    /// Otherwise give what <see cref="Uri.Scheme" /> gives us.
    /// <para>If non parseable by Uri, return empty string. Will never return null</para>
    /// </summary>
    /// <returns></returns>
    public static string GetScheme(string url)
    {
        //IETF RFC 3986
        if (Regex.IsMatch(url, "^//[^/]")) {
            return "http";
        }
        //Unix style path
        else if (Regex.IsMatch(url, "^/[^/]")) {
            return "file";
        }
        else if (Uri.TryCreate(url, UriKind.Absolute, out var uri)) {
            return uri.Scheme;
        }
        else {
            return string.Empty;
        }
    }


    [GeneratedRegex(@"\r?\n\s*\r?\n", RegexOptions.Singleline)]
    private static partial Regex LinkTextRegex();

    private static readonly StringReplaceValues _linkTextReplaceValues = new() {
        ["["] = @"\[",
        ["]"] = @"\]",
    };

    /// <summary>
    /// Escape/clean characters which would break the [] section of a markdown []() link
    /// </summary>
    public static string EscapeLinkText(string rawText)
    {
        return LinkTextRegex()
            .Replace(rawText, Environment.NewLine)
            .Replace(_linkTextReplaceValues);
    }


    private static readonly Dictionary<string, string> EmptyStyles = new();

    public static Dictionary<string, string> ParseStyle(string? style)
    {
        if (string.IsNullOrEmpty(style)) {
            return EmptyStyles;
        }

        var styles = style!.Split(';');
        return styles.Select(styleItem => styleItem.Split(':'))
            .Where(styleParts => styleParts.Length == 2)
            .Select(styleParts => new[] { styleParts[0].Trim(), styleParts[1].Trim() })
            .DistinctBy(styleParts => styleParts[0], StringComparer.OrdinalIgnoreCase)
            .ToDictionary(styleParts => styleParts[0], styleParts => styleParts[1], StringComparer.OrdinalIgnoreCase);
    }

    public static string EscapeTelegramMarkdownV2(string content)
    {
        return EscapeChars(content, c => TelegramMarkdownV2EscapableChars.Contains(c));
    }

    public static string EscapeTelegramMarkdownV2Code(string content)
    {
        return EscapeChars(content, c => c is '`' or '\\');
    }

    public static string EscapeTelegramMarkdownV2LinkUrl(string url)
    {
        return EscapeChars(url, c => c is ')' or '\\');
    }

    private static string EscapeChars(string content, Func<char, bool> mustEscape)
    {
        if (string.IsNullOrEmpty(content)) {
            return content;
        }

        var builder = new StringBuilder(content.Length + 16);
        foreach (var c in content) {
            if (mustEscape(c)) {
                builder.Append('\\');
            }

            builder.Append(c);
        }

        return builder.ToString();
    }
}


================================================
FILE: src/ReverseMarkdown/ImageUtils.cs
================================================
using System;
using System.IO;
using System.Text.RegularExpressions;

namespace ReverseMarkdown
{
    public static class ImageUtils
    {
        public static string SaveBase64Image(string base64Image, string targetDir, string? fileNameWithoutExtension = null)
        {
            if (string.IsNullOrWhiteSpace(base64Image))
                throw new ArgumentException("Base64 image string is null or empty.", nameof(base64Image));

            // Regex to extract mime type and base64 data
            var match = Regex.Match(base64Image, @"^data:(?<mime>[\w/\-\.]+);base64,(?<data>.+)$");
            if (!match.Success)
                throw new FormatException("Invalid or non supported base64 image format.");

            var mimeType = match.Groups["mime"].Value;  // e.g. "image/png"
            var base64Data = match.Groups["data"].Value;

            // Get extension from a MIME type
            var extension = MimeTypeToExtension(mimeType);
            
            if (string.IsNullOrEmpty(extension))
                throw new FormatException("Invalid or non supported base64 image format.");

            // Decode
            var imageBytes = Convert.FromBase64String(base64Data);

            // Build the file path
            var fileName = (fileNameWithoutExtension ?? "image") + extension;
            var filePath = Path.Combine(targetDir, fileName);
            File.WriteAllBytes(filePath, imageBytes);
            return filePath;
        }

        public static bool IsValidBase64ImageData(string data)
        {
            var match = Regex.Match(data, @"^data:image/(?<ext>[a-zA-Z0-9+\-\.]+);base64,(?<data>[a-zA-Z0-9+/=\r\n]+)$");

            if (!match.Success)
                return false;

            var mimeType = "image/" + match.Groups["ext"].Value;
            var extension = MimeTypeToExtension(mimeType);
            return !string.IsNullOrWhiteSpace(extension);
        }

        private static string MimeTypeToExtension(string mimeType)
        {
            return mimeType.ToLower() switch
            {
                "image/png" => ".png",
                "image/jpeg" => ".jpg",
                "image/jpg" => ".jpg",
                "image/gif" => ".gif",
                "image/bmp" => ".bmp",
                "image/tiff" => ".tiff",
                "image/webp" => ".webp",
                "image/svg+xml" => ".svg",
                _ => ""
            };
        }
    }

}

================================================
FILE: src/ReverseMarkdown/ReverseMarkdown.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <Description>ReverseMarkdown is a Html to Markdown converter library in c#</Description>
    <VersionPrefix>5.3.0</VersionPrefix>
    <Authors>Babu Annamalai</Authors>
    <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
    <LangVersion>preview</LangVersion>
    <Nullable>enable</Nullable>
    <PackageProjectUrl>https://github.com/mysticmind/reversemarkdown-net</PackageProjectUrl>
    <PackageLicenseExpression>MIT</PackageLicenseExpression>
    <PackageTags>htmltomarkdown;html2markdown;converthtml;htmlconverter;html;converter;markdown</PackageTags>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
  </ItemGroup>
  <!--SourceLink specific settings-->
  <PropertyGroup>
    <RepositoryUrl>https://github.com/mysticmind/reversemarkdown-net.git</RepositoryUrl>
    <RepositoryType>git</RepositoryType>
    <PublishRepositoryUrl>true</PublishRepositoryUrl>
    <IncludeSymbols>true</IncludeSymbols>
    <SymbolPackageFormat>snupkg</SymbolPackageFormat>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.103" PrivateAssets="all" />
  </ItemGroup>
</Project>


================================================
FILE: src/ReverseMarkdown/UnknownTagException.cs
================================================
using System;


namespace ReverseMarkdown;

public class UnknownTagException(string tagName) : Exception($"Unknown tag: {tagName}");


================================================
FILE: src/ReverseMarkdown/UnsupportedTagExtension.cs
================================================
using System;


namespace ReverseMarkdown;

public class UnsupportedTagException : Exception {
    internal UnsupportedTagException(string message) : base(message)
    {
    }
}

public class SlackUnsupportedTagException : UnsupportedTagException {
    internal SlackUnsupportedTagException(string tagName)
        : base($"<{tagName}> tags cannot be converted to Slack-flavored markdown")
    {
    }
}

public class TelegramUnsupportedTagException : UnsupportedTagException {
    internal TelegramUnsupportedTagException(string tagName)
        : base($"<{tagName}> tags cannot be converted to Telegram MarkdownV2")
    {
    }
}


================================================
FILE: src/ReverseMarkdown.Benchmark/Benchmark.md
================================================
# Benchmark Results for ReverseMarkdown

**Legends**

```
  Mean      : Arithmetic mean of all measurements
  Error     : Half of 99.9% confidence interval
  StdDev    : Standard deviation of all measurements
  Gen0      : GC Generation 0 collects per 1000 operations
  Gen1      : GC Generation 1 collects per 1000 operations
  Gen2      : GC Generation 2 collects per 1000 operations
  Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
  Ratio     : Mean of the method divided by the mean of the baseline
  1 s       : 1 Second (1 sec)
  1 ms      : 1 Millisecond (1 ms)
```

---

## Comparing ReverseMarkdown v4.7.1 vs TextWriter approach

**Hardware: AMD Ryzen 9 3900X 3.80GHz, 24 cores, 12 physical cores**

```
BenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.6899)
AMD Ryzen 9 3900X 3.80GHz, 1 CPU, 24 logical and 12 physical cores
.NET SDK 9.0.306
  [Host]   : .NET 9.0.10 (9.0.10, 9.0.1025.47515), X64 RyuJIT x86-64-v3
  .NET 9.0 : .NET 9.0.10 (9.0.10, 9.0.1025.47515), X64 RyuJIT x86-64-v3
```

Job=.NET 9.0  Runtime=.NET 9.0

### Running with `Files/1000-paragraphs.html` file (size: 442KB) and `1000` paragraphs.

| Method                     | Mean     | Error    | StdDev   | Gen0        | Gen1       | Gen2       | Allocated | Ratio |
|--------------------------- |---------:|---------:|---------:|------------:|-----------:|-----------:|----------:|------:|
| ReverseMarkdown TextWriter | 25.96 ms | 0.519 ms | 0.744 ms |   3500.0000 |  1593.7500 |  1218.7500 |  24.95 MB |     1 |
| ReverseMarkdown v4.7.1     | 147.7 ms |  2.95 ms |  8.37 ms | 100500.0000 | 97500.0000 | 97500.0000 | 896.35 MB | 5.689 |

Outliers:
- ReverseMarkdown v4.7.1: .NET 9.0 -> 2 outliers were removed (172.08 ms, 174.42 ms)

### Running with `Files/10k-paragraphs.html` file (size: 3.7MB) and `10k` paragraphs.

| Method                     | Mean     | Error   | StdDev  | Gen0        | Gen1        | Gen2        | Allocated | Ratio |
|--------------------------- |---------:|--------:|--------:|------------:|------------:|------------:|----------:|------:|
| ReverseMarkdown TextWriter |  0.232 s | 0.005 s | 0.009 s |  20000.0000 |   5000.0000 |   1000.0000 | 210.15 MB |     1 |
| ReverseMarkdown v4.7.1     |  14.08 s | 0.280 s | 0.747 s | 624000.0000 | 605000.0000 | 603000.0000 |  75.27 GB | 60.69 |

Outliers:
- ReverseMarkdown TextWriter: .NET 9.0 -> 1 outlier  was  removed (265.53 ms)
- ReverseMarkdown v4.7.1: .NET 9.0 -> 2 outliers were removed (17.04 s, 17.23 s)

### Running with `Files/huge.html` file (size: 16MB) and `41312` paragraphs.

| Method                     | Mean      | Error    | StdDev   | Gen0         | Gen1         | Gen2         | Allocated    | Ratio  |
|--------------------------- |----------:|---------:|---------:|-------------:|-------------:|-------------:|-------------:|-------:|
| ReverseMarkdown TextWriter |   0.944 s | 0.0175 s | 0.0172 s |   86000.0000 |   20000.0000 |    3000.0000 |    955.34 MB |      1 |
| ReverseMarkdown v4.7.1     | 191.611 s | 3.7686 s | 4.4863 s | 2735000.0000 | 2666000.0000 | 2659000.0000 | 640544.94 MB | 202.97 |


================================================
FILE: src/ReverseMarkdown.Benchmark/CompareBenchmark.cs
================================================
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;


namespace ReverseMarkdown.Benchmark;

[SimpleJob(RuntimeMoniker.Net90)]
[RPlotExporter]
[MemoryDiagnoser]
public class CompareBenchmark {
    private string _html = null!;
    private Converter _converter = null!;

    [GlobalSetup]
    public void Setup()
    {
        //_html = FileHelper.ReadFile("Files/1000-paragraphs.html");
        _html = FileHelper.ReadFile("Files/10k-paragraphs.html");
        _converter = new Converter(new Config());
    }

    [Benchmark]
    public string ReverseMarkdown()
    {
        var result = _converter.Convert(_html);
        return result;
    }
}


================================================
FILE: src/ReverseMarkdown.Benchmark/FileHelper.cs
================================================
namespace ReverseMarkdown.Benchmark {
    internal sealed class FileHelper {
        public static string ReadFile(string path)
        {
            return File.ReadAllText(Path.Combine(AppContext.BaseDirectory, path));
        }
    }
}


================================================
FILE: src/ReverseMarkdown.Benchmark/Files/1000-paragraphs.html
================================================
<p><strong>CODICE DI PROCEDURA PENALE</strong></p>
<p><strong>COMMENTATO</strong></p>
<p><strong>CON DOTTRINA E GIURISPRUDENZA</strong></p>
<p><strong>AGGIORNATO AL 2 NOVEMBRE 2025</strong></p>
<p><strong>Art. 1 - Giurisdizione penale 1. </strong></p>
<p>1. La giurisdizione penale è esercitata dai giudici previsti dalle leggi di ordinamento giudiziario [102 Cost.; 1 ord. giud.] secondo le norme di questo codice.</p>
<p> </p>
<p>[1] Per la giurisdizione dei tribunali militari, v. art. 103 Cost., artt. 263 e 264 c.p.m.p. e artt. 231 e 232 c.p.m.g. Per gli appartenenti al Corpo di polizia penitenziaria, v. l'art. 24 l. 15 dicembre 1990, n. 395. Per la giurisdizione penale della Corte costituzionale, limitata ai reati di alto tradimento e attentato alla Costituzione dallal. cost. 16 gennaio 1989, n. 1, v. artt. 134 e 135 Cost., come modificati da detta legge. V. anche l'art. 96 Cost., che sottopone alla giurisdizione ordinaria i reati «ministeriali».</p>
<p>1. Inquadramento</p>
<p>L'articolo 1 « fissa una norma che, pur essendo essenzialmente ricognitiva, (...), attraverso il collegamento con la disciplina dell'ordinamento giudiziario, vale a sottolineare, sin dall'esordio del discorso normativo, la regola dell'esercizio della giurisdizione penale da parte dei giudici ordinari, in linea con il disposto dell'art. 102 Cost. » (Rel. prog. prel. c.p.p., 2).</p>
<p>2. La giurisdizione penale</p>
<p>La giurisdizione penale può essere esercitata da giudici ordinari (cd. giurisdizione ordinaria) o speciali (cd. giurisdizione speciale): nel primo caso, è esercitata da magistrati ordinari istituiti e regolati dalle norme sull'ordinamento giudiziario (art. 102, comma 1, Cost.); nel secondo caso, in considerazione del divieto di istituire nuovi giudici straordinari o speciali (art. 102, comma 2, Cost.), è esercitata dagli unici giudici speciali ammessi dall'ordinamento, i tribunali militari (che, ai sensi dell'art. 103 Cost., in tempo di guerra, hanno la giurisdizione stabilita dalla legge e, in tempo di pace, hanno giurisdizione soltanto per i reati militari commessi da appartenenti alle Forze armate) e la Corte costituzionale (che giudica, ai sensi degli artt. 134 e 90 Cost., sulle accuse promosse contro il Presidente della Repubblica per altro tradimento ed attentato alla Costituzione).</p>
<p>Parte della dottrina (Tranchina, in Siracusano-Galati-Tranchina-Zappalà, Diritto, 89 s.) ricomprende in questo ambito anche il Tribunale per i minorenni, competente a conoscere tutti i reati commessi da soggetti che non abbiano superato, al momento della commissione, il diciottesimo anno d'età.</p>
<p>In proposito, secondo la giurisprudenza, la sentenza pronunciata dal Tribunale ordinario per fatti commessi da soggetto all'epoca degli stessi minorenne, non è inesistente, ma viziata da nullità assoluta per incompetenza funzionale, vizio non più deducibile dopo il giudicato (Cass. V, n. 4310/2016); analogamente, si è chiarito che anche la sentenza di condanna pronunciata dal Tribunale per i minorenni per fatti commessi da un soggetto maggiore di età non può dirsi inesistente ed il ricorso per cassazione fondato su tale motivo è inammissibile per carenza di interesse, in quanto, in applicazione del principio del favor rei, stante il trattamento più favorevole applicabile, va sempre affermata la competenza del Tribunale per i minorenni quando le risultanze non offrono la certezza che l'imputato abbia raggiunto la maggiore età (Cass. II, n. 13153/2017).</p>
<p>Il riferimento alla necessità che la giurisdizione sia esercitata dai giudici previsti dalle leggi di ordinamento giudiziario secondo le norme del codice di rito indica che la normativa codicistica ha natura di lex generalis e va coordinata con quanto stabilito dagli artt. 207 ss. disp. coord.: in particolare, l'art. 207 cit. stabilisce che le disposizioni del codice si osservano nei procedimenti relativi a tutti i reati anche se previsti da leggi speciali (salvo quanto stabilito nei titoli II e III: è, ad es., il caso dell'art. 210 disp. coord., in tema di competenza per materia o per territorio, prevista « in deroga alla disciplina del codice », e dell'art. 212 disp. coord., che, fuori dai casi di esercizio dell'azione civile nel processo penale ex art. 74, consente l'intervento del terzo nel processo penale soltanto nei limiti ed alle condizioni previste dagli artt. 91-94). In tal modo, si afferma la prevalenza delle norme processuali codicistiche rispetto alle plurime disposizioni a carattere processuale che si erano succedute nel tempo.</p>
<p>Le Sezioni unite penali hanno chiarito che il vigente codice di procedura penale, tutte le volte che indica il giudice competente all'esercizio della giurisdizione nei diversi stati e gradi del procedimento e del processo, lo fa con riferimento a singoli organi giudiziari, senza cenno alcuno all'identità fisica dei magistrati che detti organi compongono (Cass.S.U., n. 26/2000: in applicazione del principio, si è ritenuto che, nella fase del giudizio, la richiesta di adozione, modifica o revoca di una misura cautelare personale coercitiva deve essere esaminata e decisa dal tribunale, in composizione monocratica o collegiale, dalla Corte d'assise, dalla Corte d'appello o dalla Corte d'assise d'appello investiti della cognizione, nel merito, del processo, preferibilmente, ma non necessariamente, nella composizione fisica dei magistrati componenti l'organo giudicante che sta conducendo l'istruttoria dibattimentale o che, pur avendo definito il processo in quel determinato grado, è ancora in possesso dei relativi atti; conformi,Cass. V, n. 26880/2002, e Cass. VI, n. 30582/2003).</p>
<p><br />2.1. Giurisdizione penale e responsabilità degli enti immateriali</p>
<p>Nella giurisdizione del giudice penale rientra anche la responsabilità degli enti immateriali prevista dal d.lgs. n. 231/2001, intrinsecamente connessa alla commissione di una delle fattispecie di reato che, ai sensi degli artt. 24-26 d.lgs. n. 231/2001, costituiscono il presupposto della responsabilità dell'ente immateriale.</p>
<p><br />3. Segue. Casistica</p>
<p>La giurisprudenza ha ritenuto che la sentenza penale, costituente atto di esercizio della giurisdizione, sia inesistente quando sia emessa da un soggetto privato o pubblico estraneo all'ordinamento giudiziario, che non abbia la qualità di giudice e che si sia arrogato i relativi poteri: è tale la sentenza emessa a non judice, non quella emessa da giudice incompetente (Cass. II, n. 21956/2005, in riferimento a fattispecie nella quale il tribunale per i minorenni aveva emesso sentenze nei confronti di soggetti già maggiorenni all'epoca dei fatti, e la relativa sentenza — pur essendo nulla per difetto di competenza — non è stata ritenuta inesistente, ma suscettibile di passare in giudicato secondo le ordinarie regole di rito).</p>
<p>In tema di riparto di giurisdizione tra Stati, si è ritenuto che l'azione di risarcimento del danno da reato o di restituzione (art. 185 c.p.) nei confronti dell'imputato e dei responsabili civili dimoranti od aventi stabilimento principale in uno Stato estero aderente alle Convenzioni di Lugano del 16 settembre 1988 e del 30 ottobre 2007 può essere legittimamente esercitata davanti al giudice italiano presso il quale è esercitata l'azione penale (Cass. I, n. 7941/2015).</p>
<p><br />4. I rapporti con la giurisdizione civile</p>
<p>Il nuovo codice di procedura penale non ha riprodotto la disposizione di cui all'art. 3,comma 2, del codice di rito abrogato: la giurisprudenza civile ne ha desunto che il nostro ordinamento non è più ispirato al principio dell'unità della giurisdizione e della prevalenza del giudizio penale su quello civile, essendo stato, dal legislatore, instaurato il sistema della quasi completa autonomia e separazione tra i due processi, nel senso che, ad eccezione di alcune e limitate ipotesi di sospensione del processo civile previste dall'art. 75, comma 3 (e, cioè, se il danneggiato proponga l'azione per il risarcimento dei danni in sede civile, dopo essersi costituito parte civile nel processo penale o dopo la sentenza penale di primo grado), da un lato, il processo civile deve proseguire il suo corso senza essere influenzato dal processo penale e, dall'altro, il giudice civile deve procedere ad un autonomo accertamento dei fatti e della responsabilità (civile) con pienezza di cognizione, non essendo vincolato alle soluzioni ed alle qualificazioni del giudice penale, con la conseguenza che lo stesso giudice civile non è vincolato a sospendere il giudizio avanti a lui pendente in attesa della definizione del giudizio penale correlato in cui si sia proceduto ad una valutazione di risultanze probatorie in senso parzialmente difforme (Cass. civ. lav., n. 1095/2007; Cass. civ. II, n. 27494/2009). Ciò, peraltro, non preclude al giudice civile la possibilità di utilizzare, come fonte del proprio convincimento, le prove raccolte in un giudizio penale con sentenza passata in cosa giudicata, e di fondare la decisione su elementi e circostanze già acquisiti con le garanzie di legge in quella sede, procedendo, a tal fine, a diretto esame del contenuto del materiale probatorio, ovvero ricavando tali elementi dalla sentenza, o, se necessario, dagli atti del relativo processo, in modo da accertare esattamente i fatti materiali sottoponendoli al proprio vaglio critico; tale possibilità non comporta, tuttavia, anche l'obbligo per il giudice civile — in presenza di un giudicato penale — di esaminare e valutare le prove e le risultanze acquisite nel processo penale (Cass. civ. II, n. 6478/2005: nella specie, è stata confermata la decisione impugnata che, nel dichiarare apocrifo il testamento olografo impugnato di falso, aveva utilizzato soltanto le prove acquisite nel giudizio civile, senza valutare gli elementi raccolti nel processo penale, all'esito del quale il beneficiario delle disposizioni testamentarie era stato assolto dall'imputazione di avere falsificato il testamento).</p>
<p><br />5. Segue. Casistica<br />5.1. Il ricorso contro il decreto di liquidazione dei compensi dovuti al perito ed agli altri ausiliari del giudice</p>
<p>La giurisprudenza ha ritenuto che il ricorso contro il decreto di liquidazione dei compensi dovuti al perito ed agli altri ausiliari del giudice deve essere proposto davanti al tribunale od alla corte d'appello che svolgono funzioni civili o penali, a seconda che il provvedimento impugnato sia stato adottato in un procedimento civile o penale: il giudice erroneamente adito difetta, infatti, di giurisdizione, e non semplicemente di competenza, in quanto, sia l'art. 1 sia l'art. 1 c.p.c. attribuiscono ai giudici la funzione loro demandata secondo le norme proprie di ciascuno dei codici stessi (Cass. IV, n. 22483/2007: con riguardo ad una opposizione a decreto di pagamento emesso in favore di un Ctu, erroneamente proposta davanti al giudice civile, la S.C. — alla luce del principio — ha ribadito che il tribunale civile avrebbe dovuto limitarsi a dichiarare il ricorso improcedibile od inammissibile, senza trasmettere gli atti al Presidente del tribunale per la fissazione dell'udienza davanti al giudice penale).</p>
<p>Sarebbe giuridicamente inesistente il provvedimento giurisdizionale che, quantunque materialmente esistente ed ascrivibile ad un giudice, risulti, tuttavia, privo del requisito minimo della provenienza da un organo giudiziario investito del potere di decisione in una materia riservata agli organi della giurisdizione penale e, come tale, risulti esorbitante, siccome invasivo dello specifico campo riservato al giudice penale dai limiti interni ed oggettivi che, alla stregua dell'ordinamento positivo, discriminano il ramo civile e quello penale nella distribuzione della giurisdizione (Cass.S.U., n. 25/1999, in fattispecie relativa ad un'ordinanza del tribunale civile, ritenuta viziata da difetto assoluto di giurisdizione, con conseguente accoglimento del ricorso del difensore avverso decreto del g.i.p. militare in materia di liquidazione dei compensi professionali a norma della all'epoca vigente l. n. 217/1990).</p>
<p><br />5.2. Misure di prevenzione</p>
<p>In tema di misure di prevenzione, si è ritenuto che sussiste il difetto assoluto della giurisdizione penale, in favore di quella civile, a conoscere della controversia tra l'Agenzia del Demanio, alla quale sia stato trasferito un immobile, a seguito di confisca definitiva, ed il proprietario della pertinente area di sedime, in ordine all'esercizio dei diritti reali relativi ai suddetti beni (Cass. I, n. 20793/2009, in fattispecie nella quale, con incidente di esecuzione, il proprietario del suolo sul quale sorgeva l'immobile, definitivamente acquisito al patrimonio dello Stato, aveva chiesto al giudice della misura di prevenzione lo “sgombero” del manufatto dal suo terreno; conforme, Cass. I, n. 21063/2010, in fattispecie avente ad oggetto la domanda di rilascio promossa dal proprietario di un complesso immobiliare occupato dai beni del complesso aziendale di un'impresa confiscata in via definitiva).</p>
<p>Si è, inoltre, chiarito che è devoluta alla cognizione del giudice civile, non di quello penale, l'azione di responsabilità nei confronti degli amministratori giudiziari incaricati della gestione di beni aziendali sequestrati nell'ambito di un procedimento di prevenzione (Cass. V, n. 18859/2013, che, in applicazione del principio, e rilevato il difetto della giurisdizione penale in favore di quella civile, ha annullato senza rinvio la condanna al risarcimento dei danni pronunziata dal giudice penale all'esito del giudizio di contestazione del rendiconto finale).</p>
<p><br />6. I rapporti con la giurisdizione amministrativa</p>
<p>Un orientamento giurisprudenziale ormai consolidato ritiene che l'autorità giudiziaria ordinaria non abbia il potere di valutare la conformità a legge di un arret di un'altra giurisdizione (ad esempio, di una sentenza del tribunale amministrativo regionale coperta da giudicato), in quanto il cittadino — pena la vanificazione dei suoi diritti civili — non può essere privato della facoltà di fare affidamento sugli strumenti della tutela giurisdizionale posti a sua disposizione dall'ordinamento (Cass. III, n. 54/1996, in fattispecie riguardante la configurabilità del reato di costruzione senza concessione edilizia, nella quale era stato disposto un provvedimento di sequestro, nonostante l'esistenza di una pronunzia definitiva del T.A.R. che affermava la legittimità della costruzione; conforme, Cass. III, n. 39707/2003, per la quale, in materia edilizia, il potere del giudice penale di accertare la conformità alla legge ed agli strumenti urbanistici di una costruzione edilizia, e conseguentemente di valutare la legittimità di eventuali provvedimenti amministrativi concessori o autorizzatori, trova un limite nei provvedimenti giurisdizionali del giudice amministrativo passati in giudicato che abbiano espressamente affermato la legittimità della concessione o dell'autorizzazione edilizia ed il conseguente diritto del cittadino alla realizzazione dell'opera. Nella specie, peraltro, la S.C. ha evidenziato che le sentenze amministrative invocate dal ricorrente si erano limitate, la prima, a pronunciare un annullamento per mero difetto di motivazione, la seconda, un annullamento per mancata acquisizione di un parere obbligatorio).</p>
<p>Si è anche evidenziato che il giudice penale, nel caso in cui accerti e dichiari (ai sensi dell'art. 537 c.p.p.) la falsità di atti e/o documenti costituenti presupposto per l'inserimento di un soggetto nella graduatoria di un concorso pubblico, non può  modificare la predetta graduatoria, eliminando il nominativo del soggetto responsabile dell'accertata falsità, poiché detto potere può essere esercitato esclusivamente dall'amministrazione competente nelle forme proprie dei provvedimenti amministrativi: l'approvazione della graduatoria di concorsi a pubblici impieghi è, infatti, un provvedimento di amministrazione attiva, attraverso il quale la PA fa proprio l'operato della commissione esaminatrice, e quindi soltanto la PA competente ha il potere di modificare le graduatorie, pur se, in ipotesi, agli effetti penali illegittimamente formate (Cass. V, n. 32035/2014). </p>
<p><br />7. Giurisdizione e competenza</p>
<p>Può ritenersi pacifico che, se con il termine “Giurisdizione” si fa riferimento all'esercizio della funzione sovrana dello jus dicere attraverso l'applicazione di norme astratte a casi concreti, per l'attuazione della volontà della legge, con quello di “Competenza” si mira ad individuare, secondo regole di materia, territorio o funzione, la sfera entro la quale i diversi organi giudiziari esercitano la giurisdizione loro conferita (Cass.S.U., n. 26/2000, in motivazione).</p>
<p><br />7.1. L'assegnazione interna degli affari di giustizia</p>
<p>L'assegnazione di un affare ad una sezione piuttosto che ad un'altra attiene non alla giurisdizione, ma alla competenza interna e, comunque, ai sensi dell'art. 33, non si considera afferente alla capacità del giudice; ne consegue che non è vietata, dall'ordinamento, l'assegnazione dei procedimenti, aventi ad oggetto la ricusazione di magistrati addetti a funzioni penali, ad una sezione della corte d'appello che non sia anche incaricata della trattazione di affari penali (Cass. II, n. 20288/2004: la S.C. ha anche ribadito che, in mancanza — nell'ordinamento vigente — di una distinzione tra i ruoli organici dei magistrati addetti all'esercizio della giurisdizione penale e quelli dei magistrati addetti all'esercizio della giurisdizione civile, deve ritenersi che tutti i magistrati dell'ufficio giudiziario siano, in eguale modo, potenzialmente investiti del potere giurisdizionale in materia civile e penale, come desumibile anche dagli artt. 7-bis e 7-ter r.d. n. 12/1941, recante le disposizioni sull'ordinamento giudiziario, che prevedono un apposito provvedimento tabellare per la ripartizione delle funzioni all'interno dell'ufficio giudiziario e l'assegnazione degli affari alle sezioni; conforme, Cass. II, n. 27948/2008). In parziale difformità da detto orientamento, Cass. III, n. 38112/2006 ha fatto salva l'ipotesi dell'assegnazione effettuata al di fuori di ogni criterio tabellare, proprio per costituire un giudice ad hoc, e che, pertanto, essendo caratterizzata dall'arbitrio nella designazione, può essere definita extra ordinem, peraltro esclusa nel caso esaminato, nel quale la sezione assegnataria di un processo aveva trasmesso direttamente gli atti ad altra sezione, in applicazione di nuovi ed oggettivi criteri di assegnazione contenuti in un provvedimento tabellare sopravvenuto del Presidente del Tribunale.</p>
<p><br />7.2. Il regolamento delle questioni di giurisdizione</p>
<p>Le Sezioni unite penali della Corte di cassazione (Cass.S.U., n. 1/1991). hanno evidenziato che non esiste — nel sistema della giurisdizione (e della competenza) penale — un rimedio preventivo analogo a quello previsto dall'art. 41 c.p.c. nell'ambito del processo civile, poiché il codice di rito penale (artt. 51 ss.) disciplina soltanto casi di conflitto di giurisdizione o di competenza.</p>
<p><br />8. Le giurisdizioni speciali<br />8.1. Il collegio per i reati ministeriali</p>
<p>La l. cost. n. 1/1989, in relazione ai reati ministeriali, pur non avendo sottratto al p.m. la titolarità dell'azione penale, ha attribuito il potere di compiere le indagini preliminari ad uno speciale collegio costituito presso ciascun tribunale del capoluogo del distretto di corte d'appello competente per territorio: permane, quindi, il riferimento al Procuratore della Repubblica come legittimo destinatario della notitia criminis, ma, ai sensi dell'art. 6, ult. comma, l. cost. n. 1/1989, è interdetto a quest'ultimo il compimento di qualsiasi indagine. Detto collegio svolge, oltre alle funzioni proprie del p.m. nel procedimento ordinario, anche quelle devolute, dall'ordinamento processuale vigente, al g.i.p., e tale sua peculiare competenza, unitamente alla sua composizione, ne fanno un organo specializzato, dotato di specifica competenza funzionale, in relazione alla particolare qualificazione dei reati dei quali lo stesso deve occuparsi.</p>
<p>Le sezioni unite hanno ritenuto che il collegio per i reati ministeriali non è un giudice speciale né un organo della giustizia penale-costituzionale, ma è soltanto un organo specializzato della giurisdizione ordinaria, il quale, dotato di specifica competenza funzionale in relazione alla particolare qualificazione dei reati dei quali deve occuparsi, esercita, con riguardo a questi ultimi, oltre alle funzioni proprie del p.m., anche quelle del g.i.p.; se ne è desunto che, ove tali ultime funzioni vengano esercitate da un normale g.i.p., il provvedimento da questi adottato (nella specie, trattavasi di ordinanza di custodia cautelare emessa su richiesta del locale ufficio del p.m.), non può dirsi viziato da carenza di giurisdizione, ma soltanto da incompetenza funzionale che dà luogo, comunque, ad una nullità assoluta ed insanabile (Cass.S.U., 20 luglio 1994, De Lorenzo).</p>
<p><br />8.2. La giurisdizione militare</p>
<p>L'art. 103, comma 3, Cost. stabilisce che « i tribunali militari in tempo di guerra hanno la giurisdizione stabilita dalla legge. In tempo di pace hanno giurisdizione soltanto per i reati militari commessi da appartenenti alle Forze armate ».</p>
<p>La giurisprudenza costituzionale (Corte cost. n. 429/1992) ha evidenziato che la nozione di « appartenenza alle Forze armate », nell'art. 103, comma 3, Cost., è più ristretta di quella accolta nel codice penale militare di pace, essendo, la prima, destinata a circoscrivere entro rigorosi limiti la giurisdizione speciale militare, e, la seconda, ispirata a far coincidere giurisdizione e assoggettamento alla legge penale militare; la diversità di piani di iurisdictio e lex, presente in Costituzione ma assente nel codice penale militare, vale a sottolineare il principio secondo cui, in tempo di pace, per i reati militari, la giurisdizione normalmente da adire è quella ordinaria, mentre quella speciale militare è eccezionale e giustificata solo ove si tratti di reati comuni commessi “sotto le armi”. Posto che il Costituente ha inteso conservare la giurisdizione dei tribunali militari in tempo di pace solo per i reati militari commessi da « appartenenti alle Forze armate », nell'accezione ristretta di cittadini che stanno prestando il servizio militare, « le persone alle quali è applicabile la legge penale militare » assoggettabili alla giurisdizione militare, cui si riferisce l'art. 263 c.p. mil. p., non possono essere altre o di più di quelle indicate dallo stesso codice negli artt. 3 (militari in servizio) e 5 (militari considerati in servizio), questi ultimi caratterizzati dall'assenza del servizio effettivo ma, al contempo, dalla permanenza di un legame organico con la forza in servizio. Per tutti gli altri militari in congedo illimitato, che il codice penale militare, ma non la Costituzione, considera appartenenti alle Forze armate, la cognizione dei reati militari spetta ai giudici ordinari, e non a quelli militari. È stata, conseguentemente, dichiarata l'illegittimità costituzionale dell'art. 263 c.p. mil. p. nella parte in cui assoggettava alla giurisdizione militare le persone alle quali è applicabile la legge penale militare, anziché i soli militari in servizio alle armi o considerati tali dalla legge al momento del commesso reato.</p>
<p>In tempo di pace, la giurisdizione “normale” è quella ordinaria, mentre quella militare ha carattere eccezionale (così, fra le tante, Cass. I, 31 maggio 1994, Confl. giur. in proc. Natale, con la precisazione che il principio di cui all'art. 103, comma 3, Cost. opera con riferimento al solo processo di cognizione: se ne è desunto che esso non può essere invocato in tema di giurisdizione nel processo esecutivo, ed in particolare in quello di sorveglianza, anche se il medesimo principio stabilisce il criterio generale per delimitare l'ambito di estensione rispettivo della giurisdizione ordinaria e di quella speciale in detto processo), ed è subordinata a due limiti:</p>
<p>a) uno di natura oggettiva, rappresentato dal fatto che ne formano oggetto esclusivamente i reati militari;</p>
<p>b) l'altro di natura soggettiva, costituito dall'appartenenza alle Forze armate degli autori dei reati, i quali, pertanto, devono trovarsi in effettivo servizio attuale alle armi.</p>
<p>Successivamente, la giurisprudenza ha affermato che la distinzione fra l'ambito di giurisdizione proprio dell'autorità giudiziaria ordinaria e quello proprio dei tribunali militari è, per ogni suo aspetto, fissata direttamente dall'art. 103, comma 3, Cost., il quale, nella sua ultima proposizione, stabilisce che i predetti tribunali « in tempo di pace hanno giurisdizione soltanto per i reati militari commessi da appartenenti alle Forze armate ». L'Amministrazione militare deve intendersi circoscritta nelle strutture occorrenti per l'organizzazione del personale e dei mezzi materiali destinati alla difesa armata dello Stato, ed i beni in dotazione della stessa si identificano in quelli che, a norma delle leggi sulla contabilità generale dello Stato, sono amministrati dal Ministero della difesa o dai corpi militari; non rientrano, tra i beni appartenenti all'Amministrazione militare, quelli assegnati ad altri Ministeri per essere adoperati dagli stessi o dai servizi da essi dipendenti od amministrati, e quelli dei quali all'amministrazione militare è devoluta la mera gestione sotto un profilo esclusivamente privatistico (Cass. I, n. 1410/2000: in applicazione del principio, e considerato che il corpo della Guardia di Finanza fa parte integrante delle Forze armate dello Stato, è stata ritenuta sussistente la giurisdizione dell'autorità giudiziaria militare, e non di quella ordinaria, con riguardo ad una truffa che si assumeva consumata da un sottufficiale di detto corpo in danno dell'Amministrazione di appartenenza, mediante il conseguimento dell'indebito rimborso di spese di missione eccedenti quanto effettivamente pagato; conforme,Cass. I, n. 3491/2000, inerente alla medesima fattispecie, ma con la precisazione che l'indebito rimborso di spese di missione eccedenti quanto effettivamente pagato integra un danno non solo per il Ministero delle Finanze — che non può essere considerato ente militare —, ma anche per il Corpo di appartenenza, che ha natura di ente militare, dovendo riconoscersi la giurisdizione militare tutte le volte in cui siano ravvisabili specifiche fattispecie penali per interessi direttamente collegabili ad organismi o enti aventi natura militare).</p>
<p>Da ultimo, Cass. II, n. 20136/2018 ha ribadito che la configurabilità di un reato militare postula, ai sensi dell'art. 234 c.p.mil.p.  che sia il soggetto attivo che quello passivo abbiano qualifica di “militare”, ed ha, inoltre, precisato che la giurisdizione militare sussiste soltanto nei casi in cui il danno provocato dal reato incida anche sul corpo militare di appartenenza, mentre se il danno è interamente sopportato (non già dall'amministrazione  militare bensì) da un ente pubblico del tutto estraneo all'apparato militare, sussiste la giurisdizione ordinaria.  </p>
<p><br />9. Segue . Casistica</p>
<p>Agli ufficiali in congedo, sia pure collocati in ausiliaria, è applicabile il codice penale militare solo quando ciò sia espressamente previsto dalla legge (Cass. VI, n. 2326/1998, per la quale non può rispondere del reato di collusione un ufficiale della Guardia di Finanza collocato in ausiliaria, che ricopra l'ufficio di ispettore del S.E.C.I.T.).</p>
<p>Le norme contenute nel nuovo codice di procedura penale si applicano anche ai processi riguardanti reati di competenza dell'autorità giudiziaria militare (Cass. I, 22 marzo 1991, Pagliarini ed altri: se ne è desunto che l'art. 314 c.p.mil.p., relativo ai casi in cui è facoltativa l'emissione del mandato di cattura, è da ritenere implicitamente abrogato dalla norma di sbarramento discendente dal combinato disposto degli artt. 278 e 280).</p>
<p>L'attrazione nella giurisdizione del giudice ordinario dei procedimenti per reati concorrenti, comuni e militari, opera solo se il reato comune è più grave di quello militare, mentre negli altri casi le sfere di giurisdizione, ordinaria e militare, rimangono separate, con la conseguenza che al giudice militare appartiene la cognizione dei reati militari e al giudice ordinario quella per i reati comuni (Cass. I, n. 5680/2015).</p>
<p>Per quanto riguarda i criteri di attribuzione della giurisdizione in materia di esecuzione della pena, si rinvia subart. 665 e ss.</p>
<p><br />10. Cenni di diritto penale internazionale</p>
<p>Si ammette, generalmente, l'esistenza « di una norma internazionale consuetudinaria in base alla quale l'organo di uno Stato estero (Capo di Governo o suo rappresentante, membri di missioni speciali, etc.) non è penalmente responsabile degli atti compiuti nell'esercizio delle sue funzioni » (V. Brunelli, 2).</p>
<p>L'immunità dalla giurisdizione è generale rispetto agli agenti diplomatici mentre, per quanto riguarda i funzionari e gli impiegati consolari, è limitata agli atti compiuti nell'esercizio delle funzioni consolari.</p>
<p>In particolare, si è osservato che, in virtù degli artt. 23, 31, 35, 41 e 43 della Convenzione di Vienna sulle relazioni consolari del 24 aprile 1963, ratificata e resa esecutiva in Italia con l. 9 agosto 1967 n. 804:</p>
<p>a) il console straniero in Italia è immune dalla giurisdizione italiana solo per gli atti compiuti nell'esercizio della funzione consolare;</p>
<p>b) egli può essere arrestato e trattenuto in carcere per gravissimi reati, cioè per delitti non colposi punibili con la reclusione non inferiore nel massimo a cinque anni;</p>
<p>c) l'inviolabilità dei locali consolari è limitata alla parte destinata esclusivamente ad ufficio (il c.d. “archivio consolare”);</p>
<p>d) la libertà di comunicazione del consolato è garantita e protetta per le finalità istituzionali dell'ufficio;</p>
<p>e) la facoltà, dello Stato territoriale, di informare quello estero, senza motivazione, che il console non è gradito, è posta a tutela dello Stato di residenza del console, e non di quello di provenienza (Cass. I, 24 marzo 1983, Nuvoletta).</p>
<p>Gli artt. 12, lett. A), del Protocollo sui privilegi e le immunità della Comunità europea, allegato al Trattato istitutivo (firmato a Bruxelles in data 8 aprile 1965, e ratificato e reso esecutivo con l. n. 437/1966), e 16 lett. A), dei Protocolli sui privilegi e le immunità dell'E.S.R.O. e dell'E.L.D.O. (firmati, il primo, a Parigi, in data 31 ottobre 1963, ed il secondo, a Londra, in data 29 giugno 1964, ed entrambi ratificati e resi esecutivi con l. n. 1313/1967) accordano analoga immunità ai funzionari ed agli agenti di organizzazioni internazionali.</p>
<p>La giurisprudenza ha chiarito che, in tema di esenzione dalla giurisdizione penale di un agente diplomatico, poiché quest'ultima qualità si acquista, ai sensi della Convenzione di Vienna sulle relazioni diplomatiche, soltanto con la notificazione dello Stato accreditante allo Stato accreditato, l'immunità non spetta all'agente consolare del quale il paese d'origine si sia limitato a dichiarare, in note verbali dirette al Ministero degli Affari esteri, la qualità di agente diplomatico, senza avere mai provveduto alla relativa notificazione formale (Cass. V, n. 16659/2002, e Cass. II, n. 3679/2005).</p>
<p><br />10.1. I reati commessi da militari appartenenti ad uno Stato membro della N.A.T.O</p>
<p>La Convenzione tra gli Stati partecipanti al Trattato Nord Atlantico (N.A.T.O.) sullo statuto delle loro Forze armate, firmato a Londra il 19 giugno 1951, ratificata e resa esecutiva con l. n. 1335/1955, stabilisce che « le autorità militari dello Stato di origine hanno il diritto di esercitare sul territorio dello Stato di soggiorno i poteri di giurisdizione penale e disciplinare che conferisce loro la legislazione dello Stato di origine su tutte le persone soggette alla legge militare di questo Stato » (art. VII).</p>
<p>Il d.P.R. n. 1666/1956 disciplina « la facoltà di rinunciare al diritto di priorità nell'esercizio della giurisdizione (...) nei casi previsti nell'articolo VII paragrafo 3 lett. c) della Convenzione ». Quest'ultimo, a sua volta, stabilisce che, se lo Stato che ha il diritto di esercitare in via prioritaria la propria giurisdizione decide di rinunziarvi, deve notificare, al più presto possibile, la sua decisione alle autorità dell'altro Stato; le autorità dello Stato che ha il diritto di esercitare in via prioritaria la propria giurisdizione devono esaminare con benevolenza le istanze di rinuncia alla propria giurisdizione, presentate dalle autorità dell'altro Stato, nei casi in cui ritengano che tali richieste siano giustificate da considerazioni particolarmente importanti.</p>
<p>La giurisprudenza ha ritenuto che, quando la priorità nell'esercizio della giurisdizione per fatti commessi da militari stranieri di stanza in Italia spetta all'autorità giudiziaria italiana, secondo le norme della Convenzione di Londra, il giudice italiano può validamente esercitare la giurisdizione penale se il Ministro della Giustizia non si avvale, prima della notifica all'imputato del decreto di citazione per il dibattimento di primo grado, della facoltà di richiedere allo stesso giudice la dichiarazione di rinuncia al diritto di priorità; tale facoltà non appartiene all'autorità giudiziaria, che è vincolata alla rigorosa osservanza dei principi della officiosità e della indisponibilità dell'azione penale, ma al più qualificato organo del potere amministrativo, posto che essa sottintende una valutazione di natura squisitamente politica (Cass. I, 29 marzo 1982, Hanlon). La rinuncia al diritto di priorità nell'esercizio della giurisdizione di cui all'art. VII del Trattato N.A.T.O. è una facoltà discrezionale, che può essere esercitata solo dal competente organo politico-amministrativo (Ministro per la Giustizia, su richiesta o previo parere del Ministro per gli esteri) e non spetta al giudice italiano, la cui sentenza si pone come atto meramente dichiarativo della rinuncia, poiché al giudice compete solamente di verificare «l'esistenza delle condizioni previste dalla legge per l'ammissibilità e la validità della rinuncia»; ne consegue che la sentenza, che dichiara la rinuncia in assenza della determinazione del competente organo politico-amministrativo, è un provvedimento radicalmente nullo, viziato da eccesso di potere e ricorribile per cassazione ex art. 606 comma 1 lett. a), risolvendosi nell'esercizio di una potestà riservata dalla legge ad un organo amministrativo (Cass. V, n. 4640/1999, in fattispecie relativa alla Convenzione di Londra, concernente un militare delle forze N.A.T.O. di stanza in Italia, indagato per il reato di cui all'art. 582 c.p., nella quale il ministro aveva respinto l'istanza di rinuncia del capo divisione dell'ufficio legale del comando Setaf di Vicenza).</p>
<p>In presenza della volontà, dello Stato cui appartiene il militare imputato, di esercitare la propria prioritaria giurisdizione, e dell'adesione dello Stato di soggiorno, il giudice italiano deve limitarsi a prendere atto del proprio difetto di giurisdizione; sarebbe, pertanto, abnorme (e, come tale, immediatamente ricorribile per cassazione) il provvedimento del g.u.p. che, in siffatta situazione, dichiari la propria giurisdizione, emettendo il decreto che dispone il giudizio nei confronti di un militare N.A.T.O. (Cass. I, 27 gennaio 1997, Thierry Bonne).</p>
<p>Questa decisione ha sollevato perplessità in parte della dottrina (Ghiron, 3056 ss.), per il rilievo che non troverebbe riscontro nel quadro normativo di riferimento « l'affermazione che l'attuale normativa obbliga il giudice ad astenersi dall'esercizio della giurisdizione, (...) “per effetto di un'attività politica coerente con la Convenzione” »; pertanto, pur essendo innegabile, nel caso di specie, il difetto di giurisdizione del giudice italiano, tale difetto «si sarebbe dovuto far valere nel giudizio e poi eventualmente in sede di impugnazione».</p>
<p>Una giurisprudenza di merito ha ritenuto, sempre in relazione alla Convenzione di Londra sullo statuto dei militari N.A.T.O., che spetta esclusivamente all'autorità giudiziaria stabilire se il reato sia stato commesso nell'espletamento del servizio militare, poiché solo in tal caso la priorità da attribuire, a richiesta, alla giurisdizione dello Stato di appartenenza comporterebbe il difetto di giurisdizione dello Stato di soggiorno; si è aggiunto che ricorre il presupposto della giurisdizione concorrente — in presenza della quale si rende necessario stabilire a quale giurisdizione accordare priorità — soltanto quando si è in presenza della commissione di un fatto che risulti previsto come reato sia dalla legislazione dello Stato di origine del militare che da quella dello Stato di soggiorno, anche se le rispettive fattispecie criminose abbiano diverso nomen iuris, ovvero tutelino beni-interessi disomogenei (G.i.p. Trib. Trento, 13 luglio 1998, Ashby ed altri: nella specie, è stata ritenuta la priorità della giurisdizione U.S.A. per il disastro del Cermis, determinato da un aereo militare americano che, volando in addestramento a bassa quota, aveva reciso di netto i cavi di una funivia, cagionandone il crollo, con la conseguente morte degli occupanti).</p>
<p>In argomento, la dottrina (Barberini 3599 ss.) ha osservato che un esercizio non credibile della giurisdizione nel Paese cui il Trattato N.A.T.O. conferisce il diritto ad esercitarla, solleva problemi non indifferenti, e che sarebbe il caso di valutare l'opportunità della revisione (consentita dall'art. XVII della Convenzione) di alcune tra le disposizioni più discutibili, come « quelle che di fatto consentono alle forze militari dei Paesi N.A.T.O. la possibilità di godere di una vera e propria impunità, tenuto conto, da un lato, dell'incontrollabilità dell'effettivo esercizio della giurisdizione, in Paesi, come gli Stati Uniti, in cui l'esercizio dell'azione penale è discrezionale, nonché, dall'altro, dell'impossibilità di controllare le modalità dell'esercizio concreto della giurisdizione, in ordinamenti, come quello statunitense, in cui la trasparenza della decisione non è garantita né dall'obbligo di motivazione, né dalla possibilità di impugnazione ».</p>
<p><br />10.2. Casistica</p>
<p>Secondo la giurisprudenza sussiste la giurisdizione del giudice penale italiano in relazione alla domanda di risarcimento dei danni proposta nei confronti di uno Stato estero, quale responsabile civile, per i crimini di guerra (da individuare in quei comportamenti posti in essere nell'ambito di un conflitto armato, i quali, pur risultando privi dei connotati di estensione e sistematicità propri dei crimini contro l'umanità, si caratterizzino comunque per la lesione dei valori universali di rispetto della dignità umana che trascendono gli interessi delle singole comunità statali impegnate nel contesto bellico) commessi da appartenenti alle sue forze armate (Cass. I, n. 43696/2015, che ha richiamato, in motivazione, la sentenza della Corte cost. n. 238/1994, per la quale il principio di immunità degli Stati per gli atti compiuti iure imperii soccombe rispetto al diritto di agire in giudizio a tutela di diritti inviola
Download .txt
gitextract_l7_nm_6w/

├── .github/
│   ├── FUNDING.yml
│   ├── dependabot.yml
│   └── workflows/
│       ├── ci.yaml
│       ├── nuget-publish.yaml
│       └── on-push-do-docs.yml
├── .gitignore
├── LICENSE
├── README.md
├── dotnet
├── mdsnippets.json
└── src/
    ├── .editorconfig
    ├── ReverseMarkdown/
    │   ├── Cleaner.cs
    │   ├── Config.cs
    │   ├── Converter.cs
    │   ├── ConverterContext.cs
    │   ├── Converters/
    │   │   ├── A.cs
    │   │   ├── AliasConverter.cs
    │   │   ├── AliasTagConverter.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
    │   │   └── UnknownTagReplacer.cs
    │   ├── Helpers/
    │   │   ├── LineSplitEnumerator.cs
    │   │   ├── StringExtensions.cs
    │   │   ├── StringReplaceValues.cs
    │   │   └── StringUtils.cs
    │   ├── ImageUtils.cs
    │   ├── ReverseMarkdown.csproj
    │   ├── UnknownTagException.cs
    │   └── UnsupportedTagExtension.cs
    ├── ReverseMarkdown.Benchmark/
    │   ├── Benchmark.md
    │   ├── CompareBenchmark.cs
    │   ├── FileHelper.cs
    │   ├── Files/
    │   │   ├── 1000-paragraphs.html
    │   │   ├── 10k-paragraphs.html
    │   │   └── huge.html
    │   ├── Program.cs
    │   └── ReverseMarkdown.Benchmark.csproj
    ├── ReverseMarkdown.Test/
    │   ├── ChildConverterTests.cs
    │   ├── Children/
    │   │   └── IgnoreAWhenHasClass.cs
    │   ├── CommonMarkSpecTests.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.Bug400_MissingSpanSpaceWithItalics.verified.md
    │   ├── ConverterTests.Bug403_unexpectedBehaviourWhenTableBodyRowsWithTHCells.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.EscapeMarkdownCharsInTextProperly.verified.md
    │   ├── 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.WhenTableRowWithDuplicateStyleKeysAfterTrimming_ThenConvertWithoutException.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_WithCaptionAndGithubFlavored_ThenCaptionAppearsAboveTable.verified.md
    │   ├── ConverterTests.WhenTable_WithCaptionAndNoHeaderRow_EmptyRowHandling_ThenCaptionAppearsAboveTable.verified.md
    │   ├── ConverterTests.WhenTable_WithCaptionContainingMarkdownChars_ThenHandleProperly.verified.md
    │   ├── ConverterTests.WhenTable_WithCaptionContainingNestedTags_ThenExtractTextOnly.verified.md
    │   ├── ConverterTests.WhenTable_WithCaptionContainingNewlines_ThenHandleNewlines.verified.md
    │   ├── ConverterTests.WhenTable_WithCaptionContainingWhitespace_ThenTrimWhitespace.verified.md
    │   ├── ConverterTests.WhenTable_WithCaption_ThenCaptionAppearsAboveTable.verified.md
    │   ├── ConverterTests.WhenTable_WithColSpan_TableHeaderColumnSpansHandling_ThenConvertToGFMTable.verified.md
    │   ├── ConverterTests.WhenTable_WithEmptyCaption_ThenConvertNormally.verified.md
    │   ├── ConverterTests.WhenTable_WithoutCaption_ThenConvertNormally.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.WhenThereAreSemanticContainerTags.verified.md
    │   ├── ConverterTests.WhenThereAreStrongTag_ThenConvertToMarkdownDoubleAsterisks.verified.md
    │   ├── ConverterTests.WhenThereHtmlWithHrefAndNoSchema_NotWhitelisted_ThenConvertToPlain.verified.md
    │   ├── ConverterTests.WhenThereHtmlWithHrefAndNoSchema_WhitelistedEmptyString_ThenConvertToMarkdown.verified.md
    │   ├── ConverterTests.WhenThereIsAsideTag.verified.md
    │   ├── ConverterTests.WhenThereIsBase64ImgTag_WithDefaultConfig_ThenIncludeInMarkdown.verified.md
    │   ├── ConverterTests.WhenThereIsBase64ImgTag_WithSaveToFileConfigButNoDirectory_ThenSkipImage.verified.md
    │   ├── ConverterTests.WhenThereIsBase64ImgTag_WithSkipConfig_ThenSkipImage.verified.md
    │   ├── ConverterTests.WhenThereIsBase64JpegImgTag_WithSkipConfig_ThenSkipImage.verified.md
    │   ├── ConverterTests.WhenThereIsBlockquoteTag_ThenConvertToMarkdownBlockquote.verified.md
    │   ├── ConverterTests.WhenThereIsBreakTag_ThenConvertToMarkdownDoubleSpacesCarriageReturn.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.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.WhenThereIsInputListWithGithubFlavoredDisabled_ThenConvertToTypicalMarkdownList.verified.md
    │   ├── ConverterTests.WhenThereIsInputListWithGithubFlavoredEnabled_ThenConvertToMarkdownCheckList.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.WhenUnclosedScriptTag_WithBypassUnknownTags_ThenConvertToMarkdown.verified.md
    │   ├── ConverterTests.WhenUnclosedStyleTag_WithBypassUnknownTags_ThenConvertToMarkdown.verified.md
    │   ├── ConverterTests.When_AlternatingEmptyAndFilledNestedParagraphs_ThenConvertCorrectly.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_ComplexNestedTableIsInTable_LeaveNestedTableAsHtml.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_DeeplyNestedParagraphs_WithMalformedHTML_ThenConvertWithoutHanging.verified.md
    │   ├── ConverterTests.When_DescriptionListTag_ThenConvertToMarkdown_List.verified.md
    │   ├── ConverterTests.When_EmptyNestedParagraphs_ThenConvertCorrectly.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_InterleavedParagraphsAndSpans_ThenConvertCorrectly.verified.md
    │   ├── ConverterTests.When_ManySequentialUnclosedParagraphs_ThenConvertCorrectly.verified.md
    │   ├── ConverterTests.When_MultipleNestedTablesInTable_LeaveAllNestedTablesAsHtml.verified.md
    │   ├── ConverterTests.When_NestedParagraphs_FiveLevelsDeep_ThenConvertCorrectly.verified.md
    │   ├── ConverterTests.When_NestedParagraphs_TenLevelsDeep_ThenConvertCorrectly.verified.md
    │   ├── ConverterTests.When_NestedSpans_FiveLevelsDeep_ThenConvertCorrectly.verified.md
    │   ├── ConverterTests.When_NestedTableIsInTable_LeaveNestedTableAsHtml.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.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_TextContainsBacktickInlineCode_DecodeAngleEntities.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_UnclosedParagraphsWithSpansAndTextNodes_ThenConvertCorrectly.verified.md
    │   ├── ConverterTests.When_Underline_Tag_With_AliasConverter_Register_ThenConvertToItalics.verified.md
    │   ├── ConverterTests.When_Underline_Tag_With_TagAlias_ThenConvertToItalics.verified.md
    │   ├── ConverterTests.When_Underline_Tag_With_UnknownTagsReplacer_ThenConvertToItalics.verified.md
    │   ├── ConverterTests.When_UnorderedListIsInTable_LeaveListAsHtml.verified.md
    │   ├── ConverterTests.cs
    │   ├── ReverseMarkdown.Test.csproj
    │   ├── Snippets.Usage.verified.txt
    │   ├── Snippets.cs
    │   └── TestData/
    │       ├── README.md
    │       ├── cases.json
    │       └── commonmark.json
    └── ReverseMarkdown.sln
Download .txt
SYMBOL INDEX (312 symbols across 51 files)

FILE: src/ReverseMarkdown.Benchmark/CompareBenchmark.cs
  class CompareBenchmark (line 7) | [SimpleJob(RuntimeMoniker.Net90)]
    method Setup (line 14) | [GlobalSetup]
    method ReverseMarkdown (line 22) | [Benchmark]

FILE: src/ReverseMarkdown.Benchmark/FileHelper.cs
  class FileHelper (line 2) | internal sealed class FileHelper {
    method ReadFile (line 3) | public static string ReadFile(string path)

FILE: src/ReverseMarkdown.Test/ChildConverterTests.cs
  class ChildConverterTests (line 10) | public class ChildConverterTests {
    method WhenConverter_A_IsReplacedByConverter_IgnoreAWhenHasClass (line 11) | [Fact]

FILE: src/ReverseMarkdown.Test/Children/IgnoreAWhenHasClass.cs
  class IgnoreAWhenHasClass (line 7) | internal class IgnoreAWhenHasClass(Converter converter) : A(converter) {
    method Convert (line 10) | public override void Convert(TextWriter writer, HtmlNode node)

FILE: src/ReverseMarkdown.Test/CommonMarkSpecTests.cs
  class CommonMarkSpecTests (line 12) | public class CommonMarkSpecTests
    method CommonMarkSpecTests (line 16) | public CommonMarkSpecTests(ITestOutputHelper output)
    method CommonMark_Spec_Examples_RoundTripHtml (line 21) | [Fact]
    method NormalizeHtml (line 76) | private static string NormalizeHtml(string html)
    method FormatFailure (line 99) | private static string FormatFailure(CommonMarkExample example, string ...
    method GetIntEnvironmentVariable (line 107) | private static int GetIntEnvironmentVariable(string name)
    method GetSpecPath (line 113) | private static string GetSpecPath()
    class CommonMarkExample (line 127) | private sealed class CommonMarkExample

FILE: src/ReverseMarkdown.Test/ConverterTests.cs
  class ConverterTests (line 14) | public class ConverterTests
    method ConverterTests (line 19) | public ConverterTests(ITestOutputHelper testOutputHelper)
    method LoadHtml (line 36) | private static string LoadHtml(string id)
    method GetProjectDirectory (line 45) | private static DirectoryInfo GetProjectDirectory()
    class CaseHtml (line 59) | private sealed class CaseHtml
    method WhenThereIsHtmlWithHttpSchemeAndNameWithoutScheme_SmartHandling_ThenConvertToPlain (line 91) | [Fact]
    method WhenEscapeMarkdownLineStartsEnabled_ThenEscapeHeadingAndListMarkers (line 106) | [Fact]
    method WhenTextContainsBracketsBracesAndParentheses_ThenDoNotEscapeThem (line 120) | [Fact]
    method WhenCommonMarkTextContainsMarkdownLinkPattern_ThenEscapeOnlyPatternDelimiters (line 132) | [Fact]
    method WhenOutputLineEndingConfigured_ThenNormalizeOutputLineEndings (line 145) | [Fact]
    method WhenThereIsHtmlLinkWithHttpSchemaAndNameWithout_SmartHandling_ThenOutputOnlyHref (line 165) | [Fact]
    method WhenThereIsHtmlNonWellFormedLinkLink_SmartHandling_ThenConvertToMarkdown (line 179) | [Fact]
    method WhenThereIsBase64PngImgTag_WithSaveToFileConfigAndValidDirectory_ThenSaveImageAndReferenceFilePath (line 284) | [Fact]
    method WhenThereIsBase64JpegImgTag_WithSaveToFileConfigAndValidDirectory_ThenSaveImageWithJpgExtension (line 321) | [Fact]
    method WhenMultipleBase64ImgTags_WithSaveToFileConfig_ThenSaveAllImagesWithUniqueNames (line 356) | [Fact]
    method WhenBase64ImgTag_WithCustomFileNameGenerator_ThenUseCustomFileName (line 389) | [Fact]
    method WhenBase64ImgTag_WithSaveToFileAndNonExistentDirectory_ThenCreateDirectoryAndSaveImage (line 420) | [Fact]
    method When_Underline_Tag_With_AliasConverter_Register_ThenConvertToItalics (line 495) | [Fact]
    method Check_Converter_With_Unknown_Tag_Raise_Option (line 515) | [Fact]
    method TestConversionWithPastedHtmlContainingUnicodeSpaces (line 564) | [Fact]
    method Converter_Is_Thread_Safe_For_Concurrent_Use (line 586) | [Fact]
    method WhenPreContainsTable_ThenTreatAsCodeBlock (line 607) | [Fact]
    method WhenPreContainsHtml_WithConvertPreContentAsHtml_ThenConvertHtml (line 624) | [Fact]
    method SlackFlavored_Unsupported_Hr (line 641) | [Fact]
    method TelegramMarkdownV2_BasicFormatting (line 650) | [Fact]
    method TelegramMarkdownV2_EscapeSpecialCharactersInText (line 661) | [Fact]
    method TelegramMarkdownV2_EscapeLinkTextAndHref (line 672) | [Fact]
    method TelegramMarkdownV2_EscapesListMarkers (line 683) | [Fact]
    method TelegramMarkdownV2_Img_FallsBackToLink (line 695) | [Fact]
    method TelegramMarkdownV2_Sup_FallsBackToCaretNotation (line 705) | [Fact]
    method TelegramMarkdownV2_Table_FallsBackToCodeBlock (line 715) | [Fact]
    method SlackFlavored_Unsupported_Img (line 726) | [Fact]
    method SlackFlavored_Unsupported_Sup (line 735) | [Fact]
    method SlackFlavored_Unsupported_Table (line 744) | [Fact]
    method SlackFlavored_Unsupported_Table_Td (line 753) | [Fact]
    method SlackFlavored_Unsupported_Table_Tr (line 762) | [Fact]
  class DataDrivenCasesTests (line 801) | public class DataDrivenCasesTests
    method CaseRuns (line 806) | [Theory]
    method LoadCases (line 817) | private static IEnumerable<CaseData> LoadCases()
    method ApplyTagFilter (line 834) | private static IEnumerable<CaseData> ApplyTagFilter(IEnumerable<CaseDa...
    method LoadExpected (line 845) | private static string LoadExpected(CaseData testCase)
    method BuildConfig (line 873) | private static Config BuildConfig(CaseData testCase)
    method ApplyBool (line 946) | private static void ApplyBool(bool? value, Action<bool> apply)
    method ApplyEnum (line 953) | private static void ApplyEnum<T>(string value, Action<T> apply) where ...
    method GetProjectDirectory (line 964) | private static DirectoryInfo GetProjectDirectory()
    method GetCasesPath (line 978) | private static string GetCasesPath()
    class CaseData (line 983) | public class CaseData
      method ToString (line 994) | public override string ToString()
    class CaseConfig (line 1000) | public class CaseConfig
    class ConfigPresets (line 1025) | private static class ConfigPresets
      method Get (line 1027) | public static Config Get(string name)

FILE: src/ReverseMarkdown.Test/Snippets.cs
  class Snippets (line 7) | public class Snippets
    method Usage (line 9) | [Fact]
    method UsageWithConfig (line 25) | [Fact]
    method Base64ImageInclude (line 47) | [Fact]
    method Base64ImageSkip (line 60) | [Fact]
    method Base64ImageSaveToFile (line 77) | [Fact]
    method Base64ImageCustomFilename (line 96) | [Fact]

FILE: src/ReverseMarkdown/Cleaner.cs
  class Cleaner (line 8) | public static partial class Cleaner {
    method SlackBoldCleaner (line 9) | [GeneratedRegex(@"\*(\s\*)+")]
    method SlackItalicCleaner (line 12) | [GeneratedRegex(@"_(\s_)+")]
    method NonBreakingSpaces (line 15) | [GeneratedRegex(@"[\u0020\u00A0]")]
    method CleanTagBorders (line 23) | private static string CleanTagBorders(string content)
    method NormalizeSpaceChars (line 29) | private static string NormalizeSpaceChars(string content)
    method PreTidy (line 36) | public static string PreTidy(string content, bool removeComments)
    method FixUnclosedTag (line 46) | private static string FixUnclosedTag(string content, string tagName)
    method SlackTidy (line 62) | public static string SlackTidy(string content)

FILE: src/ReverseMarkdown/Config.cs
  class Config (line 6) | public class Config
    type UnknownTagsOption (line 108) | public enum UnknownTagsOption
    type TableWithoutHeaderRowHandlingOption (line 131) | public enum TableWithoutHeaderRowHandlingOption
    type Base64ImageHandling (line 144) | public enum Base64ImageHandling
    method IsSchemeWhitelisted (line 191) | internal bool IsSchemeWhitelisted(string scheme)

FILE: src/ReverseMarkdown/Converter.cs
  class Converter (line 17) | public class Converter {
    method Converter (line 29) | public Converter() : this(new Config())
    method Converter (line 33) | public Converter(Config config) : this(config, null)
    method Converter (line 37) | public Converter(Config config, params Assembly[]? additionalAssemblies)
    method Convert (line 95) | public virtual string Convert(string html)
    method ApplyOutputLineEndings (line 163) | private string ApplyOutputLineEndings(string content)
    method LooksLikeCommonMarkHtmlBlock (line 171) | private static bool LooksLikeCommonMarkHtmlBlock(string html)
    method Register (line 192) | public virtual void Register(string tagName, IConverter converter)
    method MesureCapacity (line 197) | internal int MesureCapacity(HtmlNode node)
    method CreateWriter (line 209) | internal StringWriter CreateWriter(HtmlNode node)
    method ConvertNode (line 219) | public virtual string ConvertNode(HtmlNode node)
    method ConvertNode (line 227) | public virtual void ConvertNode(TextWriter writer, HtmlNode node)
    method Lookup (line 236) | public virtual IConverter Lookup(string tagName)
    method GetUnknownTagReplacer (line 259) | private IConverter GetUnknownTagReplacer(string tagName, string replac...
    method GetAliasConverter (line 271) | private IConverter GetAliasConverter(string tagName, string targetTag)
    method ResolveAliasTarget (line 283) | private string? ResolveAliasTarget(string tagName)
    method GetDefaultConverter (line 308) | private IConverter GetDefaultConverter(string tagName)
    method EnsureContext (line 318) | private IDisposable EnsureContext()
    class ContextScope (line 328) | private sealed class ContextScope : IDisposable {
      method ContextScope (line 331) | public ContextScope(System.Threading.AsyncLocal<ConverterContext?> s...
      method Dispose (line 336) | public void Dispose()
    class NoopDisposable (line 342) | private sealed class NoopDisposable : IDisposable {
      method NoopDisposable (line 345) | private NoopDisposable()
      method Dispose (line 349) | public void Dispose()

FILE: src/ReverseMarkdown/ConverterContext.cs
  class ConverterContext (line 8) | public class ConverterContext {
    method Enter (line 14) | public void Enter(HtmlNode node)
    method Leave (line 30) | public void Leave(HtmlNode node)
    method AncestorsAny (line 46) | public bool AncestorsAny(string nodeName)
    method AncestorsCount (line 54) | public int AncestorsCount(string nodeName)

FILE: src/ReverseMarkdown/Converters/A.cs
  class A (line 9) | public class A : ConverterBase {
    method A (line 10) | public A(Converter converter) : base(converter)
    method Convert (line 21) | public override void Convert(TextWriter writer, HtmlNode node)
    method ConvertTelegramMarkdownV2 (line 209) | private void ConvertTelegramMarkdownV2(TextWriter writer, HtmlNode nod...
    method WriteRawHtmlAnchor (line 248) | private static void WriteRawHtmlAnchor(TextWriter writer, HtmlNode nod...
    method EncodeAnchorText (line 264) | private static string EncodeAnchorText(string text)

FILE: src/ReverseMarkdown/Converters/AliasConverter.cs
  class AliasConverter (line 5) | public sealed class AliasConverter : ConverterBase {
    method AliasConverter (line 8) | public AliasConverter(Converter converter, string targetTag) : base(co...
    method Convert (line 13) | public override void Convert(TextWriter writer, HtmlNode node)

FILE: src/ReverseMarkdown/Converters/AliasTagConverter.cs
  class AliasTagConverter (line 5) | internal sealed class AliasTagConverter : ConverterBase {
    method AliasTagConverter (line 9) | public AliasTagConverter(Converter converter, string sourceTag, string...
    method Convert (line 17) | public override void Convert(TextWriter writer, HtmlNode node)

FILE: src/ReverseMarkdown/Converters/Aside.cs
  class Aside (line 6) | public class Aside : ConverterBase {
    method Aside (line 7) | public Aside(Converter converter)
    method Convert (line 13) | public override void Convert(TextWriter writer, HtmlNode node)

FILE: src/ReverseMarkdown/Converters/Blockquote.cs
  class Blockquote (line 7) | public class Blockquote : ConverterBase {
    method Blockquote (line 8) | public Blockquote(Converter converter) : base(converter)
    method Convert (line 13) | public override void Convert(TextWriter writer, HtmlNode node)

FILE: src/ReverseMarkdown/Converters/Br.cs
  class Br (line 6) | public class Br : ConverterBase {
    method Br (line 7) | public Br(Converter converter) : base(converter)
    method Convert (line 12) | public override void Convert(TextWriter writer, HtmlNode node)

FILE: src/ReverseMarkdown/Converters/ByPass.cs
  class ByPass (line 6) | public class ByPass : ConverterBase {
    method ByPass (line 7) | public ByPass(Converter converter) : base(converter)
    method Convert (line 17) | public override void Convert(TextWriter writer, HtmlNode node)

FILE: src/ReverseMarkdown/Converters/Code.cs
  class Code (line 8) | public class Code : ConverterBase {
    method Code (line 9) | public Code(Converter converter) : base(converter)
    method Convert (line 14) | public override void Convert(TextWriter writer, HtmlNode node)
    method CreateCommonMarkCodeFence (line 89) | private static string CreateCommonMarkCodeFence(string content)

FILE: src/ReverseMarkdown/Converters/ConverterBase.cs
  class ConverterBase (line 7) | public abstract class ConverterBase(Converter converter) : IConverter {
    method TreatChildren (line 12) | protected void TreatChildren(TextWriter writer, HtmlNode node)
    method TreatChildrenAsString (line 21) | protected string TreatChildrenAsString(HtmlNode node)
    method ExtractTitle (line 36) | protected static string ExtractTitle(HtmlNode node)
    method DecodeHtml (line 42) | protected static string DecodeHtml(string html)
    method DecodeHtml (line 47) | protected static void DecodeHtml(TextWriter writer, string html)
    method IndentationFor (line 53) | protected string IndentationFor(HtmlNode node, bool zeroIndex = false)
    method TreatEmphasizeContentWhitespaceGuard (line 71) | public static void TreatEmphasizeContentWhitespaceGuard(
    method Convert (line 112) | public abstract void Convert(TextWriter writer, HtmlNode node);

FILE: src/ReverseMarkdown/Converters/Dd.cs
  class Dd (line 6) | public class Dd : ConverterBase {
    method Dd (line 7) | public Dd(Converter converter) : base(converter)
    method Convert (line 12) | public override void Convert(TextWriter writer, HtmlNode node)

FILE: src/ReverseMarkdown/Converters/Div.cs
  class Div (line 6) | public class Div : ConverterBase {
    method Div (line 7) | public Div(Converter converter) : base(converter)
    method Convert (line 20) | public override void Convert(TextWriter writer, HtmlNode node)

FILE: src/ReverseMarkdown/Converters/Dl.cs
  class Dl (line 6) | public class Dl : ConverterBase {
    method Dl (line 7) | public Dl(Converter converter) : base(converter)
    method Convert (line 12) | public override void Convert(TextWriter writer, HtmlNode node)

FILE: src/ReverseMarkdown/Converters/Drop.cs
  class Drop (line 6) | public class Drop : ConverterBase {
    method Drop (line 7) | public Drop(Converter converter) : base(converter)
    method Convert (line 16) | public override void Convert(TextWriter writer, HtmlNode node)

FILE: src/ReverseMarkdown/Converters/Dt.cs
  class Dt (line 6) | public class Dt : ConverterBase {
    method Dt (line 7) | public Dt(Converter converter) : base(converter)
    method Convert (line 12) | public override void Convert(TextWriter writer, HtmlNode node)

FILE: src/ReverseMarkdown/Converters/Em.cs
  class Em (line 6) | public class Em : ConverterBase {
    method Em (line 7) | public Em(Converter converter) : base(converter)
    method Convert (line 13) | public override void Convert(TextWriter writer, HtmlNode node)
    method AlreadyItalic (line 93) | private bool AlreadyItalic()
    method IsWordChar (line 98) | private static bool IsWordChar(char? value)
    method FirstNonWhitespaceChar (line 103) | private static char? FirstNonWhitespaceChar(string content)
    method LastNonWhitespaceChar (line 114) | private static char? LastNonWhitespaceChar(string content)
    method IsAdjacentWordChar (line 126) | private static bool IsAdjacentWordChar(HtmlNode? sibling, bool checkEnd)

FILE: src/ReverseMarkdown/Converters/H.cs
  class H (line 6) | public class H : ConverterBase {
    method H (line 7) | public H(Converter converter) : base(converter)
    method Convert (line 17) | public override void Convert(TextWriter writer, HtmlNode node)
    method EscapeTrailingHashes (line 52) | private static string EscapeTrailingHashes(string content)

FILE: src/ReverseMarkdown/Converters/Hr.cs
  class Hr (line 6) | public class Hr : ConverterBase {
    method Hr (line 7) | public Hr(Converter converter) : base(converter)
    method Convert (line 12) | public override void Convert(TextWriter writer, HtmlNode node)

FILE: src/ReverseMarkdown/Converters/IConverter.cs
  type IConverter (line 6) | public interface IConverter {
    method Convert (line 7) | void Convert(TextWriter writer, HtmlNode node);

FILE: src/ReverseMarkdown/Converters/Ignore.cs
  class Ignore (line 6) | public class Ignore : ConverterBase {
    method Ignore (line 7) | public Ignore(Converter converter) : base(converter)
    method Convert (line 13) | public override void Convert(TextWriter writer, HtmlNode node)

FILE: src/ReverseMarkdown/Converters/Img.cs
  class Img (line 7) | public class Img : ConverterBase {
    method Img (line 10) | public Img(Converter converter) : base(converter)
    method Convert (line 15) | public override void Convert(TextWriter writer, HtmlNode node)
    method WriteTelegramFallback (line 86) | private void WriteTelegramFallback(TextWriter writer, string alt, stri...
    method SaveBase64ImageToFile (line 110) | private string SaveBase64ImageToFile(string base64Src)

FILE: src/ReverseMarkdown/Converters/Li.cs
  class Li (line 9) | public class Li : ConverterBase {
    method Li (line 10) | public Li(Converter converter) : base(converter)
    method Convert (line 15) | public override void Convert(TextWriter writer, HtmlNode node)
    method ContentFor (line 87) | private string ContentFor(HtmlNode node)

FILE: src/ReverseMarkdown/Converters/Ol.cs
  class Ol (line 7) | public class Ol : ConverterBase {
    method Ol (line 8) | public Ol(Converter converter) : base(converter)
    method Convert (line 14) | public override void Convert(TextWriter writer, HtmlNode node)
    method NextElementIsList (line 40) | private static bool NextElementIsList(HtmlNode node)

FILE: src/ReverseMarkdown/Converters/P.cs
  class P (line 6) | public class P : ConverterBase {
    method P (line 7) | public P(Converter converter) : base(converter)
    method Convert (line 12) | public override void Convert(TextWriter writer, HtmlNode node)
    method TreatIndentation (line 28) | private void TreatIndentation(TextWriter writer, HtmlNode node)

FILE: src/ReverseMarkdown/Converters/PassThrough.cs
  class PassThrough (line 6) | public class PassThrough : ConverterBase {
    method PassThrough (line 7) | public PassThrough(Converter converter) : base(converter)
    method Convert (line 11) | public override void Convert(TextWriter writer, HtmlNode node)

FILE: src/ReverseMarkdown/Converters/Pre.cs
  class Pre (line 9) | public partial class Pre : ConverterBase {
    method Pre (line 10) | public Pre(Converter converter) : base(converter)
    method Convert (line 15) | public override void Convert(TextWriter writer, HtmlNode node)
    method ConvertHtmlContent (line 76) | private void ConvertHtmlContent(TextWriter writer, HtmlNode node)
    method GetLanguage (line 91) | private string? GetLanguage(HtmlNode node)
    method GetLanguageFromHighlightClassAttribute (line 105) | private static string GetLanguageFromHighlightClassAttribute(HtmlNode ...
    method ClassRegex (line 132) | [GeneratedRegex(@"(highlight-source-|language-|highlight-|brush:\s)([^...
    method ClassMatch (line 141) | private static Match ClassMatch(HtmlNode node)
    method CreateCommonMarkFence (line 152) | private static string CreateCommonMarkFence(string content)

FILE: src/ReverseMarkdown/Converters/S.cs
  class S (line 6) | public class S : ConverterBase {
    method S (line 7) | public S(Converter converter) : base(converter)
    method Convert (line 14) | public override void Convert(TextWriter writer, HtmlNode node)
    method AlreadyStrikethrough (line 34) | private bool AlreadyStrikethrough()

FILE: src/ReverseMarkdown/Converters/Strong.cs
  class Strong (line 6) | public class Strong : ConverterBase {
    method Strong (line 7) | public Strong(Converter converter) : base(converter)
    method Convert (line 13) | public override void Convert(TextWriter writer, HtmlNode node)
    method AlreadyBold (line 72) | private bool AlreadyBold()
    method IsWordChar (line 77) | private static bool IsWordChar(char? value)
    method FirstNonWhitespaceChar (line 82) | private static char? FirstNonWhitespaceChar(string content)
    method LastNonWhitespaceChar (line 93) | private static char? LastNonWhitespaceChar(string content)
    method IsAdjacentWordChar (line 105) | private static bool IsAdjacentWordChar(HtmlNode? sibling, bool checkEnd)

FILE: src/ReverseMarkdown/Converters/Sup.cs
  class Sup (line 7) | public class Sup : ConverterBase {
    method Sup (line 8) | public Sup(Converter converter) : base(converter)
    method Convert (line 13) | public override void Convert(TextWriter writer, HtmlNode node)
    method AlreadySup (line 41) | private bool AlreadySup()

FILE: src/ReverseMarkdown/Converters/Table.cs
  class Table (line 10) | public class Table : ConverterBase {
    method Table (line 11) | public Table(Converter converter) : base(converter)
    method Convert (line 16) | public override void Convert(TextWriter writer, HtmlNode node)
    method WriteTelegramFallback (line 68) | private static void WriteTelegramFallback(TextWriter writer, HtmlNode ...
    method NormalizeWhitespace (line 118) | private static string NormalizeWhitespace(string value)
    method HasNoTableHeaderRow (line 124) | private static bool HasNoTableHeaderRow(HtmlNode node)
    method EmptyHeader (line 130) | private static string EmptyHeader(HtmlNode node)

FILE: src/ReverseMarkdown/Converters/Td.cs
  class Td (line 6) | public class Td : ConverterBase {
    method Td (line 7) | public Td(Converter converter) : base(converter)
    method Convert (line 13) | public override void Convert(TextWriter writer, HtmlNode node)
    method FirstNodeWithinCell (line 41) | public static bool FirstNodeWithinCell(HtmlNode node)
    method LastNodeWithinCell (line 59) | public static bool LastNodeWithinCell(HtmlNode node)
    method GetColSpan (line 72) | private int GetColSpan(HtmlNode node)

FILE: src/ReverseMarkdown/Converters/Text.cs
  class Text (line 11) | public partial class Text : ConverterBase {
    method Text (line 12) | public Text(Converter converter) : base(converter)
    method BackTicks (line 40) | [GeneratedRegex(@"`.*?`")]
    method CommonMarkInlineLinkOrImagePattern (line 43) | [GeneratedRegex(@"!?\[[^\]\r\n]*\]\([^\)\r\n]*\)")]
    method CommonMarkReferenceLinkPattern (line 46) | [GeneratedRegex(@"\[[^\]\r\n]+\]\[[^\]\r\n]*\]")]
    method CommonMarkLinkDefinitionPattern (line 49) | [GeneratedRegex(@"(?m)^ {0,3}\[[^\]\r\n]+\]:")]
    method Convert (line 55) | public override void Convert(TextWriter writer, HtmlNode node)
    method TreatText (line 88) | private void TreatText(TextWriter writer, HtmlNode node)
    method EscapeSpecialCommonMarkCharacters (line 198) | private static string EscapeSpecialCommonMarkCharacters(string content...
    method EscapeSpecialCommonMarkCharacters (line 206) | private static string EscapeSpecialCommonMarkCharacters(string content)
    method TryGetMarkedDelimiterSequence (line 213) | private static bool TryGetMarkedDelimiterSequence(HtmlNode node, out b...
    method EscapeMarkedDelimiters (line 275) | private static string EscapeMarkedDelimiters(string content, bool[] de...
    method EscapeCommonMarkPatternDelimiters (line 302) | private static string EscapeCommonMarkPatternDelimiters(string content)
    method MarkCommonMarkPatternDelimiters (line 330) | private static bool MarkCommonMarkPatternDelimiters(bool[] shouldEscap...
    method IsCommonMarkDelimiter (line 347) | private static bool IsCommonMarkDelimiter(char character)
    method PreserveCommonMarkAmpersands (line 352) | private static string PreserveCommonMarkAmpersands(string rawContent)
    method RestoreCommonMarkAmpersands (line 363) | private static string RestoreCommonMarkAmpersands(string content)
    method EscapeCommonMarkBackslashes (line 379) | private static string EscapeCommonMarkBackslashes(string content)
    method EscapeMarkdownLineStarts (line 388) | private static string EscapeMarkdownLineStarts(string content)
    method EscapeLineStart (line 403) | private static string EscapeLineStart(string line)
    method IsSetextUnderline (line 448) | private static bool IsSetextUnderline(string line, int index)
    method IsLineMarker (line 469) | private static bool IsLineMarker(string line, int markerIndex, int min...

FILE: src/ReverseMarkdown/Converters/Tr.cs
  class Tr (line 8) | public class Tr : ConverterBase {
    method Tr (line 9) | public Tr(Converter converter) : base(converter)
    method Convert (line 14) | public override void Convert(TextWriter writer, HtmlNode node)
    method ShouldWriteUnderline (line 46) | private bool ShouldWriteUnderline(HtmlNode node)
    method WriteUnderline (line 79) | private static void WriteUnderline(TextWriter writer, HtmlNode node, b...
    method GetColSpan (line 106) | private static int GetColSpan(HtmlNode node, bool tableHeaderColumnSpa...

FILE: src/ReverseMarkdown/Converters/UnknownTagReplacer.cs
  class UnknownTagReplacer (line 5) | internal sealed class UnknownTagReplacer : ConverterBase {
    method UnknownTagReplacer (line 9) | public UnknownTagReplacer(Converter converter, string tagName, string ...
    method Convert (line 17) | public override void Convert(TextWriter writer, HtmlNode node)

FILE: src/ReverseMarkdown/Helpers/LineSplitEnumerator.cs
  type LineSplitEnumerator (line 9) | internal ref struct LineSplitEnumerator(string text) {
    method GetEnumerator (line 13) | public LineSplitEnumerator GetEnumerator() => this;
    method MoveNext (line 16) | public bool MoveNext()
    method ReadNextLine (line 27) | private ReadOnlySpan<char> ReadNextLine(int pos)

FILE: src/ReverseMarkdown/Helpers/StringExtensions.cs
  class StringExtensions (line 7) | public static class StringExtensions {
    method Chomp (line 8) | public static string Chomp(this string content)
    method ReadLines (line 15) | internal static LineSplitEnumerator ReadLines(this string content)
    method Replace (line 20) | internal static string Replace(this string content, StringReplaceValue...
    method FixMultipleNewlines (line 25) | public static string FixMultipleNewlines(this string markdown)
    method CompactHtmlForMarkdown (line 37) | public static string CompactHtmlForMarkdown(this string html)
    method CompactHtmlForCommonMarkBlock (line 53) | public static string CompactHtmlForCommonMarkBlock(this string html)

FILE: src/ReverseMarkdown/Helpers/StringReplaceValues.cs
  class StringReplaceValues (line 10) | internal class StringReplaceValues : Dictionary<string, string> {
    method Replace (line 15) | public string Replace(string input)

FILE: src/ReverseMarkdown/Helpers/StringUtils.cs
  class StringUtils (line 10) | public static partial class StringUtils {
    method GetScheme (line 23) | public static string GetScheme(string url)
    method LinkTextRegex (line 42) | [GeneratedRegex(@"\r?\n\s*\r?\n", RegexOptions.Singleline)]
    method EscapeLinkText (line 53) | public static string EscapeLinkText(string rawText)
    method ParseStyle (line 63) | public static Dictionary<string, string> ParseStyle(string? style)
    method EscapeTelegramMarkdownV2 (line 77) | public static string EscapeTelegramMarkdownV2(string content)
    method EscapeTelegramMarkdownV2Code (line 82) | public static string EscapeTelegramMarkdownV2Code(string content)
    method EscapeTelegramMarkdownV2LinkUrl (line 87) | public static string EscapeTelegramMarkdownV2LinkUrl(string url)
    method EscapeChars (line 92) | private static string EscapeChars(string content, Func<char, bool> mus...

FILE: src/ReverseMarkdown/ImageUtils.cs
  class ImageUtils (line 7) | public static class ImageUtils
    method SaveBase64Image (line 9) | public static string SaveBase64Image(string base64Image, string target...
    method IsValidBase64ImageData (line 38) | public static bool IsValidBase64ImageData(string data)
    method MimeTypeToExtension (line 50) | private static string MimeTypeToExtension(string mimeType)

FILE: src/ReverseMarkdown/UnknownTagException.cs
  class UnknownTagException (line 6) | public class UnknownTagException(string tagName) : Exception($"Unknown t...

FILE: src/ReverseMarkdown/UnsupportedTagExtension.cs
  class UnsupportedTagException (line 6) | public class UnsupportedTagException : Exception {
    method UnsupportedTagException (line 7) | internal UnsupportedTagException(string message) : base(message)
  class SlackUnsupportedTagException (line 12) | public class SlackUnsupportedTagException : UnsupportedTagException {
    method SlackUnsupportedTagException (line 13) | internal SlackUnsupportedTagException(string tagName)
  class TelegramUnsupportedTagException (line 19) | public class TelegramUnsupportedTagException : UnsupportedTagException {
    method TelegramUnsupportedTagException (line 20) | internal TelegramUnsupportedTagException(string tagName)
Condensed preview — 246 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (4,857K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 147,
    "preview": "# These are supported funding model platforms\ngithub: mysticmind\n# ko_fi: 'babuannamalai'\n# custom: ['https://www.buymea"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 108,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: \"nuget\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "chars": 862,
    "preview": "name: Build\non:\n  push:\n    branches:\n      - master\n  pull_request:\n    branches:\n      - master\n\nenv:\n  DOTNET_CLI_TEL"
  },
  {
    "path": ".github/workflows/nuget-publish.yaml",
    "chars": 873,
    "preview": "name: NuGet Manual Publish\n\non: [workflow_dispatch]\n\nenv:\n  config: Release\n  DOTNET_CLI_TELEMETRY_OPTOUT: 1\n  DOTNET_SK"
  },
  {
    "path": ".github/workflows/on-push-do-docs.yml",
    "chars": 745,
    "preview": "name: on-push-do-docs\non:\n  push:\njobs:\n  docs:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v2\n  "
  },
  {
    "path": ".gitignore",
    "chars": 127,
    "preview": ".vs\nobj\nbin\nbin/*\ndeploy\ndeploy/*\n_ReSharper.*\n*.user\n*.suo\n*.cache\n*.Cache\nThumbs.db\n**/packages\n.idea\n*.received.*\n.DS"
  },
  {
    "path": "LICENSE",
    "chars": 1082,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2015 Babu Annamalai\n\nPermission is hereby granted, free of charge, to any person ob"
  },
  {
    "path": "README.md",
    "chars": 16446,
    "preview": "# Meet ReverseMarkdown\n\n[![Build status](https://github.com/mysticmind/reversemarkdown-net/actions/workflows/ci.yaml/bad"
  },
  {
    "path": "dotnet",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "mdsnippets.json",
    "chars": 99,
    "preview": "{\n  \"Convention\": \"InPlaceOverwrite\",\n  \"ExcludeMarkdownDirectories\": [ \"ReverseMarkdown.Test\" ]\n}"
  },
  {
    "path": "src/.editorconfig",
    "chars": 57,
    "preview": "root = true\n\n[*.cs]\nindent_style = space\nindent_size = 4\n"
  },
  {
    "path": "src/ReverseMarkdown/Cleaner.cs",
    "chars": 2249,
    "preview": "using System;\nusing System.Text.RegularExpressions;\nusing ReverseMarkdown.Helpers;\n\n\nnamespace ReverseMarkdown;\n\npublic"
  },
  {
    "path": "src/ReverseMarkdown/Config.cs",
    "chars": 8012,
    "preview": "using System;\nusing System.Collections.Generic;\n\nnamespace ReverseMarkdown\n{\n    public class Config\n    {\n        publ"
  },
  {
    "path": "src/ReverseMarkdown/Converter.cs",
    "chars": 13112,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Reflection;\nusing Syst"
  },
  {
    "path": "src/ReverseMarkdown/ConverterContext.cs",
    "chars": 1481,
    "preview": "using System;\nusing System.Collections.Generic;\nusing HtmlAgilityPack;\n\n\nnamespace ReverseMarkdown;\n\npublic class Conve"
  },
  {
    "path": "src/ReverseMarkdown/Converters/A.cs",
    "chars": 10212,
    "preview": "using System;\nusing System.IO;\nusing System.Linq;\nusing HtmlAgilityPack;\nusing ReverseMarkdown.Helpers;\n\n\nnamespace Rev"
  },
  {
    "path": "src/ReverseMarkdown/Converters/AliasConverter.cs",
    "chars": 925,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\n\nnamespace ReverseMarkdown.Converters {\n    public sealed class AliasConverter :"
  },
  {
    "path": "src/ReverseMarkdown/Converters/AliasTagConverter.cs",
    "chars": 828,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\n\nnamespace ReverseMarkdown.Converters {\n    internal sealed class AliasTagConver"
  },
  {
    "path": "src/ReverseMarkdown/Converters/Aside.cs",
    "chars": 466,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\n\n\nnamespace ReverseMarkdown.Converters {\n    public class Aside : ConverterBase "
  },
  {
    "path": "src/ReverseMarkdown/Converters/Blockquote.cs",
    "chars": 729,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\nusing ReverseMarkdown.Helpers;\n\n\nnamespace ReverseMarkdown.Converters {\n    pub"
  },
  {
    "path": "src/ReverseMarkdown/Converters/Br.cs",
    "chars": 918,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\n\n\nnamespace ReverseMarkdown.Converters {\n    public class Br : ConverterBase {\n"
  },
  {
    "path": "src/ReverseMarkdown/Converters/ByPass.cs",
    "chars": 629,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\n\n\nnamespace ReverseMarkdown.Converters {\n    public class ByPass : ConverterBas"
  },
  {
    "path": "src/ReverseMarkdown/Converters/Code.cs",
    "chars": 3966,
    "preview": "using System;\nusing System.IO;\nusing HtmlAgilityPack;\nusing ReverseMarkdown.Helpers;\n\n\nnamespace ReverseMarkdown.Convert"
  },
  {
    "path": "src/ReverseMarkdown/Converters/ConverterBase.cs",
    "chars": 3370,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\nusing ReverseMarkdown.Helpers;\n\n\nnamespace ReverseMarkdown.Converters {\n    pub"
  },
  {
    "path": "src/ReverseMarkdown/Converters/Dd.cs",
    "chars": 605,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\n\n\nnamespace ReverseMarkdown.Converters {\n    public class Dd : ConverterBase {\n "
  },
  {
    "path": "src/ReverseMarkdown/Converters/Div.cs",
    "chars": 1918,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\n\n\nnamespace ReverseMarkdown.Converters {\n    public class Div : ConverterBase {"
  },
  {
    "path": "src/ReverseMarkdown/Converters/Dl.cs",
    "chars": 445,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\n\n\nnamespace ReverseMarkdown.Converters {\n    public class Dl : ConverterBase {\n "
  },
  {
    "path": "src/ReverseMarkdown/Converters/Drop.cs",
    "chars": 589,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\n\n\nnamespace ReverseMarkdown.Converters {\n    public class Drop : ConverterBase "
  },
  {
    "path": "src/ReverseMarkdown/Converters/Dt.cs",
    "chars": 559,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\n\n\nnamespace ReverseMarkdown.Converters {\n    public class Dt : ConverterBase {\n "
  },
  {
    "path": "src/ReverseMarkdown/Converters/Em.cs",
    "chars": 5027,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\n\n\nnamespace ReverseMarkdown.Converters {\n    public class Em : ConverterBase {\n"
  },
  {
    "path": "src/ReverseMarkdown/Converters/H.cs",
    "chars": 2309,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\n\n\nnamespace ReverseMarkdown.Converters {\n    public class H : ConverterBase {\n "
  },
  {
    "path": "src/ReverseMarkdown/Converters/Hr.cs",
    "chars": 781,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\n\n\nnamespace ReverseMarkdown.Converters {\n    public class Hr : ConverterBase {\n"
  },
  {
    "path": "src/ReverseMarkdown/Converters/IConverter.cs",
    "chars": 180,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\n\n\nnamespace ReverseMarkdown.Converters {\n    public interface IConverter {\n    "
  },
  {
    "path": "src/ReverseMarkdown/Converters/Ignore.cs",
    "chars": 443,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\n\n\nnamespace ReverseMarkdown.Converters {\n    public class Ignore : ConverterBas"
  },
  {
    "path": "src/ReverseMarkdown/Converters/Img.cs",
    "chars": 5284,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\nusing ReverseMarkdown.Helpers;\n\n\nnamespace ReverseMarkdown.Converters {\n    pub"
  },
  {
    "path": "src/ReverseMarkdown/Converters/Li.cs",
    "chars": 3761,
    "preview": "using System;\nusing System.IO;\nusing System.Linq;\nusing HtmlAgilityPack;\nusing ReverseMarkdown.Helpers;\n\n\nnamespace Rev"
  },
  {
    "path": "src/ReverseMarkdown/Converters/Ol.cs",
    "chars": 1619,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\nusing ReverseMarkdown.Helpers;\n\n\nnamespace ReverseMarkdown.Converters {\n    pub"
  },
  {
    "path": "src/ReverseMarkdown/Converters/P.cs",
    "chars": 1765,
    "preview": "using System.IO;\r\nusing HtmlAgilityPack;\r\n\r\n\r\nnamespace ReverseMarkdown.Converters {\r\n    public class P : ConverterBas"
  },
  {
    "path": "src/ReverseMarkdown/Converters/PassThrough.cs",
    "chars": 357,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\n\n\nnamespace ReverseMarkdown.Converters {\n    public class PassThrough : Convert"
  },
  {
    "path": "src/ReverseMarkdown/Converters/Pre.cs",
    "chars": 5765,
    "preview": "using System;\nusing System.IO;\nusing System.Text.RegularExpressions;\nusing HtmlAgilityPack;\nusing ReverseMarkdown.Helpe"
  },
  {
    "path": "src/ReverseMarkdown/Converters/S.cs",
    "chars": 1168,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\n\n\nnamespace ReverseMarkdown.Converters {\n    public class S : ConverterBase {\n "
  },
  {
    "path": "src/ReverseMarkdown/Converters/Strong.cs",
    "chars": 4175,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\n\n\nnamespace ReverseMarkdown.Converters {\n    public class Strong : ConverterBas"
  },
  {
    "path": "src/ReverseMarkdown/Converters/Sup.cs",
    "chars": 1190,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\nusing ReverseMarkdown.Helpers;\n\n\nnamespace ReverseMarkdown.Converters {\n    pub"
  },
  {
    "path": "src/ReverseMarkdown/Converters/Table.cs",
    "chars": 5323,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing HtmlAgilityPack;\nusing Revers"
  },
  {
    "path": "src/ReverseMarkdown/Converters/Td.cs",
    "chars": 3076,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\n\n\nnamespace ReverseMarkdown.Converters {\n    public class Td : ConverterBase {\n"
  },
  {
    "path": "src/ReverseMarkdown/Converters/Text.cs",
    "chars": 16999,
    "preview": "using System;\nusing System.IO;\nusing System.Linq;\nusing System.Text;\nusing System.Text.RegularExpressions;\nusing HtmlAgi"
  },
  {
    "path": "src/ReverseMarkdown/Converters/Tr.cs",
    "chars": 3674,
    "preview": "using System.IO;\nusing System.Linq;\nusing HtmlAgilityPack;\nusing ReverseMarkdown.Helpers;\n\n\nnamespace ReverseMarkdown.C"
  },
  {
    "path": "src/ReverseMarkdown/Converters/UnknownTagReplacer.cs",
    "chars": 1044,
    "preview": "using System.IO;\nusing HtmlAgilityPack;\n\nnamespace ReverseMarkdown.Converters {\n    internal sealed class UnknownTagRepl"
  },
  {
    "path": "src/ReverseMarkdown/Helpers/LineSplitEnumerator.cs",
    "chars": 1300,
    "preview": "using System;\n\n\nnamespace ReverseMarkdown.Helpers;\n\n/// <summary>\n/// Enumerates the lines in a string as ReadOnlySpan&"
  },
  {
    "path": "src/ReverseMarkdown/Helpers/StringExtensions.cs",
    "chars": 2284,
    "preview": "using System;\nusing System.Text.RegularExpressions;\n\n\nnamespace ReverseMarkdown.Helpers;\n\npublic static class StringExt"
  },
  {
    "path": "src/ReverseMarkdown/Helpers/StringReplaceValues.cs",
    "chars": 995,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Text.RegularExpressi"
  },
  {
    "path": "src/ReverseMarkdown/Helpers/StringUtils.cs",
    "chars": 3493,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Text.RegularExpressi"
  },
  {
    "path": "src/ReverseMarkdown/ImageUtils.cs",
    "chars": 2439,
    "preview": "using System;\nusing System.IO;\nusing System.Text.RegularExpressions;\n\nnamespace ReverseMarkdown\n{\n    public static clas"
  },
  {
    "path": "src/ReverseMarkdown/ReverseMarkdown.csproj",
    "chars": 1236,
    "preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <Description>ReverseMarkdown is a Html to Markdown converter li"
  },
  {
    "path": "src/ReverseMarkdown/UnknownTagException.cs",
    "chars": 134,
    "preview": "using System;\n\n\nnamespace ReverseMarkdown;\n\npublic class UnknownTagException(string tagName) : Exception($\"Unknown tag:"
  },
  {
    "path": "src/ReverseMarkdown/UnsupportedTagExtension.cs",
    "chars": 633,
    "preview": "using System;\n\n\nnamespace ReverseMarkdown;\n\npublic class UnsupportedTagException : Exception {\n    internal Unsupported"
  },
  {
    "path": "src/ReverseMarkdown.Benchmark/Benchmark.md",
    "chars": 3125,
    "preview": "# Benchmark Results for ReverseMarkdown\n\n**Legends**\n\n```\n  Mean      : Arithmetic mean of all measurements\n  Error     "
  },
  {
    "path": "src/ReverseMarkdown.Benchmark/CompareBenchmark.cs",
    "chars": 656,
    "preview": "using BenchmarkDotNet.Attributes;\nusing BenchmarkDotNet.Jobs;\n\n\nnamespace ReverseMarkdown.Benchmark;\n\n[SimpleJob(Runtime"
  },
  {
    "path": "src/ReverseMarkdown.Benchmark/FileHelper.cs",
    "chars": 239,
    "preview": "namespace ReverseMarkdown.Benchmark {\n    internal sealed class FileHelper {\n        public static string ReadFile(strin"
  },
  {
    "path": "src/ReverseMarkdown.Benchmark/Files/1000-paragraphs.html",
    "chars": 450312,
    "preview": "<p><strong>CODICE DI PROCEDURA PENALE</strong></p>\n<p><strong>COMMENTATO</strong></p>\n<p><strong>CON DOTTRINA E GIURISPR"
  },
  {
    "path": "src/ReverseMarkdown.Benchmark/Files/10k-paragraphs.html",
    "chars": 3872329,
    "preview": "<p><strong>CODICE DI PROCEDURA PENALE</strong></p>\n<p><strong>COMMENTATO</strong></p>\n<p><strong>CON DOTTRINA E GIURISPR"
  },
  {
    "path": "src/ReverseMarkdown.Benchmark/Program.cs",
    "chars": 142,
    "preview": "using BenchmarkDotNet.Running;\nusing ReverseMarkdown.Benchmark;\n\n\nvar summary = BenchmarkRunner.Run<CompareBenchmark>()"
  },
  {
    "path": "src/ReverseMarkdown.Benchmark/ReverseMarkdown.Benchmark.csproj",
    "chars": 946,
    "preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <OutputType>Exe</OutputType>\n    <TargetFramework>net9.0</Targe"
  },
  {
    "path": "src/ReverseMarkdown.Test/ChildConverterTests.cs",
    "chars": 1068,
    "preview": "using ReverseMarkdown.Converters;\nusing ReverseMarkdown.Test.Children;\nusing System.Collections.Generic;\nusing System.L"
  },
  {
    "path": "src/ReverseMarkdown.Test/Children/IgnoreAWhenHasClass.cs",
    "chars": 448,
    "preview": "using HtmlAgilityPack;\nusing ReverseMarkdown.Converters;\nusing System.IO;\n\n\nnamespace ReverseMarkdown.Test.Children {\n "
  },
  {
    "path": "src/ReverseMarkdown.Test/CommonMarkSpecTests.cs",
    "chars": 4940,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text.Json;\nusing Markdi"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.Bug255_table_newline_char_issue.verified.md",
    "chars": 40,
    "preview": "| Progression | Focus |\n| :--- | :--- |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.Bug294_Table_bug_with_row_superfluous_newlines.verified.md",
    "chars": 322,
    "preview": "| 比较 | wordpress | hexo & hugo |\n| --- | --- | --- |\n| 搭建要求 | 一台服务器以及运行环境 | 静态生成页面,无需服务器。 |\n| 性能 | 由于是动态生成页面,可以通过自行配置提高"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.Bug391_AnchorTagUnnecessarilyIndented.verified.md",
    "chars": 171,
    "preview": "An error occurred while importing data from feed 'FBA Producten'. More details can be found in the latest [feed validat"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.Bug393_RegressionWithVaryingNewLines.verified.md",
    "chars": 83,
    "preview": "This is regular text\n\nThis is HTML:\n\n* Line 1\n* Line 2\n* Line 3 has an unknown tag"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.Bug400_MissingSpanSpaceWithItalics.verified.md",
    "chars": 171,
    "preview": "### *What we thought:* When we built Pages, we assumed that customers would use them like newsletters to share relevant"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.Bug403_unexpectedBehaviourWhenTableBodyRowsWithTHCells.verified.md",
    "chars": 78,
    "preview": "| Heading1 | Heading2 |\n| --- | --- |\n| data 1 | data 2 |\n| data 3 | data 4 |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.Check_Converter_With_Unknown_Tag_ByPass_Option.verified.md",
    "chars": 19,
    "preview": "text in unknown tag"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.Check_Converter_With_Unknown_Tag_Drop_Option.verified.md",
    "chars": 14,
    "preview": "paragraph text"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.Check_Converter_With_Unknown_Tag_PassThrough_Option.verified.md",
    "chars": 61,
    "preview": "<unknown-tag>text in unknown tag</unknown-tag>\nparagraph text"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.Check_Converter_With_Unknown_Tag_Raise_Option.verified.txt",
    "chars": 69,
    "preview": "{\n  Type: UnknownTagException,\n  Message: Unknown tag: unknown-tag\n}"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.EscapeMarkdownCharsInTextProperly.verified.md",
    "chars": 19,
    "preview": "[a-z]([0-9]){0,4}\n"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.Li_With_No_Parent.verified.md",
    "chars": 6,
    "preview": "- item"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.SlackFlavored_Bold.verified.md",
    "chars": 15,
    "preview": "*test* | *test*"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.SlackFlavored_Bullets.verified.md",
    "chars": 26,
    "preview": "• Item 1\n• Item 2\n• Item 3"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.SlackFlavored_Italic.verified.md",
    "chars": 15,
    "preview": "_test_ | _test_"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.SlackFlavored_Strikethrough.verified.md",
    "chars": 6,
    "preview": "~test~"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.TestConversionOfMultiParagraphWithHeaders.verified.md",
    "chars": 59,
    "preview": "# Heading1\n\nFirst paragraph.\n\n# Heading2\n\nSecond paragraph."
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenAnchorTagContainsImgTag_LinkTextShouldNotBeEscaped.verified.md",
    "chars": 61,
    "preview": "[![](https://example.com/image.jpg)](https://www.example.com)"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenBoldTagContainsBRTag_ThenConvertToMarkdown.verified.md",
    "chars": 12,
    "preview": "test**test**"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenCommentOverlapTag_WithRemoveComments_ThenDoNotStripContentBetweenComments.verified.md",
    "chars": 12,
    "preview": "test content"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenListContainsMultipleParagraphs_ConvertToMarkdownAndIndentSiblings.verified.md",
    "chars": 67,
    "preview": "1. Paragraph 1\n\n    Paragraph 1.1\n\n    Paragraph 1.2\n2. Paragraph 3"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenListContainsNewlineAndTabBetweenTagBorders_CleanupAndConvertToMarkdown.verified.md",
    "chars": 21,
    "preview": "1. **Item1**\n2. Item2"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenListContainsParagraphsOutsideItems_ConvertToMarkdownAndIndentSiblings.verified.md",
    "chars": 45,
    "preview": "1. Item1\n\n    Item 1 additional info\n2. Item2"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenListItemTextContainsLeadingAndTrailingSpacesAndTabs_ThenConvertToMarkdownListItemWithSpacesAndTabsStripped.verified.md",
    "chars": 59,
    "preview": "1. This is a text with leading and trailing spaces and tabs"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenRemovedCommentsIsEnabled_CommentsAreRemoved.verified.md",
    "chars": 11,
    "preview": "Hello there"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenStyletagWithBypassOption_ReturnEmpty.verified.md",
    "chars": 1,
    "preview": ""
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTableCellsWithDataAndP_ThenNewlineBeforeP.verified.md",
    "chars": 22,
    "preview": "| data1<br>p |\n| --- |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTableCellsWithDiv_ThenDoNotAddNewlines.verified.md",
    "chars": 47,
    "preview": "| col1 | col2 |\n| --- | --- |\n| data1 | data2 |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTableCellsWithMultipleP_ThenNoNewlines.verified.md",
    "chars": 24,
    "preview": "| p1<br><br>p2 |\n| --- |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTableCellsWithPWithMarkupNewlines_ThenTrimExcessNewlines.verified.md",
    "chars": 26,
    "preview": "| col1 |\n| --- |\n| data1 |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTableCellsWithP_ThenDoNotAddNewlines.verified.md",
    "chars": 47,
    "preview": "| col1 | col2 |\n| --- | --- |\n| data1 | data2 |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTableCellsWithP_ThenNoNewlines.verified.md",
    "chars": 17,
    "preview": "| data1 |\n| --- |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTableHeadingWithAlignmentStyles_ThenTableHeaderShouldHaveProperAlignment.verified.md",
    "chars": 61,
    "preview": "| Col1 | Col2 | Col2 |\n| :--- | :---: | ---: |\n| 1 | 2 | 3 |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTableRowWithDuplicateStyleKeysAfterTrimming_ThenConvertWithoutException.verified.md",
    "chars": 29,
    "preview": "| Header |\n| --- |\n| Data |\n"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTable_CellContainsBr_PreserveBrAndConvertToGFMTable.verified.md",
    "chars": 37,
    "preview": "| col1 |\n| --- |\n| line 1<br>line 2 |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTable_CellContainsParagraph_AddBrThenConvertToGFMTable.verified.md",
    "chars": 39,
    "preview": "| col1 |\n| --- |\n| line1<br><br>line2 |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTable_Cell_Content_WithNewline_Add_BR_ThenConvertToGFMTable.verified.md",
    "chars": 82,
    "preview": "| col1 | col2 | col3 |\n| --- | --- | --- |\n| data line1<br>line2 | data2 | data3 |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTable_ContainsTheadTd_ConvertToGFMTable.verified.md",
    "chars": 47,
    "preview": "| col1 | col2 |\n| --- | --- |\n| data1 | data2 |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTable_ContainsTheadTh_ConvertToGFMTable.verified.md",
    "chars": 47,
    "preview": "| col1 | col2 |\n| --- | --- |\n| data1 | data2 |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTable_HasEmptyRow_DropsEmptyRow.verified.md",
    "chars": 27,
    "preview": "| <!----> |\n| --- |\n| abc |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTable_ThenConvertToGFMTable.verified.md",
    "chars": 68,
    "preview": "| col1 | col2 | col3 |\n| --- | --- | --- |\n| data1 | data2 | data3 |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTable_WithCaptionAndGithubFlavored_ThenCaptionAppearsAboveTable.verified.md",
    "chars": 71,
    "preview": "Code Review Summary\n\n| PR | Status |\n| --- | --- |\n| #123 | Approved |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTable_WithCaptionAndNoHeaderRow_EmptyRowHandling_ThenCaptionAppearsAboveTable.verified.md",
    "chars": 84,
    "preview": "Data Table\n\n| <!----> | <!----> |\n| --- | --- |\n| data1 | data2 |\n| data3 | data4 |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTable_WithCaptionContainingMarkdownChars_ThenHandleProperly.verified.md",
    "chars": 84,
    "preview": "Sales Report [2024] - **Important**\n\n| Month | Sales |\n| --- | --- |\n| Jan | $100 |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTable_WithCaptionContainingNestedTags_ThenExtractTextOnly.verified.md",
    "chars": 49,
    "preview": "Sales Report for 2024\n\n| Month |\n| --- |\n| Jan |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTable_WithCaptionContainingNewlines_ThenHandleNewlines.verified.md",
    "chars": 45,
    "preview": "Multi\nLine\nCaption\n\n| Col |\n| --- |\n| Data |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTable_WithCaptionContainingWhitespace_ThenTrimWhitespace.verified.md",
    "chars": 42,
    "preview": "Table Caption\n\n| Col1 |\n| --- |\n| Data1 |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTable_WithCaption_ThenCaptionAppearsAboveTable.verified.md",
    "chars": 74,
    "preview": "Monthly Sales Report\n\n| Month | Sales |\n| --- | --- |\n| January | $1000 |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTable_WithColSpan_TableHeaderColumnSpansHandling_ThenConvertToGFMTable.verified.md",
    "chars": 94,
    "preview": "| col1 | col2 | col2 | col3 |\n| --- | --- | --- | --- |\n| data1 | data2.1 | data2.2 | data3 |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTable_WithEmptyCaption_ThenConvertNormally.verified.md",
    "chars": 52,
    "preview": "| Month | Sales |\n| --- | --- |\n| January | $1000 |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTable_WithoutCaption_ThenConvertNormally.verified.md",
    "chars": 52,
    "preview": "| Month | Sales |\n| --- | --- |\n| January | $1000 |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTable_WithoutHeaderRow_With_TableWithoutHeaderRowHandlingOptionDefault_ThenConvertToGFMTable_WithFirstRowAsHeaderRow.verified.md",
    "chars": 71,
    "preview": "| data1 | data2 | data3 |\n| --- | --- | --- |\n| data4 | data5 | data6 |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenTable_WithoutHeaderRow_With_TableWithoutHeaderRowHandlingOptionEmptyRow_ThenConvertToGFMTable_WithEmptyHeaderRow.verified.md",
    "chars": 103,
    "preview": "| <!----> | <!----> | <!----> |\n| --- | --- | --- |\n| data1 | data2 | data3 |\n| data4 | data5 | data6 |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereAreBTag_ThenConvertToMarkdownDoubleAsterisks.verified.md",
    "chars": 37,
    "preview": "This paragraph contains **bold** text"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereAreLineBreaksEncompassingParagraphText_It_Should_be_Removed.verified.md",
    "chars": 20,
    "preview": "Some text goes here."
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereAreMultipleLinks_ThenConvertThemToMarkdownLinks.verified.md",
    "chars": 73,
    "preview": "This is [first link](http://test.com) and [second link](http://test1.com)"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereAreSemanticContainerTags.verified.md",
    "chars": 106,
    "preview": "Whatever Inc.\nThanks for your enquiry.\nSection intro\n\nArticle text\n\nHome\n\nDiagram\n\nFigure caption\n\nMail us"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereAreStrongTag_ThenConvertToMarkdownDoubleAsterisks.verified.md",
    "chars": 37,
    "preview": "This paragraph contains **bold** text"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereHtmlWithHrefAndNoSchema_NotWhitelisted_ThenConvertToPlain.verified.md",
    "chars": 4,
    "preview": "yeah"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereHtmlWithHrefAndNoSchema_WhitelistedEmptyString_ThenConvertToMarkdown.verified.md",
    "chars": 19,
    "preview": "[yeah](example.com)"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsAsideTag.verified.md",
    "chars": 61,
    "preview": "This text is in an aside tag.\n This text appears after aside."
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsBase64ImgTag_WithDefaultConfig_ThenIncludeInMarkdown.verified.md",
    "chars": 141,
    "preview": "Before\n![test](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAA"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsBase64ImgTag_WithSaveToFileConfigButNoDirectory_ThenSkipImage.verified.md",
    "chars": 14,
    "preview": "Before\n\nAfter"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsBase64ImgTag_WithSkipConfig_ThenSkipImage.verified.md",
    "chars": 14,
    "preview": "Before\n\nAfter"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsBase64JpegImgTag_WithSkipConfig_ThenSkipImage.verified.md",
    "chars": 2,
    "preview": "\n"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsBlockquoteTag_ThenConvertToMarkdownBlockquote.verified.md",
    "chars": 62,
    "preview": "This text has \n\n> blockquote\n\n. This text appear after header."
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsBreakTag_ThenConvertToMarkdownDoubleSpacesCarriageReturn.verified.md",
    "chars": 53,
    "preview": "This is a paragraph.  \nThis line appears after break."
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsEmptyBlockquoteTag_ThenConvertToMarkdownBlockquote.verified.md",
    "chars": 48,
    "preview": "This text has \n\n. This text appear after header."
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsEmptyPreTag_ThenConvertToMarkdownPre.verified.md",
    "chars": 49,
    "preview": "This text has pre tag content \n\nNext line of text"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsEmptyPreTag_ThenConvertToMarkdownPre_GFM.verified.md",
    "chars": 59,
    "preview": "This text has pre tag content \n\n```\n\n```\nNext line of text"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsEncompassingEmOrITag_ThenConvertToMarkdownSingleAsterisks_AnyEmOrITagsInsideAreIgnored.verified.md",
    "chars": 28,
    "preview": "*This is a sample paragraph*"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsEncompassingStrongOrBTag_ThenConvertToMarkdownDoubleAsterisks_AnyStrongOrBTagsInsideAreIgnored.verified.md",
    "chars": 83,
    "preview": "**Paragraph is encompassed with strong tag and also has bold text words within it**"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsH1Tag_ThenConvertToMarkdownHeader.verified.md",
    "chars": 56,
    "preview": "This text has \n# header\n. This text appear after header."
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsH2Tag_ThenConvertToMarkdownHeader.verified.md",
    "chars": 57,
    "preview": "This text has \n## header\n. This text appear after header."
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsH3Tag_ThenConvertToMarkdownHeader.verified.md",
    "chars": 58,
    "preview": "This text has \n### header\n. This text appear after header."
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsH4Tag_ThenConvertToMarkdownHeader.verified.md",
    "chars": 59,
    "preview": "This text has \n#### header\n. This text appear after header."
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsH5Tag_ThenConvertToMarkdownHeader.verified.md",
    "chars": 60,
    "preview": "This text has \n##### header\n. This text appear after header."
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsH6Tag_ThenConvertToMarkdownHeader.verified.md",
    "chars": 61,
    "preview": "This text has \n###### header\n. This text appear after header."
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHeadingInsideTable_ThenIgnoreHeadingLevel.verified.md",
    "chars": 40,
    "preview": "| Heading **text** |\n| --- |\n| Content |"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHorizontalRule_ThenConvertToMarkdownHorizontalRule.verified.md",
    "chars": 54,
    "preview": "This text has horizontal rule.\n* * *\nNext line of text"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHtmlLinkNotWhitelisted_ThenBypass.verified.md",
    "chars": 175,
    "preview": "Leave [http](http://example.com), [https](https://example.com), [ftp](ftp://example.com), [ftps](ftps://example.com), [f"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHtmlLinkWithDisallowedCharsInChildren_ThenEscapeTextInMarkdown.verified.md",
    "chars": 50,
    "preview": "[this \\]( might break things](http://example.com)\n"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHtmlLinkWithParensInHref_ThenEscapeHrefInMarkdown.verified.md",
    "chars": 39,
    "preview": "[link](http://example.com?id=foo%29bar)"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHtmlLinkWithTitle_ThenConvertToMarkdownLink.verified.md",
    "chars": 46,
    "preview": "This is [a link](http://test.com \"with title\")"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHtmlLinkWithoutHttpSchemaAndNameWithoutScheme_SmartHandling_ThenConvertToMarkdown.verified.md",
    "chars": 32,
    "preview": "[example.com](ftp://example.com)"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHtmlLink_ThenConvertToMarkdownLink.verified.md",
    "chars": 33,
    "preview": "This is [a link](http://test.com)"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHtmlWithHrefAndNameMatching_SmartHandling_ThenConvertToPlain.verified.md",
    "chars": 24,
    "preview": "http://example.com/abc?x"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHtmlWithHrefAndNameNotMatching_SmartHandling_ThenConvertToMarkdown.verified.md",
    "chars": 39,
    "preview": "[Something intact](https://example.com)"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHtmlWithMailtoSchemeAndNameWithoutScheme_SmartHandling_ThenConvertToPlain.verified.md",
    "chars": 18,
    "preview": "george@example.com"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHtmlWithProtocolRelativeUrlHrefAndNameNotMatching_SmartHandling_ThenConvertToMarkdown.verified.md",
    "chars": 28,
    "preview": "[example.com](//example.com)"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsHtmlWithTelSchemeAndNameWithoutScheme_SmartHandling_ThenConvertToPlain.verified.md",
    "chars": 11,
    "preview": "+1123-45678"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagAndSrcWithNoSchema_NotWhitelisted_ThenConvertToPlain.verified.md",
    "chars": 1,
    "preview": ""
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagAndSrcWithNoSchema_WhitelistedEmptyString_ThenConvertToMarkdown.verified.md",
    "chars": 16,
    "preview": "![](example.com)"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagWithBracesInAltText_ThenEnsureAltTextIsEscapedInMarkdown.verified.md",
    "chars": 79,
    "preview": "This text has image ![a\\]b](http://test.com/images/test.png). Next line of text"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagWithHttpProtocolRelativeUrl_ConfigHasWhitelist_ThenConvertToMarkdown.verified.md",
    "chars": 18,
    "preview": "![](//example.gif)"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagWithMultilineAltText_ThenEnsureNoBlankLinesInMarkdownAltText.verified.md",
    "chars": 83,
    "preview": "This text has image ![cat\ndog](http://test.com/images/test.png). Next line of text"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagWithRelativeUrl_NotWhitelisted_ThenConvertToMarkdown.verified.md",
    "chars": 1,
    "preview": ""
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagWithUnixUrl_ConfigHasWhitelist_ThenConvertToMarkdown.verified.md",
    "chars": 17,
    "preview": "![](/example.gif)"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagWithoutAltText_ThenConvertToMarkdownImageWithoutAltText.verified.md",
    "chars": 75,
    "preview": "This text has image ![](http://test.com/images/test.png). Next line of text"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTagWithoutTitle_ThenConvertToMarkdownImageWithoutTitle.verified.md",
    "chars": 78,
    "preview": "This text has image ![alt](http://test.com/images/test.png). Next line of text"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTag_SchemeIsWhitelisted_ThenConvertToMarkdown.verified.md",
    "chars": 42,
    "preview": "![](data:image/gif;base64,R0lGODlhEAAQ...)"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTag_SchemeNotWhitelisted_ThenEmptyOutput.verified.md",
    "chars": 1,
    "preview": ""
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsImgTag_ThenConvertToMarkdownImage.verified.md",
    "chars": 86,
    "preview": "This text has image ![alt](http://test.com/images/test.png \"title\"). Next line of text"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsInputListWithGithubFlavoredDisabled_ThenConvertToTypicalMarkdownList.verified.md",
    "chars": 87,
    "preview": "- <input type=\"checkbox\" disabled> Unchecked\n- <input type=\"checkbox\" checked> Checked"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsInputListWithGithubFlavoredEnabled_ThenConvertToMarkdownCheckList.verified.md",
    "chars": 30,
    "preview": "- [ ] Unchecked\n- [x] Checked"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsOrderedListWithNestedUnorderedList_ThenConvertToMarkdownListWithNestedList.verified.md",
    "chars": 84,
    "preview": "This text has ordered list.\n1. OuterItem1\n    - InnerItem1\n    - InnerItem2\n2. Item2"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsOrderedList_ThenConvertToMarkdownList.verified.md",
    "chars": 45,
    "preview": "This text has ordered list.\n1. Item1\n2. Item2"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsParagraphTag_ThenConvertToMarkdownDoubleLineBreakBeforeAndAfter.verified.md",
    "chars": 51,
    "preview": "This text has markup \nparagraph.\n Next line of text"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsPreTag_ThenConvertToMarkdownPre.verified.md",
    "chars": 70,
    "preview": "This text has pre tag content \n\n    Predefined text\n\nNext line of text"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsSingleAsteriskInText_ThenConvertToMarkdownEscapedAsterisk.verified.md",
    "chars": 31,
    "preview": "This is a sample(\\*) paragraph\n"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsUnorderedListAndBulletIsAsterisk_ThenConvertToMarkdownList.verified.md",
    "chars": 45,
    "preview": "This text has unordered list.\n* Item1\n* Item2"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsUnorderedListWithNestedOrderedList_ThenConvertToMarkdownListWithNestedList.verified.md",
    "chars": 84,
    "preview": "This text has ordered list.\n- OuterItem1\n    1. InnerItem1\n    2. InnerItem2\n- Item2"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsUnorderedList_ThenConvertToMarkdownList.verified.md",
    "chars": 45,
    "preview": "This text has unordered list.\n- Item1\n- Item2"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenThereIsWhitespaceAroundNestedLists_PreventBlankLinesWhenConvertingToMarkdownList.verified.md",
    "chars": 64,
    "preview": "- OuterItem1\n    1. InnerItem1\n- Item2\n    1. InnerItem2\n- Item3"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenUnclosedScriptTag_WithBypassUnknownTags_ThenConvertToMarkdown.verified.md",
    "chars": 13,
    "preview": "Test content\n"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.WhenUnclosedStyleTag_WithBypassUnknownTags_ThenConvertToMarkdown.verified.md",
    "chars": 13,
    "preview": "Test content\n"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.When_AlternatingEmptyAndFilledNestedParagraphs_ThenConvertCorrectly.verified.md",
    "chars": 14,
    "preview": "A\n\nB\n\nC\n\nD\n\nE"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.When_Anchor_Text_with_Underscore_Do_Not_Escape.verified.md",
    "chars": 122,
    "preview": "This a sample **paragraph** from [https://www.w3schools.com/html/mov_bbb.mp4](https://www.w3schools.com/html/mov_bbb.mp"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.When_BR_With_GitHubFlavored_Config_ThenConvertToGFM_BR.verified.md",
    "chars": 22,
    "preview": "First part\nSecond part"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.When_CodeContainsSpacesAndIsSurroundedByWhitespace_Should_NotRemoveSpaces.verified.md",
    "chars": 29,
    "preview": "A JavaScript ` function ` ..."
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.When_CodeContainsSpaces_ShouldPreserveSpaces.verified.md",
    "chars": 27,
    "preview": "A JavaScript` function `..."
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.When_CodeContainsSpanWithExtraSpaces_Should_NotNormalizeSpaces.verified.md",
    "chars": 31,
    "preview": "A JavaScript`    function  `..."
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.When_ComplexNestedTableIsInTable_LeaveNestedTableAsHtml.verified.md",
    "chars": 179,
    "preview": "| Category | Details |\n| --- | --- |\n| Products | <table> <tr><th>Name</th><th>Price</th></tr> <tr><td>Item 1</td><td>$1"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.When_Consecutive_Em_Tags_Should_Convert_Properly.verified.md",
    "chars": 35,
    "preview": "*block1* *block2* *block3* *block4*"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.When_Consecutive_Strong_Tags_Should_Convert_Properly.verified.md",
    "chars": 43,
    "preview": "**block1** **block2** **block3** **block4**"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.When_Content_Contains_script_tags_ignore_it.verified.md",
    "chars": 17,
    "preview": "simple paragraph"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.When_Converting_HTML_Ensure_To_Process_Only_Body.verified.md",
    "chars": 11,
    "preview": "sample text"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.When_DeeplyNestedParagraphs_WithMalformedHTML_ThenConvertWithoutHanging.verified.md",
    "chars": 1,
    "preview": ""
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.When_DescriptionListTag_ThenConvertToMarkdown_List.verified.md",
    "chars": 82,
    "preview": "- Coffee\n    - Filter Coffee\n    - Hot Black Coffee\n- Milk\n    - White Cold Drink"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.When_EmptyNestedParagraphs_ThenConvertCorrectly.verified.md",
    "chars": 1,
    "preview": ""
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.When_FencedCodeBlocks_Shouldnt_Have_Trailing_Line.verified.md",
    "chars": 69,
    "preview": "```xml\n<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>\n```"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.When_Html_Containing_Nested_DIVs_Process_ONLY_Inner_Most_DIV.verified.md",
    "chars": 11,
    "preview": "sample text"
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.When_InlineCode_Shouldnt_Contain_Encoded_Chars.verified.md",
    "chars": 48,
    "preview": "This is inline code: `<AspNetCoreHostingModel>`."
  },
  {
    "path": "src/ReverseMarkdown.Test/ConverterTests.When_InterleavedParagraphsAndSpans_ThenConvertCorrectly.verified.md",
    "chars": 24,
    "preview": "Text1\nText2\nText3\nText4"
  }
]

// ... and 46 more files (download for full content)

About this extraction

This page contains the full source code of the mysticmind/reversemarkdown-net GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 246 files (20.9 MB), approximately 1.2M tokens, and a symbol index with 312 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!