feat: format founding_history into structured HTML sections
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

Parses emoji-sectioned text (ZARZĄD, WSPÓLNICY, DANE REJESTROWE,
DANE FINANSOWE, PROFIL) into card-based layout with icons, lists,
and highlighted key-value pairs. Plain text gets newline conversion.
HTML from Quill editor passes through unchanged.

Affects 45 companies with emoji format, 63 with plain text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-16 22:45:28 +01:00
parent df6ef48f5f
commit 81c839ab5a
3 changed files with 255 additions and 1 deletions

4
app.py
View File

@ -229,6 +229,10 @@ def ensure_url_filter(url):
from utils.markdown import register_markdown_filter
register_markdown_filter(app)
# Register founding history formatter
from utils.history_formatter import register_history_filter
register_history_filter(app)
# Initialize extensions from centralized extensions.py
from extensions import csrf, limiter, login_manager

View File

@ -849,6 +849,86 @@
grid-template-columns: 1fr !important;
}
}
/* Founding history sections */
.founding-history-content {
line-height: 1.6;
color: var(--text-secondary);
}
.history-sections {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.history-section {
padding: var(--spacing-md);
border-radius: var(--radius);
background: var(--background);
border: 1px solid var(--border);
}
.history-section-header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-sm);
}
.history-section-icon {
width: 28px;
height: 28px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
}
.history-section-title {
font-weight: 600;
font-size: var(--font-size-sm);
color: var(--text-primary);
}
.history-list {
margin: 0;
padding-left: 1.2rem;
list-style: none;
}
.history-list li {
position: relative;
padding: 3px 0;
font-size: var(--font-size-sm);
color: var(--text-secondary);
line-height: 1.5;
}
.history-list li::before {
content: '';
position: absolute;
left: -1rem;
top: 11px;
width: 5px;
height: 5px;
border-radius: 50%;
background: #d1d5db;
}
.history-list li strong {
color: var(--text-primary);
font-weight: 500;
}
.history-text {
margin: var(--spacing-xs) 0 0;
font-size: var(--font-size-sm);
color: var(--text-secondary);
line-height: 1.6;
}
</style>
{% endblock %}
@ -1241,7 +1321,7 @@
</div>
<div style="flex: 1;">
<h3 style="font-size: var(--font-size-base); font-weight: 600; color: var(--text-primary); margin: 0 0 var(--spacing-sm);">Historia i doświadczenie</h3>
<div style="line-height: 1.8; color: var(--text-secondary);">{{ company.founding_history | safe }}</div>
<div class="founding-history-content">{{ company.founding_history | format_history }}</div>
</div>
</div>
</div>

170
utils/history_formatter.py Normal file
View File

