From 81c839ab5a20add942fbba37a20afdec73e5e3c3 Mon Sep 17 00:00:00 2001 From: Maciej Pienczyn Date: Mon, 16 Mar 2026 22:45:28 +0100 Subject: [PATCH] feat: format founding_history into structured HTML sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- app.py | 4 + templates/company_detail.html | 82 +++++++++++++++- utils/history_formatter.py | 170 ++++++++++++++++++++++++++++++++++ 3 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 utils/history_formatter.py diff --git a/app.py b/app.py index c240f07..52a3090 100644 --- a/app.py +++ b/app.py @@ -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 diff --git a/templates/company_detail.html b/templates/company_detail.html index 17be109..5339a00 100755 --- a/templates/company_detail.html +++ b/templates/company_detail.html @@ -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; + } {% endblock %} @@ -1241,7 +1321,7 @@

Historia i doświadczenie

-
{{ company.founding_history | safe }}
+
{{ company.founding_history | format_history }}
diff --git a/utils/history_formatter.py b/utils/history_formatter.py new file mode 100644 index 0000000..6dfcaf9 --- /dev/null +++ b/utils/history_formatter.py @@ -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 '

' in text or '

' in text or '
' 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
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('• ', '
  • ') + if '') + in_list = False + continue + if '') + in_list = True + formatted.append(line + '
  • ') + else: + if in_list: + formatted.append('') + in_list = False + formatted.append(f'

    {line}

    ') + if in_list: + formatted.append('') + return '\n'.join(formatted) + + return str(escaped).replace('\n', '
    ') + + +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 = ['
    '] + + 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'
    ') + html_parts.append( + f'
    ' + f'{emoji}' + f'{escape(title)}' + f'
    ' + ) + + 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('
      ') + 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'
    • {content}
    • ') + html_parts.append('
    ') + + for nl in non_bullet: + content = escape(nl) + html_parts.append(f'

    {content}

    ') + + html_parts.append('
    ') + + html_parts.append('
    ') + 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'\1', text) + return text + + +def register_history_filter(app): + """Register the Jinja2 filter.""" + app.jinja_env.filters['format_history'] = format_founding_history