""" 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'
\n

{emoji}{text}

' html = re.sub(pattern, replace_section, html, flags=re.MULTILINE) # Nettoyer le premier
en trop html = html.replace('
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'''

{title}

{desc}

''' 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'''
{title}

{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'''

{title}

{content}

''' 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'''

{label}: {content}

''' 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'' html_table += '' html_table += '' for row in rows: html_table += '' for cell in row: cell = self._process_inline_code(cell) html_table += f'' html_table += '' html_table += '
{h}
{cell}
' 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'
{code}
' 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 = 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()