Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1886,6 +1886,191 @@ def _normalize_checks_paid_table(grid):
|
|
| 1886 |
return new_rows
|
| 1887 |
|
| 1888 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1889 |
def normalize_html_tables(text: str) -> str:
|
| 1890 |
"""
|
| 1891 |
For every <table>...</table>:
|
|
@@ -1978,6 +2163,7 @@ def stabilize_tables_and_text(page_md: str) -> str:
|
|
| 1978 |
out_blocks.append(b)
|
| 1979 |
|
| 1980 |
stabilized = "\n\n".join(out_blocks)
|
|
|
|
| 1981 |
stabilized = normalize_html_tables(stabilized)
|
| 1982 |
return close_unclosed_html(stabilized)
|
| 1983 |
|
|
|
|
| 1886 |
return new_rows
|
| 1887 |
|
| 1888 |
|
| 1889 |
+
# ββ Johnson Bank plain-text section converter βββββββββββββββββββββββββββββ
|
| 1890 |
+
# Johnson Bank formats Deposits, Withdrawals, Checks, and Daily Account Balance
|
| 1891 |
+
# as plain text columns (no HTML tables). This converter detects those sections
|
| 1892 |
+
# and emits proper <table> HTML.
|
| 1893 |
+
#
|
| 1894 |
+
# SAFETY GUARDS β all four must hold before any conversion fires:
|
| 1895 |
+
# 1. Line matches exact section-header pattern
|
| 1896 |
+
# 2. Next non-blank line matches exact column-header pattern
|
| 1897 |
+
# 3. First data row uses MM-DD date format (Johnson Bank's unique date separator)
|
| 1898 |
+
# 4. The candidate section contains NO existing <table> tags
|
| 1899 |
+
# Guard 3 is the key discriminator: all other supported banks use MM/DD.
|
| 1900 |
+
|
| 1901 |
+
_JB_SECTION_HDR = re.compile(
|
| 1902 |
+
r"^(Deposits?|Withdrawals?|Checks?(?:\s+Paid)?|Daily\s+Account\s+Balance)"
|
| 1903 |
+
r"\s*(?:\(cont\.?\))?\s*$",
|
| 1904 |
+
re.IGNORECASE,
|
| 1905 |
+
)
|
| 1906 |
+
_JB_COL_HDR = re.compile(
|
| 1907 |
+
r"^Date\s+(Description|Number)\s+Amount\s*$"
|
| 1908 |
+
r"|^Date\s+(?:Number\s+)?(?:Balance|Amount)\s*$",
|
| 1909 |
+
re.IGNORECASE,
|
| 1910 |
+
)
|
| 1911 |
+
# MM-DD date (dash separator) β Johnson Bank's unique format
|
| 1912 |
+
_JB_DATE_DD = re.compile(r"^\d{2}-\d{2}\s+")
|
| 1913 |
+
_JB_TXN_ROW = re.compile(r"^(\d{2}-\d{2})\s+(.+?)\s+(-?\d{1,3}(?:,\d{3})*\.\d{2})\s*$")
|
| 1914 |
+
_JB_BAL_ROW = re.compile(r"^(\d{2}-\d{2})\s+(\d{1,3}(?:,\d{3})*\.\d{2})\s*$")
|
| 1915 |
+
|
| 1916 |
+
|
| 1917 |
+
def _jb_rows_to_html(header_cols, rows):
|
| 1918 |
+
th = "".join("<th>" + h + "</th>" for h in header_cols)
|
| 1919 |
+
parts = ["<table>", "<tr>" + th + "</tr>"]
|
| 1920 |
+
for row in rows:
|
| 1921 |
+
td = "".join("<td>" + str(row.get(h, "")) + "</td>" for h in header_cols)
|
| 1922 |
+
parts.append("<tr>" + td + "</tr>")
|
| 1923 |
+
parts.append("</table>")
|
| 1924 |
+
return "\n".join(parts)
|
| 1925 |
+
|
| 1926 |
+
|
| 1927 |
+
def _jb_section_has_table(lines, start, end):
|
| 1928 |
+
"""Return True if any line in lines[start:end] contains a <table tag."""
|
| 1929 |
+
return any("<table" in l.lower() for l in lines[start:end])
|
| 1930 |
+
|
| 1931 |
+
|
| 1932 |
+
def _find_first_data_row(lines, start):
|
| 1933 |
+
"""Return index of first non-blank line after start, or None."""
|
| 1934 |
+
for i in range(start, min(start + 20, len(lines))):
|
| 1935 |
+
if lines[i].strip():
|
| 1936 |
+
return i
|
| 1937 |
+
return None
|
| 1938 |
+
|
| 1939 |
+
|
| 1940 |
+
def convert_plaintext_bank_sections(text: str) -> str:
|
| 1941 |
+
"""
|
| 1942 |
+
Convert Johnson Bank plain-text transaction sections to HTML tables.
|
| 1943 |
+
All four safety guards must pass before conversion fires.
|
| 1944 |
+
Safe no-op for all other banks.
|
| 1945 |
+
"""
|
| 1946 |
+
# Quick exit: if the page already has lots of tables, skip entirely
|
| 1947 |
+
# (handles pages that are already properly formatted)
|
| 1948 |
+
lines = text.splitlines()
|
| 1949 |
+
out = []
|
| 1950 |
+
i = 0
|
| 1951 |
+
|
| 1952 |
+
while i < len(lines):
|
| 1953 |
+
line = lines[i].strip()
|
| 1954 |
+
|
| 1955 |
+
# Guard 1: section header
|
| 1956 |
+
if not _JB_SECTION_HDR.match(line):
|
| 1957 |
+
out.append(lines[i])
|
| 1958 |
+
i += 1
|
| 1959 |
+
continue
|
| 1960 |
+
|
| 1961 |
+
section_title = line
|
| 1962 |
+
j = i + 1
|
| 1963 |
+
|
| 1964 |
+
# Skip blank lines to find column header
|
| 1965 |
+
while j < len(lines) and not lines[j].strip():
|
| 1966 |
+
j += 1
|
| 1967 |
+
|
| 1968 |
+
# Guard 2: column header
|
| 1969 |
+
if j >= len(lines) or not _JB_COL_HDR.match(lines[j].strip()):
|
| 1970 |
+
out.append(lines[i])
|
| 1971 |
+
i += 1
|
| 1972 |
+
continue
|
| 1973 |
+
|
| 1974 |
+
col_hdr_line = lines[j].strip()
|
| 1975 |
+
data_start = j + 1
|
| 1976 |
+
|
| 1977 |
+
# Find first non-blank data line
|
| 1978 |
+
first_data_idx = _find_first_data_row(lines, data_start)
|
| 1979 |
+
|
| 1980 |
+
# Guard 3: first data row must use MM-DD date format
|
| 1981 |
+
if first_data_idx is None or not _JB_DATE_DD.match(lines[first_data_idx].strip()):
|
| 1982 |
+
out.append(lines[i])
|
| 1983 |
+
i += 1
|
| 1984 |
+
continue
|
| 1985 |
+
|
| 1986 |
+
# Find section end (next section header, page separator, or end of text)
|
| 1987 |
+
section_end = len(lines)
|
| 1988 |
+
for k in range(data_start, len(lines)):
|
| 1989 |
+
l = lines[k].strip()
|
| 1990 |
+
if (l != section_title and _JB_SECTION_HDR.match(l)
|
| 1991 |
+
or l.startswith("---page-separator")
|
| 1992 |
+
or l.startswith("Page:")):
|
| 1993 |
+
section_end = k
|
| 1994 |
+
break
|
| 1995 |
+
|
| 1996 |
+
# Guard 4: no existing <table> tags in this section
|
| 1997 |
+
if _jb_section_has_table(lines, i, section_end):
|
| 1998 |
+
out.append(lines[i])
|
| 1999 |
+
i += 1
|
| 2000 |
+
continue
|
| 2001 |
+
|
| 2002 |
+
# All guards passed β parse and convert
|
| 2003 |
+
parts_upper = col_hdr_line.upper().split()
|
| 2004 |
+
if len(parts_upper) == 2 and parts_upper[1] == "BALANCE":
|
| 2005 |
+
header_cols = ["Date", "Balance"]
|
| 2006 |
+
elif "NUMBER" in parts_upper:
|
| 2007 |
+
header_cols = ["Date", "Number", "Amount"]
|
| 2008 |
+
else:
|
| 2009 |
+
header_cols = ["Date", "Description", "Amount"]
|
| 2010 |
+
|
| 2011 |
+
out.append(section_title)
|
| 2012 |
+
rows = []
|
| 2013 |
+
current = None
|
| 2014 |
+
k = data_start
|
| 2015 |
+
|
| 2016 |
+
while k < section_end:
|
| 2017 |
+
raw = lines[k]
|
| 2018 |
+
row_line = raw.strip()
|
| 2019 |
+
|
| 2020 |
+
if not row_line:
|
| 2021 |
+
k += 1
|
| 2022 |
+
continue
|
| 2023 |
+
|
| 2024 |
+
if (_JB_SECTION_HDR.match(row_line)
|
| 2025 |
+
or row_line.startswith("---page-separator")
|
| 2026 |
+
or row_line.startswith("Page:")):
|
| 2027 |
+
break
|
| 2028 |
+
|
| 2029 |
+
# Balance-only row
|
| 2030 |
+
mb = _JB_BAL_ROW.match(row_line)
|
| 2031 |
+
if mb and header_cols == ["Date", "Balance"]:
|
| 2032 |
+
if current:
|
| 2033 |
+
rows.append(current)
|
| 2034 |
+
current = {"Date": mb.group(1), "Balance": mb.group(2)}
|
| 2035 |
+
k += 1
|
| 2036 |
+
continue
|
| 2037 |
+
|
| 2038 |
+
# Full transaction row
|
| 2039 |
+
mt = _JB_TXN_ROW.match(row_line)
|
| 2040 |
+
if mt:
|
| 2041 |
+
if current:
|
| 2042 |
+
rows.append(current)
|
| 2043 |
+
desc_key = "Description" if "Description" in header_cols else "Number"
|
| 2044 |
+
current = {
|
| 2045 |
+
"Date": mt.group(1),
|
| 2046 |
+
desc_key: mt.group(2).strip(),
|
| 2047 |
+
"Amount": mt.group(3),
|
| 2048 |
+
}
|
| 2049 |
+
k += 1
|
| 2050 |
+
continue
|
| 2051 |
+
|
| 2052 |
+
# Continuation line
|
| 2053 |
+
if current and row_line:
|
| 2054 |
+
desc_key = "Description" if "Description" in header_cols else "Number"
|
| 2055 |
+
if desc_key in current:
|
| 2056 |
+
current[desc_key] += " " + row_line
|
| 2057 |
+
k += 1
|
| 2058 |
+
continue
|
| 2059 |
+
|
| 2060 |
+
k += 1
|
| 2061 |
+
|
| 2062 |
+
if current:
|
| 2063 |
+
rows.append(current)
|
| 2064 |
+
|
| 2065 |
+
if rows:
|
| 2066 |
+
out.append(_jb_rows_to_html(header_cols, rows))
|
| 2067 |
+
|
| 2068 |
+
i = section_end # jump past the whole section
|
| 2069 |
+
continue
|
| 2070 |
+
|
| 2071 |
+
return "\n".join(out)
|
| 2072 |
+
|
| 2073 |
+
|
| 2074 |
def normalize_html_tables(text: str) -> str:
|
| 2075 |
"""
|
| 2076 |
For every <table>...</table>:
|
|
|
|
| 2163 |
out_blocks.append(b)
|
| 2164 |
|
| 2165 |
stabilized = "\n\n".join(out_blocks)
|
| 2166 |
+
stabilized = convert_plaintext_bank_sections(stabilized)
|
| 2167 |
stabilized = normalize_html_tables(stabilized)
|
| 2168 |
return close_unclosed_html(stabilized)
|
| 2169 |
|