defmodule Html2MarkdownTest do use ExUnit.Case doctest Html2Markdown @fixture_path "test/support/fixtures/" describe "convert a HTML document to Markdown" do test "convert elixir-lang HTML document to Markdown" do {:ok, html} = File.read(@fixture_path <> "elixir.html") {:ok, markdown} = File.read(@fixture_path <> "elixir.md") assert Html2Markdown.convert(html) == markdown end test "convert elixir wikipedia HTML document to Markdown" do {:ok, html} = File.read(@fixture_path <> "elixir_wikipedia.html") {:ok, markdown} = File.read(@fixture_path <> "elixir_wikipedia.md") assert Html2Markdown.convert(html) == markdown end end test "convert a HTML fragment to Markdown" do fragment = """

The bold flavors of aged cheddar, the subtle notes of brie, and the stinky aromatic presence of blue cheese make for an unforgettable culinary experience.

""" expected = "The **bold** flavors of aged cheddar, the *subtle* notes of brie, and the ~~stinky~~ *aromatic* presence of blue cheese make for an `unforgettable` culinary experience." assert Html2Markdown.convert(fragment) == expected end test "handle " do fragment = """

a stray paragraph

a shadow

""" expected = "![a shadow](https://upload.wikimedia.org/wikipedia/commons/thumb/3/31/Bombina_bombina_1_%28Marek_Szczepanek%29.jpg/440px-Bombina_bombina_1_%28Marek_Szczepanek%29.jpg)" # The p tag adds newlines, so we just check if the image markdown is contained assert String.contains?(Html2Markdown.convert(fragment), expected) end describe "whitespace normalization" do test "normalizes multiple spaces to single space" do html = """

This has multiple spaces between words.

This has tabs between words.

""" # Paragraphs are joined with \n\n expected = "This has multiple spaces between words.\n\nThis has tabs between words." assert Html2Markdown.convert(html) == expected end test "removes leading and trailing whitespace from block elements" do html = """

Leading and trailing spaces

Header with spaces

""" # Paragraph and header are joined with \n\n expected = "Leading and trailing spaces\n\n# Header with spaces" assert Html2Markdown.convert(html) == expected end test "preserves whitespace in code blocks" do html = """
def example do
        # Indentation preserved
          nested_code
      end
""" expected = "```\ndef example do\n # Indentation preserved\n nested_code\nend\n```" assert Html2Markdown.convert(html) == expected end test "preserves whitespace in inline code" do html = """

Use spaced code for examples.

""" # Paragraph without extra newlines expected = "Use ` spaced code ` for examples." assert Html2Markdown.convert(html) == expected end test "whitespace normalization can be disabled" do html = """

This has multiple spaces.

""" result = Html2Markdown.convert(html, %{normalize_whitespace: false}) # When disabled, spaces should be preserved (though still trimmed by process_children) assert String.contains?(result, "This has multiple spaces.") end end describe "configuration options" do test "pre blocks always preserve whitespace" do html = "
  Multiple    spaces   
" # Pre blocks should preserve whitespace even with normalize_whitespace: true (default) result = Html2Markdown.convert(html) assert String.contains?(result, "```\n Multiple spaces \n```") # Should also work with normalize_whitespace: false result = Html2Markdown.convert(html, %{normalize_whitespace: false}) assert String.contains?(result, " Multiple spaces ") end test "normalize_whitespace option" do html = """

Multiple spaces between words

Leading and trailing spaces

Block element with newlines
  Preserve    whitespace   in   pre  
Keep spaces in code """ # Default behavior - normalize_whitespace is true result = Html2Markdown.convert(html) assert String.contains?(result, "Multiple spaces between words") assert String.contains?(result, "Leading and trailing spaces") assert String.contains?(result, "Block element\nwith newlines") assert String.contains?(result, "```\n Preserve whitespace in pre \n```") assert String.contains?(result, "` Keep spaces in code `") # With normalize_whitespace disabled result = Html2Markdown.convert(html, %{normalize_whitespace: false}) assert String.contains?(result, "Multiple spaces between words") assert String.contains?(result, " Leading and trailing spaces ") end test "custom navigation_classes option" do html = """
Custom header content
Main content
""" # Default behavior - removes nav tag and elements with default navigation classes (including "sidebar") result = Html2Markdown.convert(html) assert String.contains?(result, "Custom header content") assert String.contains?(result, "Main content") assert not String.contains?(result, "Navigation content") # Default includes "sidebar" assert not String.contains?(result, "Sidebar content") # With custom navigation classes result = Html2Markdown.convert(html, %{navigation_classes: ["custom-header"]}) assert not String.contains?(result, "Custom header content") assert String.contains?(result, "Main content") # No longer filtered since we replaced defaults assert String.contains?(result, "Sidebar content") end test "custom non_content_tags option" do html = """

Regular content

