"""Offline unit tests raising coverage for ``openbb_sec.utils.html2markdown``. The module is a pure HTML-to-Markdown converter for SEC filings built on BeautifulSoup and string processing. No network access is required: the public ``html_to_markdown`` entry point and the many smaller helpers are exercised directly with crafted, SEC-filing-shaped HTML / row structures. Tests are grouped: cheap pure-helper tests first, then a handful of rich ``html_to_markdown`` fixtures that sweep the large table / period / layout machinery transitively. """ from bs4 import BeautifulSoup import openbb_sec.utils.html2markdown as h2m # =========================================================================== # Helpers for building bs4 inputs # =========================================================================== def _table(html): """Parse an HTML snippet and return its first Tag.""" soup = BeautifulSoup(html, "html.parser") return soup.find("table") def _tag(html, name=None): """Parse an HTML snippet and return the first tag (optionally by name).""" soup = BeautifulSoup(html, "html.parser") if name: return soup.find(name) return soup.find(True) def _cell(text, cs=1, rs=1, th=False, idv=None): """Build a ', "td") out = h2m._extract_cell_text(cell, base_url="https://x.com/") assert "[link](https://x.com/a.htm)" in out def test_extract_cell_text_footnote_sup_only(): cell = _tag("", "td") assert h2m._extract_cell_text(cell) == "12" def test_extract_cell_text_strips_decorative_image(): cell = _tag('', "td") out = h2m._extract_cell_text(cell) assert out == "Text" def test_extract_cell_text_word_rejoin(): cell = _tag("", "td") assert h2m._extract_cell_text(cell) == "ADJUSTMENTS" def test_extract_cell_text_preserve_line_breaks(): cell = _tag( "", "td", ) out = h2m._extract_cell_text(cell, preserve_line_breaks=True) assert "Item A here" in out and "\n" in out # =========================================================================== # convert_table (data path) # =========================================================================== def test_convert_table_empty(): t = _table("
/ string with optional colspan/rowspan/id.""" tag = "th" if th else "td" attrs = "" if cs != 1: attrs += f' colspan="{cs}"' if rs != 1: attrs += f' rowspan="{rs}"' if idv: attrs += f' id="{idv}"' return f"<{tag}{attrs}>{text}" def _build_table(rows): """Build a Tag from a list of rows (each a list of _cell strings).""" return _table(_table_html(rows)) def _table_html(rows): """Build a
HTML string from a list of rows (lists of _cell strings).""" body = "".join("" + "".join(r) + "" for r in rows) return f"
{body}
" # =========================================================================== # strip_all / financial normalization # =========================================================================== def test_strip_all_removes_invisible_and_whitespace(): assert h2m.strip_all(" \u200bhello ") == "hello" assert h2m.strip_all("\xa0 x \xa0") == "x" def test_normalize_financial_cell_variants(): assert h2m._normalize_financial_cell("$ 1,787") == "$1,787" assert h2m._normalize_financial_cell("( 135 )") == "(135)" assert h2m._normalize_financial_cell("$ ( 471 )") == "$(471)" assert h2m._normalize_financial_cell("( 0.2 ) %") == "(0.2)%" # dollar-dash assert h2m._normalize_financial_cell("$ —") == "$—" # dash-percent assert h2m._normalize_financial_cell("— %") == "—%" def test_normalize_financial_cell_passthrough(): # empty / whitespace returns original assert h2m._normalize_financial_cell(" ") == " " # plain text passes through unchanged assert h2m._normalize_financial_cell("Revenue") == "Revenue" # already compact -> unchanged (compact == s branch) assert h2m._normalize_financial_cell("$1,787") == "$1,787" def test_normalize_financial_rows(): rows = [["$ 1,787", "Revenue"], ["( 135 )", "x"]] out = h2m._normalize_financial_rows(rows) assert out == [["$1,787", "Revenue"], ["(135)", "x"]] # =========================================================================== # remove_empty_columns # =========================================================================== def test_remove_empty_columns_empty_input(): assert h2m.remove_empty_columns([]) == [] def test_remove_empty_columns_drops_blank_column(): rows = [["A", "", "B"], ["1", "", "2"]] out = h2m.remove_empty_columns(rows) assert out == [["A", "B"], ["1", "2"]] def test_remove_empty_columns_ragged_rows(): rows = [["A", "x"], ["B"]] out = h2m.remove_empty_columns(rows) # second column kept (has content in first row); missing cell -> "" assert out == [["A", "x"], ["B", ""]] # =========================================================================== # is_data_row_header # =========================================================================== def test_is_data_row_header_empty(): assert h2m.is_data_row_header(["", " "]) is True def test_is_data_row_header_years_ended_no_numbers(): assert h2m.is_data_row_header(["For the Years Ended December 31,"]) is True def test_is_data_row_header_months_ended_with_data_is_not_header(): # data row whose label has "months ended" but also numeric values row = ["Three months ended April 30,", "10", "$273.42", "$2,681"] assert h2m.is_data_row_header(row) is False def test_is_data_row_header_all_caps_categories(): assert h2m.is_data_row_header(["EQUIPMENT", "OPERATIONS", "FINANCIAL"]) is True def test_is_data_row_header_year_only_row(): assert h2m.is_data_row_header(["2025", "2024", "2023"]) is True def test_is_data_row_header_period_row(): assert h2m.is_data_row_header(["May 1999", "November 1998"]) is True def test_is_data_row_header_regular_data_row(): assert h2m.is_data_row_header(["Revenue", "$100", "$200"]) is False # =========================================================================== # _convert_image_to_html # =========================================================================== def test_convert_image_no_src(): img = _tag('x', "img") assert h2m._convert_image_to_html(img) == "" def test_convert_image_markdown_no_size(): img = _tag('Logo', "img") assert h2m._convert_image_to_html(img) == "![Logo](pic.png)" def test_convert_image_with_style_size_uses_html(): img = _tag( 'check', "img" ) out = h2m._convert_image_to_html(img) assert ( out.startswith("A', "img") out = h2m._convert_image_to_html(img, base_url="https://x.com/dir/doc.htm") assert out == "![A](https://x.com/dir/img/a.png)" def test_convert_image_numeric_width_attribute(): img = _tag('', "img") out = h2m._convert_image_to_html(img) assert "width:20px" in out and "height:10px" in out # =========================================================================== # _count_data_columns / _expanded_col_count # =========================================================================== def test_count_data_columns_finds_years(): t = _table( "" "
20252024
Rev12
" ) assert h2m._count_data_columns(t) == 2 def test_count_data_columns_none(): t = _table("
Revx
") assert h2m._count_data_columns(t) == 0 def test_expanded_col_count_with_colspan(): t = _table('
AB
') assert h2m._expanded_col_count(t) == 3 # =========================================================================== # _is_continuation_table / _merge_continuation_tables # =========================================================================== def test_is_continuation_table_year_only_start(): t = _table("
2024
1
") assert h2m._is_continuation_table(t) is True def test_is_continuation_table_no_prev_not_year(): t = _table("
Rev$1
") assert h2m._is_continuation_table(t, prev_table=None) is False def test_merge_continuation_tables_appends_rows(): # Headerless continuation: same expanded column count, immediate DOM # siblings, data rows with $ -> second table merged into the first. html = ( "
" "" "
20252024
Rev$1$2
" "
Cost$3$4
" "
" ) soup = BeautifulSoup(html, "html.parser") h2m._merge_continuation_tables(soup) tables = soup.find_all("table") # second table should have been merged into the first and removed assert len(tables) == 1 assert "Cost" in tables[0].get_text() # =========================================================================== # is_header_element / get_header_level # =========================================================================== def test_is_header_element_h_tags(): assert h2m.is_header_element(_tag("

Title

", "h2")) is True def test_is_header_element_body_text_flag_false(): tag = _tag('
Big
', "div") assert h2m.is_header_element(tag) is False def test_is_header_element_div_with_table_false(): tag = _tag( '
x
', "div" ) assert h2m.is_header_element(tag) is False def test_is_header_element_large_font_div(): tag = _tag('
Section Title
', "div") assert h2m.is_header_element(tag) is True def test_is_header_element_bold_only_children(): tag = _tag("

Important Heading

", "p") assert h2m.is_header_element(tag) is True def test_is_header_element_item_bold(): assert h2m.is_header_element(_tag("ITEM 1. Business", "b")) is True def test_is_header_element_part_bold(): assert h2m.is_header_element(_tag("PART II", "b")) is True def test_is_header_element_all_caps_words(): assert h2m.is_header_element(_tag("RISK FACTORS", "b")) is True def test_is_header_element_single_caps_word(): assert h2m.is_header_element(_tag("OVERVIEW", "b")) is True def test_is_header_element_plain_text_false(): assert ( h2m.is_header_element(_tag("some lowercase phrase here", "b")) is False ) def test_get_header_level_part(): assert h2m.get_header_level(_tag("PART I", "b")) == 1 def test_get_header_level_item(): assert h2m.get_header_level(_tag("ITEM 7. MD&A", "b")) == 2 def test_get_header_level_h_tags(): assert h2m.get_header_level(_tag("

x

", "h1")) == 1 assert h2m.get_header_level(_tag("

x

", "h2")) == 2 assert h2m.get_header_level(_tag("

x

", "h3")) == 3 def test_get_header_level_font_size(): assert h2m.get_header_level(_tag('
x
', "div")) == 2 assert h2m.get_header_level(_tag('
x
', "div")) == 3 def test_get_header_level_default(): assert h2m.get_header_level(_tag("

plain

", "p")) == 3 # =========================================================================== # clean_table_cells # =========================================================================== def test_clean_table_cells_dollar_merge(): rows = [["$", "12,211"]] out = h2m.clean_table_cells(rows) assert out[0][0] == "$12,211" def test_clean_table_cells_open_paren_merge(): rows = [["(2,257", ")(b)"]] out = h2m.clean_table_cells(rows) assert out[0][0] == "(2,257)(b)" def test_clean_table_cells_suffix_merge(): rows = [["22", "%"]] out = h2m.clean_table_cells(rows) assert out[0][0] == "22%" def test_clean_table_cells_footnote_note(): rows = [["7", "(a)"]] out = h2m.clean_table_cells(rows) assert out[0][0] == "7 (a)" # =========================================================================== # merge_split_rows # =========================================================================== def test_merge_split_rows_label_continuation(): rows = [ ["Level 3 assets for which we do not", "", ""], ["bear economic exposure (7)", "(14,437)", "(1)"], ] cs = [[], []] merged, _ = h2m.merge_split_rows(rows, cs) assert merged[0][0].startswith("Level 3 assets for which we do not bear economic") assert merged[0][1] == "(14,437)" def test_merge_split_rows_incomplete_paren(): rows = [["(in millions, except per", "", ""], ["share amounts)", "", ""]] cs = [[], []] merged, _ = h2m.merge_split_rows(rows, cs) assert merged[0][0] == "(in millions, except per share amounts)" def test_merge_split_rows_balanced_first_cell_no_merge(): # First cell "(Benefit from) provision" has balanced parens and row_text # does not end with ")" -> the balanced-first-cell branch keeps it as is. # Two non-empty columns avoid the label-continuation merge path. rows = [["(Benefit from) provision for taxes", "100", ""], ["Next Row", "", ""]] cs = [[], []] merged, _ = h2m.merge_split_rows(rows, cs) assert len(merged) == 2 assert merged[0][0] == "(Benefit from) provision for taxes" def test_merge_split_rows_header_with_other_content_no_merge(): rows = [["(descriptor wrapping", "2025", "2024"], ["next", "", ""]] cs = [[], []] merged, _ = h2m.merge_split_rows(rows, cs) # has non-label content -> kept as is assert merged[0] == ["(descriptor wrapping", "2025", "2024"] # =========================================================================== # merge_split_cells # =========================================================================== def test_merge_split_cells_paren_close(): rows = [["(306", ")"]] out = h2m.merge_split_cells(rows) assert out[0][0] == "(306)" def test_merge_split_cells_footnote(): rows = [["2,264", "(1)"]] out = h2m.merge_split_cells(rows) assert out[0][0] == "2,264 (1)" def test_merge_split_cells_percent_suffix(): rows = [["22", "", "%"]] out = h2m.merge_split_cells(rows) assert out[0][0] == "22%" def test_merge_split_cells_passthrough(): rows = [["Revenue", "Other"]] out = h2m.merge_split_cells(rows) assert out == [["Revenue", "Other"]] # =========================================================================== # collapse_repeated_headers # =========================================================================== def test_collapse_repeated_headers_short_input(): assert h2m.collapse_repeated_headers([]) == [] assert h2m.collapse_repeated_headers([["A"]]) == [["A"]] def test_collapse_repeated_headers_duplicate_with_shifted_data(): rows = [ ["Name", "Fiscal Year", "", "Salary", ""], ["Ellison", "", "2025", "", "950,000"], ] out = h2m.collapse_repeated_headers(rows) # the empty colspan-expanded columns should be collapsed away assert len(out[0]) < 5 assert "2025" in out[1] # =========================================================================== # parse_row_semantic # =========================================================================== def test_parse_row_semantic_label_and_values(): label, values = h2m.parse_row_semantic(["Revenue", "$1,000", "$2,000"]) assert label == "Revenue" assert values == ["$1,000", "$2,000"] def test_parse_row_semantic_dash_value(): label, values = h2m.parse_row_semantic(["Net", "—", "$5"]) assert label == "Net" assert "—" in values def test_parse_row_semantic_total_from_currency(): label, values = h2m.parse_row_semantic(["$1,234", "$5,678"]) assert label == "Total" def test_parse_row_semantic_text_values_as_columns(): label, values = h2m.parse_row_semantic( ["Moody's", "P-1", "A1"], num_expected_cols=2 ) assert label == "Moody's" assert values == ["P-1", "A1"] def test_parse_row_semantic_extra_text_no_cols_to_label(): label, values = h2m.parse_row_semantic(["Label", "extra text"]) assert "extra text" in label # =========================================================================== # detect_and_merge_multiindex_headers # =========================================================================== def test_detect_multiindex_too_few_rows(): assert h2m.detect_and_merge_multiindex_headers([["a"], ["b"]]) == (None, 0, 0) def test_detect_multiindex_with_caps_categories(): rows = [ ["For the Years Ended December 31,"], ["EQUIPMENT", "EQUIPMENT", "FINANCIAL", "FINANCIAL"], ["OPERATIONS", "OPERATIONS", "SERVICES", "SERVICES"], ["2025", "2024", "2025", "2024"], ["Revenue", "100", "200", "300", "400"], ] header_rows, data_start, num_cols = h2m.detect_and_merge_multiindex_headers(rows) assert header_rows is not None assert num_cols >= 2 def test_detect_multiindex_no_categories_returns_none(): rows = [ ["title"], ["2025", "2024", "2023"], ["Revenue", "1", "2", "3"], ["Cost", "4", "5", "6"], ] # no all-caps categories -> falls back to None assert h2m.detect_and_merge_multiindex_headers(rows) == (None, 0, 0) # =========================================================================== # is_equity_statement_table / process_equity_statement_table # =========================================================================== def test_is_equity_statement_table_true(): rows = [ ["", "Common Stock", "Treasury Stock", "Retained Earnings"], ["", "Accumulated Comprehensive", "Noncontrolling", "Total Equity"], ["Balance", "$1", "$2", "$3"], ["Net income", "", "", "$4"], ["Balance end", "$5", "$6", "$7"], ] assert h2m.is_equity_statement_table(rows) is True def test_is_equity_statement_table_false(): rows = [["Revenue", "1"], ["Cost", "2"]] assert h2m.is_equity_statement_table(rows) is False def test_process_equity_statement_table_not_applicable(): rows = [["Revenue", "1"], ["Cost", "2"]] cs = [[("Revenue", 1), ("1", 1)], [("Cost", 1), ("2", 1)]] result, count = h2m.process_equity_statement_table(rows, cs) assert result is None and count == 0 # =========================================================================== # get_text_content # =========================================================================== def test_get_text_content_navigable_string(): soup = BeautifulSoup("plain text", "html.parser") node = soup.contents[0] assert h2m.get_text_content(node) == "plain text" def test_get_text_content_joins_split_words(): tag = _tag("

INCOME

", "p") assert h2m.get_text_content(tag) == "INCOME" def test_get_text_content_preserves_space_separator(): tag = _tag("

WORD1 WORD2

", "p") assert h2m.get_text_content(tag) == "WORD1 WORD2" def test_get_text_content_links_preserved(): tag = _tag('

See here

', "p") out = h2m.get_text_content( tag, preserve_links_in_text=True, base_url="https://x.com/" ) assert "[here](https://x.com/x.htm)" in out def test_get_text_content_br_becomes_space(): tag = _tag("

a
b

", "p") assert h2m.get_text_content(tag) == "a b" def test_get_text_content_xbrl_wrapper(): tag = _tag("

42

", "p") assert "42" in h2m.get_text_content(tag) # =========================================================================== # _html_escape / _clean_html_entities # =========================================================================== def test_html_escape(): assert h2m._html_escape("a < b & c > d") == "a < b & c > d" def test_clean_html_entities(): out = h2m._clean_html_entities("a b&c\u200bd\xa0e") assert out == "a b&cd e" # =========================================================================== # _classify_table # =========================================================================== def test_classify_table_no_rows(): t = _table("
") assert h2m._classify_table(t) == "DATA" def test_classify_table_bullet(): t = _table( "" "
•First item
•Second item
" ) assert h2m._classify_table(t) == "BULLET" def test_classify_table_footnote(): t = _table( "" "
(1)This is a footnote.
(2)Another note.
" ) assert h2m._classify_table(t) == "FOOTNOTE" def test_classify_table_header(): t = _table('
Section Title
') assert h2m._classify_table(t) == "HEADER" def test_classify_table_data_default(): t = _table("
Revenue$100$200
") assert h2m._classify_table(t) == "DATA" # =========================================================================== # _extract_bullet_list / _extract_footnote_text / _extract_header_text # =========================================================================== def test_extract_bullet_list(): t = _table( "" "
•Item one here
•Item two here
" ) out = h2m._extract_bullet_list(t) assert "- Item one here" in out and "- Item two here" in out def test_extract_bullet_list_bold_header(): t = _table( "" "
•Topic. Description text follows here.
" ) out = h2m._extract_bullet_list(t) assert "**Topic**" in out def test_extract_bullet_list_single_cell(): t = _table("
• Single bullet line
") out = h2m._extract_bullet_list(t) assert "- Single bullet line" in out def test_extract_footnote_text(): t = _table( "" "
(1)First note.
Standalone line.
" ) out = h2m._extract_footnote_text(t) assert "(1) First note." in out assert "Standalone line." in out def test_extract_header_text_with_anchor(): t = _table('
My Header
') out = h2m._extract_header_text(t) assert '' in out and "**My Header**" in out def test_extract_header_text_plain(): t = _table("
Plain Header
") out = h2m._extract_header_text(t) assert out == "\n**Plain Header**\n" def test_extract_header_text_none(): t = _table("
") assert h2m._extract_header_text(t) is None # =========================================================================== # _extract_cell_text # =========================================================================== def test_extract_cell_text_link(): cell = _tag('
See link12Text ImageADJUST- MENTS
Item A here
Item B here
Item C here
") assert h2m.convert_table(t) == "" def test_convert_table_simple_data(): t = _table( "" "" "" "" "
20252024
Revenue$100$200
Cost$50$75
" ) out = h2m.convert_table(t) assert "|" in out assert "Revenue" in out and "100" in out def test_convert_table_header_classification(): t = _table('
Risk Factors
') out = h2m.convert_table(t) assert "Risk Factors" in out def test_convert_table_bullet_classification(): t = _table( "" "
•Alpha point
•Beta point
" ) out = h2m.convert_table(t) assert "- Alpha point" in out # =========================================================================== # html_to_markdown - top-level entry # =========================================================================== def test_html_to_markdown_empty(): assert h2m.html_to_markdown("") == "" def test_html_to_markdown_bytes_input(): out = h2m.html_to_markdown(b"

Bytes work

") assert "Bytes **work**" in out def test_html_to_markdown_basic_paragraph(): out = h2m.html_to_markdown("

Hello world.

") assert "Hello world." in out def test_html_to_markdown_headers(): out = h2m.html_to_markdown("

Main Title

Sub Title

") assert "# Main Title" in out assert "## Sub Title" in out def test_html_to_markdown_lists(): out = h2m.html_to_markdown("") assert "First" in out and "Second" in out def test_html_to_markdown_table(): html = ( "" "" "" "" "
Item20252024
Revenue$1,000$2,000
Expenses$500$750
" ) out = h2m.html_to_markdown(html) assert "Revenue" in out and "Expenses" in out assert "|" in out def test_html_to_markdown_keep_tables_false(): html = "

Before

xy

After

" out = h2m.html_to_markdown(html, keep_tables=False) assert "Before" in out and "After" in out def test_html_to_markdown_xml_declaration_stripped(): out = h2m.html_to_markdown('

Body

') assert "Body" in out assert "quote“word”

") assert "word" in out # =========================================================================== # extract_periods_from_rows / build_column_headers_from_colspan (direct) # =========================================================================== def test_extract_periods_empty(): assert h2m.extract_periods_from_rows([]) == ([], 0) def test_extract_periods_stacked_categories_over_years(): rows = [ [("", 1), ("EQUIPMENT", 3), ("FINANCIAL", 3)], [("", 1), ("OPERATIONS", 3), ("SERVICES", 3)], [ ("", 1), ("2025", 1), ("2024", 1), ("2023", 1), ("2025", 1), ("2024", 1), ("2023", 1), ], ] layers, count = h2m.extract_periods_from_rows(rows, [True, True, True], None) assert count == 3 flat = " ".join(h for layer in layers for h in layer) assert "EQUIPMENT OPERATIONS" in flat assert "FINANCIAL SERVICES" in flat def test_extract_periods_vertical_merge_split_labels(): # Vertical multi-row header merge: "Retail Notes" / "& Financing" / "Leases" rows = [ [("", 1), ("Retail Notes", 2), ("Revolving", 2)], [("", 1), ("& Financing", 2), ("Charge", 2)], [("", 1), ("Leases", 2), ("Accounts", 2)], [("Total", 1), ("$1", 1), ("", 1), ("$2", 1), ("", 1)], ] layers, count = h2m.extract_periods_from_rows(rows, [True, True, True, False], None) assert layers is not None flat = " ".join(h for layer in layers for h in layer) assert "Retail Notes" in flat def test_build_column_headers_empty(): assert h2m.build_column_headers_from_colspan([], None) == (None, 0) def test_build_column_headers_with_year_range_fragments(): # year-range fragments "2009 -" / "2010" — exercises that detection branch rows = [ [("", 1), ("2009 -", 1), ("2011 -", 1)], [("", 1), ("2010", 1), ("2012", 1)], [("Rev", 1), ("$1", 1), ("$2", 1)], ] layers, count = h2m.build_column_headers_from_colspan(rows, None) # Either returns flat fragments or None; exercising the body is the goal. assert layers is None or isinstance(layers, list) # =========================================================================== # _extract_chart_legend (direct) # =========================================================================== def test_extract_chart_legend_detects_swatches(): html = ( "" '' "" '' "" "
United States
Other Americas
" ) t = _table(html) out = h2m._extract_chart_legend(t) assert out is not None assert "Legend:" in out assert "United States" in out and "Other Americas" in out def test_extract_chart_legend_rejects_data_table(): html = ( "" "" "" "
Revenue$1,234.5
Cost$2,345.6
" ) t = _table(html) assert h2m._extract_chart_legend(t) is None def test_extract_chart_legend_too_many_rows(): rows = "".join(f"r{i}" for i in range(40)) t = _table(f"{rows}
") assert h2m._extract_chart_legend(t) is None def test_extract_chart_legend_no_swatches(): html = "
Alpha
Beta
" t = _table(html) assert h2m._extract_chart_legend(t) is None # =========================================================================== # _split_composite_table / _make_sub_table # =========================================================================== def test_split_composite_table_too_few_rows(): t = _table("
x
") assert h2m._split_composite_table(t) == [t] def test_split_composite_table_no_table_headers(): t = _table( "" "" "" "" "" "
Revenue$1
Cost$2
Net$3
Total$4
" ) # no "TABLE X" headers -> returned unchanged (single element list) assert h2m._split_composite_table(t) == [t] def test_split_composite_table_splits_on_table_headers(): t = _table( "" "" "" "" "" "
TABLE 5: Revenue
Item A$1
TABLE 6: Expenses
Item B$2
" ) parts = h2m._split_composite_table(t) # split into 2+ sub-tables assert len(parts) >= 2 def test_make_sub_table_copies_rows(): src = _table('
a
b
') rows = src.find_all("tr") new_t = h2m._make_sub_table(rows[:1], src) assert new_t.name == "table" assert new_t.get("class") == ["x"] assert len(new_t.find_all("tr")) == 1 # =========================================================================== # _convert_layout_table # =========================================================================== def test_convert_layout_table_no_rows(): t = _table("
") assert h2m._convert_layout_table(t) is None def test_convert_layout_table_data_header_returns_none(): # row with 3+ bold cells -> real data table, return None t = _table( "" "" "
NameYearSalary
A20251
" ) assert h2m._convert_layout_table(t) is None def test_convert_layout_table_header_plus_bullets(): t = _table( "" "" "" "
Our Strategy
• Grow revenue here
" "
• Reduce costs here
" "
• Expand markets here
" ) out = h2m._convert_layout_table(t) assert out is not None assert "Our Strategy" in out assert "- Grow revenue here" in out # =========================================================================== # Post-processing string functions # =========================================================================== def test_merge_consecutive_headers_incomplete(): md = "### Statements of Changes in\n### Stockholders Equity" out = h2m._merge_consecutive_headers(md) assert "### Statements of Changes in Stockholders Equity" in out def test_merge_consecutive_headers_complete_not_merged(): md = "### Balance Sheet\n### Income Statement" out = h2m._merge_consecutive_headers(md) # both complete -> not merged into one assert "### Balance Sheet" in out assert "### Income Statement" in out def test_merge_consecutive_headers_different_level_incomplete(): md = "### Notes to the\n#### Financial Statements" out = h2m._merge_consecutive_headers(md) assert "### Notes to the Financial Statements" in out def test_convert_inline_subheadings(): md = "Overview Section . The body text continues with details here." out = h2m._convert_inline_subheadings(md) assert "#### Overview Section" in out assert "The body text continues" in out def test_convert_inline_subheadings_skips_tables_and_headers(): md = "| cell | data |\n# Already A Header" out = h2m._convert_inline_subheadings(md) assert out == md def test_normalize_bullet_chars_splits_inline(): md = "•item one; •item two; and •item three." out = h2m._normalize_bullet_chars(md) lines = out.split("\n") assert "- item one;" in lines assert "- item two; and" in lines assert "- item three." in lines def test_normalize_bullet_chars_no_bullets_unchanged(): md = "Just a normal line." assert h2m._normalize_bullet_chars(md) == md def test_normalize_bullet_chars_skips_table_row(): md = "| • | x |" assert h2m._normalize_bullet_chars(md) == md def test_join_split_paragraphs_connecting_word(): md = "This sentence ends with and\nacquisitions of new firms." out = h2m._join_split_paragraphs(md) assert "and acquisitions" in out def test_join_split_paragraphs_hyphen_split(): md = "The company reported pre-\ntax income growth." out = h2m._join_split_paragraphs(md) assert "pre-tax" in out or "pretax" in out def test_join_split_paragraphs_unaudited_header_removed(): md = "Some text.\n**(UNAUDITED)**\nMore text here." out = h2m._join_split_paragraphs(md) assert "**(UNAUDITED)**" not in out def test_remove_repeated_page_elements(): footer = "Company Name Page 5" lines = ["# Title"] for i in range(8): lines.append(f"Paragraph {i} content goes here for variety.") lines.append(footer) md = "\n".join(lines) out = h2m._remove_repeated_page_elements(md) # the repeated footer (normalizes the same each time) should be removed assert "Company Name Page" not in out def test_remove_repeated_page_elements_no_repeats(): md = "# Title\nUnique line one.\nUnique line two." assert h2m._remove_repeated_page_elements(md) == md # =========================================================================== # _reflow_absolute_layout (Certent CDM style position:absolute layouts) # =========================================================================== def _abs_frag(idx, left, top, text, *, fs=10, bold=False): """Build a Certent-CDM-style absolutely-positioned text fragment div.""" weight = "font-weight:bold;" if bold else "" return ( f'
{text}
' ) def _abs_rule(left, top, width): """Build a thin full-width position:absolute horizontal rule div.""" return ( f'
' ) def _build_abs_layout(page_num=1, idx_start=1): """Construct a single-page absolute-positioned filing (>= 30 abs divs).""" parts = [f'
'] idx = idx_start parts.append( _abs_frag(idx, 50, 20, "CONSOLIDATED RESULTS OF OPERATIONS", fs=14, bold=True) ) idx += 1 for k in range(10): parts.append( _abs_frag( idx, 50, 50 + k * 16, f"Body sentence {k} describing financial performance across segments.", ) ) idx += 1 parts.append(_abs_rule(50, 230, 600)) parts.append(_abs_frag(idx, 50, 240, "Item")) idx += 1 parts.append(_abs_frag(idx, 300, 240, "2025")) idx += 1 parts.append(_abs_frag(idx, 450, 240, "2024")) idx += 1 parts.append(_abs_rule(50, 260, 600)) data = [ ("Revenue", "100", "90"), ("Cost", "40", "35"), ("Net income", "60", "55"), ("Assets", "500", "480"), ("Liabilities", "200", "190"), ] for i, (lbl, a, b) in enumerate(data): y = 270 + i * 18 parts.append(_abs_frag(idx, 50, y, lbl)) idx += 1 parts.append(_abs_frag(idx, 300, y, a)) idx += 1 parts.append(_abs_frag(idx, 450, y, b)) idx += 1 parts.append(_abs_rule(50, 370, 600)) parts.append("
") return "".join(parts), idx def test_reflow_absolute_layout_basic(): html, _ = _build_abs_layout() out = h2m._reflow_absolute_layout(html) assert out is not None assert "CONSOLIDATED RESULTS OF OPERATIONS" in out assert "" in out assert "Revenue" in out and "Liabilities" in out def test_reflow_absolute_layout_rejects_normal_html(): # Few absolute elements -> not an abs-positioned document. assert h2m._reflow_absolute_layout("

Normal document content.

") is None def test_reflow_absolute_layout_rejects_with_tables(): # Has the abs fragments but also > 2 tables -> rejected early. html, _ = _build_abs_layout() html += "
x
" * 3 assert h2m._reflow_absolute_layout(html) is None def test_reflow_absolute_layout_no_text_fragments(): # 30+ absolute rules but NO id="aNN" text fragments -> rejected. rules = "".join(_abs_rule(50, 20 + i * 10, 600) for i in range(40)) html = f'
{rules}
' assert h2m._reflow_absolute_layout(html) is None def test_reflow_absolute_layout_multipage(): p1, idx = _build_abs_layout(page_num=1, idx_start=1) p2, _ = _build_abs_layout(page_num=2, idx_start=idx) html = p1 + p2 out = h2m._reflow_absolute_layout(html) assert out is not None # both pages' content present assert out.count("CONSOLIDATED RESULTS OF OPERATIONS") >= 1 def test_html_to_markdown_drives_reflow_end_to_end(): html, _ = _build_abs_layout() out = h2m.html_to_markdown(html) # The reflowed HTML should be converted to markdown with a table. assert "CONSOLIDATED RESULTS OF OPERATIONS" in out assert "Revenue" in out assert "|" in out def _build_abs_layout_rich(page_num=1, idx_start=1): """Abs-positioned page exercising H2 / heading / bullet / chart paths.""" parts = [f'
'] idx = idx_start # Large-font H2 heading (font-size > 18 -> classified as body heading). parts.append(_abs_frag(idx, 50, 20, "NET INCOME OVERVIEW", fs=22)) idx += 1 # Left-margin body paragraph lines (body font). for k in range(6): parts.append( _abs_frag( idx, 50, 50 + k * 16, f"This paragraph line {k} describes results and was prepared by " "management today.", ) ) idx += 1 # Short section heading (left<=55, len<50) directly above a bullet list. parts.append(_abs_frag(idx, 50, 150, "Key Drivers")) idx += 1 # Bullet lines: bullet glyph fragment + continuation fragment per row. for k in range(3): parts.append(_abs_frag(idx, 50, 172 + k * 16, "•")) idx += 1 parts.append( _abs_frag( idx, 70, 172 + k * 16, f"Bullet point number {k} explaining a driver of performance here.", ) ) idx += 1 # Chart annotation block (right column, bold title with deviating font + desc). parts.append(_abs_frag(idx, 320, 60, "Revenue by Segment", fs=12, bold=True)) idx += 1 parts.append( _abs_frag( idx, 320, 76, "(in millions of dollars for the fiscal period reported)", fs=12, ) ) idx += 1 # Table zone delimited by thin full-width rules. parts.append(_abs_rule(50, 240, 600)) parts.append(_abs_frag(idx, 50, 250, "Item")) idx += 1 parts.append(_abs_frag(idx, 300, 250, "2025")) idx += 1 parts.append(_abs_frag(idx, 450, 250, "2024")) idx += 1 parts.append(_abs_rule(50, 270, 600)) for i, (lbl, a, b) in enumerate( [("Revenue", "100", "90"), ("Cost", "40", "35"), ("Net income", "60", "55")] ): y = 280 + i * 18 parts.append(_abs_frag(idx, 50, y, lbl)) idx += 1 parts.append(_abs_frag(idx, 300, y, a)) idx += 1 parts.append(_abs_frag(idx, 450, y, b)) idx += 1 parts.append(_abs_rule(50, 340, 600)) parts.append("
") return "".join(parts), idx def test_reflow_absolute_layout_rich_headings_bullets_chart(): html, _ = _build_abs_layout_rich() out = h2m._reflow_absolute_layout(html) assert out is not None # Large-font heading -> H2. assert "

NET INCOME OVERVIEW

" in out # Bullet list emitted. assert "