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 @@
' 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''
+ )
+
+ 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