Custom tag content
""" # Default behavior - removes script but not custom-tag result = Html2Markdown.convert(html) assert String.contains?(result, "Regular content") assert String.contains?(result, "Custom tag content") assert not String.contains?(result, "alert") # With custom non-content tags result = Html2Markdown.convert(html, %{non_content_tags: ["custom-tag", "script"]}) assert String.contains?(result, "Regular content") assert not String.contains?(result, "Custom tag content") assert not String.contains?(result, "alert") end end describe "definition lists" do test "converts simple definition list" do html = """
Elixir
A dynamic, functional programming language
Phoenix
A productive web framework for Elixir
""" expected = """ **Elixir** : A dynamic, functional programming language **Phoenix** : A productive web framework for Elixir """ assert Html2Markdown.convert(html) |> String.trim() == String.trim(expected) end test "handles definition lists with multiple definitions per term" do html = """
HTTP
HyperText Transfer Protocol
The foundation of data communication on the web
""" result = Html2Markdown.convert(html) assert String.contains?(result, "**HTTP**") assert String.contains?(result, ": HyperText Transfer Protocol") assert String.contains?(result, ": The foundation of data communication on the web") end test "handles definition lists with nested elements" do html = """
Important Term
A definition with emphasis and code
""" result = Html2Markdown.convert(html) # The dt element makes the content bold, and the nested strong makes "Important" extra bold assert String.contains?(result, "****Important** Term**") assert String.contains?(result, ": A definition with *emphasis* and `code`") end end describe "table edge cases" do test "converts table with empty tbody" do html = """
Product Price Stock
""" expected = """ | Product | Price | Stock | | --- | --- | --- | | | """ assert Html2Markdown.convert(html) |> String.trim() == String.trim(expected) end test "converts table with message row spanning columns" do html = """
Order ID Customer Total
No orders found.
""" expected = """ | Order ID | Customer | Total | | --- | --- | --- | | No orders found. | No orders found. | No orders found. | """ assert Html2Markdown.convert(html) |> String.trim() == String.trim(expected) end test "converts dashboard with multiple empty tables" do html = """

Sales Dashboard

Region Q1 Sales Q2 Sales
No data available

Inventory Status

Item Quantity Location
""" expected_contains = [ "## Sales Dashboard", "| Region | Q1 Sales | Q2 Sales |", "| --- | --- | --- |", "| No data available | | |", "## Inventory Status", "| Item | Quantity | Location |", "| --- | --- | --- |", "| |" ] result = Html2Markdown.convert(html) Enum.each(expected_contains, fn expected_line -> assert String.contains?(result, expected_line) end) end test "handles table with mixed empty and populated rows" do html = """
Task Status
Setup environment Complete
Deploy application Pending
""" expected = """ | Task | Status | | --- | --- | | Setup environment | Complete | | | | Deploy application | Pending | """ assert Html2Markdown.convert(html) |> String.trim() == String.trim(expected) end test "handles malformed table cells" do html = """ Just text without td tags
Name Value
Valid row Valid value
""" result = Html2Markdown.convert(html) # Should not crash and should include the valid row assert String.contains?(result, "| Valid row | Valid value |") end test "converts form with nested empty tables" do html = """

Search Results

ID Name Actions
No results found.
""" result = Html2Markdown.convert(html) assert String.contains?(result, "### Search Results") assert String.contains?(result, "| ID | Name | Actions |") assert String.contains?(result, "| No results found. | | |") end test "handles table with empty header row" do html = """
Data 1 Data 2
""" result = Html2Markdown.convert(html) # Should handle empty header gracefully assert String.contains?(result, "| |") assert String.contains?(result, "| --- |") assert String.contains?(result, "| Data 1 | Data 2 |") end test "handles table where first row has no cells for header separator" do html = """
Row with data More data
""" result = Html2Markdown.convert(html) # Should not crash and should process the valid row assert String.contains?(result, "| Row with data | More data |") end end describe "HTML entity handling" do test "decodes common HTML entities" do html = """

The & symbol, <tag> brackets, and "quotes" are decoded.

Non-breaking space and apostrophe's work too.

""" # Note: nbsp is converted to a non-breaking space character by Floki result = Html2Markdown.convert(html) # Floki converts   to Unicode non-breaking space (U+00A0) # Paragraphs are joined with \n\n expected = "The & symbol, brackets, and \"quotes\" are decoded.\n\nNon-breaking\u00A0space and apostrophe's work too." assert result == expected end test "preserves entities in code blocks" do html = """
<div class="example">
      Content & more
      </div>
""" expected = "```\n
\nContent & more\n
\n```" assert Html2Markdown.convert(html) == expected end test "handles numeric entities" do html = """

Copyright © 2024 • All rights reserved

""" expected = "Copyright © 2024 • All rights reserved" assert Html2Markdown.convert(html) == expected end test "entities in inline code are preserved" do html = """

Use <div> for layout and && for logical AND.

""" expected = "Use `
` for layout and `&&` for logical AND." assert Html2Markdown.convert(html) == expected end end end