"""
Renderer de documentation Markdown vers HTML avec styles TailwindCSS.
Convertit le fichier help.md en HTML pour la page d'aide du dashboard.
"""
import re
from pathlib import Path
from typing import List, Tuple, Optional
class HelpMarkdownRenderer:
"""Convertit le Markdown personnalisé en HTML avec styles TailwindCSS."""
def __init__(self, markdown_content: str):
self.content = markdown_content
self.toc_items: List[Tuple[str, str, str]] = [] # (id, emoji, title)
def render(self) -> Tuple[str, str]:
"""
Rend le Markdown en HTML.
Returns:
Tuple (html_content, toc_html)
"""
html = self.content
# Extraire et traiter les sections avec IDs pour la TOC
html = self._process_sections(html)
# Traiter les blocs personnalisés
html = self._process_quickstart_cards(html)
html = self._process_health_levels(html)
html = self._process_score_factors(html)
html = self._process_status_badges(html)
html = self._process_accordions(html)
html = self._process_playbooks(html)
html = self._process_troubleshoot(html)
# Traiter le Markdown standard
html = self._process_blockquotes(html)
html = self._process_tables(html)
html = self._process_code_blocks(html)
html = self._process_inline_code(html)
html = self._process_lists(html)
html = self._process_headings(html)
html = self._process_paragraphs(html)
html = self._process_bold_italic(html)
html = self._process_horizontal_rules(html)
# Générer la TOC
toc_html = self._generate_toc()
return html, toc_html
def _process_sections(self, html: str) -> str:
"""Extrait les sections H2 avec IDs pour la TOC."""
pattern = r'^## ([^\n{]+)\s*\{#([^}]+)\}'
def replace_section(match):
title = match.group(1).strip()
section_id = match.group(2).strip()
# Extraire l'emoji du titre
emoji_match = re.match(r'^(\S+)\s+(.+)$', title)
if emoji_match:
emoji = emoji_match.group(1)
text = emoji_match.group(2)
else:
emoji = ""
text = title
self.toc_items.append((section_id, emoji, text))
return f'
'
return html
def _process_quickstart_cards(self, html: str) -> str:
"""Traite les cartes de démarrage rapide."""
# Trouver le bloc quickstart-cards
pattern = r'(.*?)'
def process_cards(match):
cards_content = match.group(1)
# Parser chaque carte
card_pattern = r':::card\s+(\w+)\s+(fa-\d+)\s*\n###\s+([^\n]+)\n([^:]+):::'
cards = re.findall(card_pattern, cards_content, re.DOTALL)
cards_html = '
'
for color, icon, title, description in cards:
color_class = f"text-{color}-400"
desc = self._process_inline_code(description.strip())
cards_html += f'''
'''
cards_html += '
'
return cards_html
return re.sub(pattern, process_cards, html, flags=re.DOTALL)
def _process_health_levels(self, html: str) -> str:
"""Traite les indicateurs de niveau de santé."""
pattern = r':::health-level\s+(\w+)\s+(\d+)\s+(\w+)\s*\n\*\*([^*]+)\*\*\s*\(([^)]+)\)\s*\n([^:]+):::'
def replace_health(match):
level = match.group(1)
bars = int(match.group(2))
color = match.group(3)
title = match.group(4)
subtitle = match.group(5)
description = match.group(6).strip()
bars_html = ''
for i in range(5):
if i < bars:
bars_html += f'
'
else:
bars_html += '
'
return f'''
{bars_html}
{title}
({subtitle})
{description}
'''
return re.sub(pattern, replace_health, html, flags=re.DOTALL)
def _process_score_factors(self, html: str) -> str:
"""Traite les facteurs de score."""
pattern = r':::score-factor\s+(fa-[\w-]+)\s+(\w+)\s+([^\n]+)\n\*\*([^*]+)\*\*\s*\n([^:]+):::'
def replace_factor(match):
icon = match.group(1)
color = match.group(2)
points = match.group(3).strip()
title = match.group(4)
description = match.group(5).strip()
return f'''
{title}
{points}
{description}
'''
return re.sub(pattern, replace_factor, html, flags=re.DOTALL)
def _process_status_badges(self, html: str) -> str:
"""Traite les badges de statut."""
pattern = r':::status-badge\s+(\w+)\s+(fa-[\w-]+)\s+([^\n]+)\n([^:]+):::'
def replace_badge(match):
color = match.group(1)
icon = match.group(2)
badge_text = match.group(3).strip()
description = match.group(4).strip()
description = self._process_inline_code(description)
return f'''
{badge_text}
{description}
'''
return re.sub(pattern, replace_badge, html, flags=re.DOTALL)
def _process_accordions(self, html: str) -> str:
"""Traite les accordéons."""
pattern = r':::accordion\s+(fa-[\w-]+)\s+(\w+)\s+([^\n]+)\n(.*?):::'
def replace_accordion(match):
icon = match.group(1)
color = match.group(2)
title = match.group(3).strip()
content = match.group(4).strip()
# Traiter les listes dans le contenu
content = self._process_lists(content)
content = self._process_bold_italic(content)
# Séparer description et liste
parts = content.split('\n\n', 1)
description = parts[0] if parts else ''
list_content = parts[1] if len(parts) > 1 else ''
return f'''
{description}
{list_content}
'''
return re.sub(pattern, replace_accordion, html, flags=re.DOTALL)
def _process_playbooks(self, html: str) -> str:
"""Traite les cartes de playbooks."""
pattern = r':::playbook\s+(fa-[\w-]+)\s+(\w+)\s+([^\n]+)\n([^*]+)\*\*([^*]+)\*\*\s*:::'
def replace_playbook(match):
icon = match.group(1)
color = match.group(2)
filename = match.group(3).strip()
description = match.group(4).strip()
note = match.group(5).strip()
# Déterminer la couleur de la note
note_color = "purple"
if "redémarrage" in note.lower() or "attention" in note.lower():
note_color = "orange"
return f'''
{filename}
{description}
{note}
'''
return re.sub(pattern, replace_playbook, html, flags=re.DOTALL)
def _process_troubleshoot(self, html: str) -> str:
"""Traite les sections de dépannage."""
pattern = r':::troubleshoot\s+([^\n]+)\n(.*?):::'
def replace_troubleshoot(match):
title = match.group(1).strip()
content = match.group(2).strip()
# Traiter le contenu
content = self._process_bold_italic(content)
content = self._process_inline_code(content)
content = content.replace('\n\n', '
')
return f'''
'''
return re.sub(pattern, replace_troubleshoot, html, flags=re.DOTALL)
def _process_blockquotes(self, html: str) -> str:
"""Traite les citations (blockquotes)."""
pattern = r'^>\s*\*\*([^:]+):\*\*\s*(.+)$'
def replace_quote(match):
label = match.group(1)
content = match.group(2).strip()
content = self._process_inline_code(content)
return f'''
'''
return re.sub(pattern, replace_quote, html, flags=re.MULTILINE)
def _process_tables(self, html: str) -> str:
"""Traite les tableaux Markdown."""
# Trouver les tableaux
table_pattern = r'(\|[^\n]+\|\n)+(\|[-:| ]+\|\n)(\|[^\n]+\|\n)+'
def replace_table(match):
table_text = match.group(0)
lines = [l.strip() for l in table_text.strip().split('\n') if l.strip()]
if len(lines) < 3:
return table_text
# En-têtes
headers = [h.strip() for h in lines[0].split('|') if h.strip()]
# Ignorer la ligne de séparation (lines[1])
# Données
rows = []
for line in lines[2:]:
cells = [c.strip() for c in line.split('|') if c.strip()]
rows.append(cells)
# Construire le HTML
html_table = '
'
html_table += ''
for h in headers:
html_table += f'| {h} | '
html_table += '
'
html_table += ''
for row in rows:
html_table += ''
for cell in row:
cell = self._process_inline_code(cell)
html_table += f'| {cell} | '
html_table += '
'
html_table += '
'
return html_table
return re.sub(table_pattern, replace_table, html)
def _process_code_blocks(self, html: str) -> str:
"""Traite les blocs de code."""
pattern = r'```(\w*)\n(.*?)```'
def replace_code(match):
lang = match.group(1) or ''
code = match.group(2)
return f'
'
return re.sub(pattern, replace_code, html, flags=re.DOTALL)
def _process_inline_code(self, html: str) -> str:
"""Traite le code inline."""
return re.sub(r'`([^`]+)`', r'
\1', html)
def _process_lists(self, html: str) -> str:
"""Traite les listes à puces."""
lines = html.split('\n')
result = []
in_list = False
for line in lines:
if line.strip().startswith('- '):
if not in_list:
result.append('
')
in_list = True
item = line.strip()[2:]
item = self._process_bold_italic(item)
item = self._process_inline_code(item)
result.append(f'- {item}
')
else:
if in_list:
result.append('
')
in_list = False
result.append(line)
if in_list:
result.append('')
return '\n'.join(result)
def _process_headings(self, html: str) -> str:
"""Traite les titres H3 et H4."""
# H3
html = re.sub(
r'^### ([^\n{]+)$',
r'
\1
',
html,
flags=re.MULTILINE
)
# H4
html = re.sub(
r'^#### ([^\n]+)$',
r'
\1
',
html,
flags=re.MULTILINE
)
return html
def _process_paragraphs(self, html: str) -> str:
"""Traite les paragraphes."""
# Remplacer les lignes vides multiples par des paragraphes
html = re.sub(r'\n\n+', '\n\n', html)
# Envelopper les paragraphes isolés
lines = html.split('\n\n')
result = []
for block in lines:
block = block.strip()
if not block:
continue
# Ne pas envelopper si c'est déjà du HTML
if block.startswith('<') or block.startswith('|'):
result.append(block)
elif not any(block.startswith(x) for x in ['#', '-', '>', '```', ':::', '', '', content)
content = re.sub(r'', '', content)
content = re.sub(r':::card\s+\w+\s+fa-\d+\s*\n', '', content)
# Convertir les health-levels
content = re.sub(r':::health-level\s+\w+\s+\d+\s+\w+\s*\n', '', content)
# Convertir les score-factors
content = re.sub(r':::score-factor\s+[^\n]+\n', '', content)
# Convertir les status-badges
content = re.sub(r':::status-badge\s+[^\n]+\n', '', content)
# Convertir les accordions
content = re.sub(r':::accordion\s+[^\n]+\n', '### ', content)
# Convertir les playbooks
content = re.sub(r':::playbook\s+[^\n]+\n', '### ', content)
# Convertir les troubleshoot
content = re.sub(r':::troubleshoot\s+', '### ', content)
# Supprimer les fermetures :::
content = re.sub(r'^:::$', '', content, flags=re.MULTILINE)
# Supprimer les attributs {icon:...}
content = re.sub(r'\s*\{icon:[^}]+\}', '', content)
# Nettoyer les lignes vides multiples
content = re.sub(r'\n{3,}', '\n\n', content)
return content.strip()