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 wikipedia HTML document to Markdown" do
{:ok, html} = File.read(@fixture_path <> "wikipedia.html")
{:ok, markdown} = File.read(@fixture_path <> "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 stinkyaromatic presence of blue cheese make for an unforgettable culinary experience.
"""
expected =
"\nThe **bold** flavors of aged cheddar, the *subtle* notes of brie, and the ~~stinky~~ *aromatic* presence of blue cheese make for an `unforgettable` culinary experience.\n"
assert Html2Markdown.convert(fragment) == expected
end
test "handle " do
fragment = """
a stray paragraph
"""
expected =
""
# 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 add newlines and are joined with \n\n
expected = "\nThis has multiple spaces between words.\n\n\n\nThis has tabs between words.\n"
assert Html2Markdown.convert(html) == expected
end
test "removes leading and trailing whitespace from block elements" do
html = """
Leading and trailing spaces
Header with spaces
"""
# Each element adds newlines and they're joined with \n\n
expected = "\nLeading and trailing spaces\n\n\n\n# Header with spaces\n"
assert Html2Markdown.convert(html) == expected
end
test "preserves whitespace in code blocks" do
html = """
def example do
# Indentation preserved
nested_code
end
"""
expected = "\n```\ndef example do\n # Indentation preserved\n nested_code\nend\n```\n"
assert Html2Markdown.convert(html) == expected
end
test "preserves whitespace in inline code" do
html = """
Use spaced code for examples.
"""
# Paragraph adds newlines
expected = "\nUse ` spaced code ` for examples.\n"
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
Sidebar 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 = """
Name
Value
Just text without td tags
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 add newlines and are joined with \n\n
expected =
"\nThe & symbol, brackets, and \"quotes\" are decoded.\n\n\n\nNon-breaking\u00A0space and apostrophe's work too.\n"
assert result == expected
end
test "preserves entities in code blocks" do
html = """
<div class="example">
Content & more
</div>
"""
expected = "\n```\n
\nContent & more\n
\n```\n"
assert Html2Markdown.convert(html) == expected
end
test "handles numeric entities" do
html = """