@ -0,0 +1,170 @@
"""
Founding History Formatter
==========================
Converts raw founding_history text (with emoji section markers)
into structured HTML cards. Handles three formats:
1. Emoji-sectioned text (from KRS/AI enrichment)
2. Plain text with newlines
3. HTML (from Quill editor) - passed through unchanged
"""
import re
from markupsafe import Markup, escape
# Section markers: emoji → (css_class, icon_color_gradient)
SECTION_MAP = {
'🏢': ('section-board', '#1e3050', '#2E4872'),
'👥': ('section-shareholders', '#7c3aed', '#6d28d9'),
'📋': ('section-registry', '#0369a1', '#0284c7'),
'📊': ('section-finance', '#059669', '#10b981'),
'📝': ('section-profile', '#d97706', '#f59e0b'),
}
EMOJI_PATTERN = re.compile(r'^(' + '|'.join(re.escape(e) for e in SECTION_MAP) + r')\s*(.+)$')
def format_founding_history(text):
"""Convert founding_history to structured HTML."""
if not text:
return ''
text = text.strip()
# Already HTML (from Quill editor) — pass through
if '<p>' in text or '<div>' in text or '<br>' in text:
return Markup(text)
# Check if it has emoji section markers
has_sections = any(emoji in text for emoji in SECTION_MAP)
if not has_sections:
# Plain text — just convert newlines to <br> and bullet points
return Markup(_format_plain_text(text))
# Parse emoji-sectioned text
return Markup(_format_sectioned_text(text))
def _format_plain_text(text):
"""Format plain text with newlines and bullet points."""
escaped = escape(text)
# Convert bullet points
result = str(escaped).replace('', '<li style="margin-bottom: 4px;">')
if '<li' in result:
lines = result.split('\n')
formatted = []
in_list = False
for line in lines:
line = line.strip()
if not line:
if in_list:
formatted.append('</ul>')
in_list = False
continue
if '<li' in line:
if not in_list:
formatted.append('<ul style="margin: 0.5rem 0; padding-left: 1.2rem;">')
in_list = True
formatted.append(line + '</li>')
else:
if in_list:
formatted.append('</ul>')
in_list = False
formatted.append(f'<p style="margin: 0.25rem 0;">{line}</p>')
if in_list:
formatted.append('</ul>')
return '\n'.join(formatted)
return str(escaped).replace('\n', '<br>')
def _format_sectioned_text(text):
"""Parse emoji-sectioned text into card-based HTML."""
sections = []
current_emoji = None
current_title = None
current_lines = []
for line in text.split('\n'):
line = line.strip()
if not line:
continue
match = EMOJI_PATTERN.match(line)
if match:
# Save previous section
if current_emoji:
sections.append((current_emoji, current_title, current_lines))
current_emoji = match.group(1)
# Clean title: remove trailing colon, normalize case
title = match.group(2).rstrip(':')
current_title = title
current_lines = []
else:
current_lines.append(line)
# Save last section
if current_emoji:
sections.append((current_emoji, current_title, current_lines))
if not sections:
return _format_plain_text(text)
html_parts = ['<div class="history-sections">']
for emoji, title, lines in sections:
css_class = SECTION_MAP.get(emoji, ('section-default', '#6b7280', '#9ca3af'))[0]
color1 = SECTION_MAP.get(emoji, ('', '#6b7280', '#9ca3af'))[1]
color2 = SECTION_MAP.get(emoji, ('', '#6b7280', '#9ca3af'))[2]
html_parts.append(f'<div class="history-section {css_class}">')
html_parts.append(
f'<div class="history-section-header">'
f'<span class="history-section-icon" style="background: linear-gradient(135deg, {color1}, {color2});">{emoji}</span>'
f'<span class="history-section-title">{escape(title)}</span>'
f'</div>'
)
if lines:
# Check if lines are bullet points
bullet_lines = [l for l in lines if l.startswith('')]
non_bullet = [l for l in lines if not l.startswith('')]
if bullet_lines:
html_parts.append('<ul class="history-list">')
for bl in bullet_lines:
content = escape(bl[2:]) # Remove "• "
# Highlight key-value pairs (e.g., "KRS: 123")
content = _highlight_kv(str(content))
html_parts.append(f'<li>{content}</li>')
html_parts.append('</ul>')
for nl in non_bullet:
content = escape(nl)
html_parts.append(f'<p class="history-text">{content}</p>')
html_parts.append('</div>')
html_parts.append('</div>')
return '\n'.join(html_parts)
def _highlight_kv(text):
"""Highlight key-value pairs like 'KRS: 0000328525' with bold keys."""
# Match patterns like "Key: value" but only for known keys
known_keys = [
'KRS', 'NIP', 'REGON', 'EBITDA', 'EBIT', 'Data rejestracji',
'Kapitał zakładowy', 'Siedziba', 'Reprezentacja',
'Wiarygodność płatnicza', 'Działalność'
]
for key in known_keys:
pattern = re.compile(rf'({re.escape(key)}:\s*)')
text = pattern.sub(rf'<strong>\1</strong>', text)
return text
def register_history_filter(app):
"""Register the Jinja2 filter."""
app.jinja_env.filters['format_history'] = format_founding_history