refactor: réorganiser Dockerfiles et scripts par module

Réorganise la structure Docker pour plus de cohérence dans le monorepo.
Chaque module (backend, docs) a maintenant ses propres Dockerfiles et scripts.

Changements:
- backend/docker/ : Dockerfile (prod) + dev.Dockerfile (hot reload) + init script
- docs/docker/ : mkdocs.Dockerfile + pdf.Dockerfile
- docs/scripts/ : generate-bdd-docs.py + generate-pdf-docs.py
- Déplace docker-compose.yml dans backend/
- Supprime scripts obsolètes (fix-markdown-*.sh, remove-broken-links.sh)
- Déplace .dockerignore à la racine
- Met à jour Makefile avec nouveaux chemins

Organisation finale:
- backend/ : tout ce qui concerne l'API backend
- docs/ : tout ce qui concerne la documentation
- scripts/ : uniquement setup.sh (scripts généraux du projet)
This commit is contained in:
jpgiannetti
2026-02-12 20:41:10 +01:00
parent 35aaa105d0
commit ae2fc3ee6f
14 changed files with 64 additions and 350 deletions

View File

@@ -1,21 +0,0 @@
FROM python:3.12-slim-bookworm
# Install system dependencies for weasyprint
RUN apt-get update && apt-get install -y --no-install-recommends \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libgdk-pixbuf-2.0-0 \
libffi-dev \
shared-mime-info \
fonts-dejavu-core \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies
RUN pip install --no-cache-dir \
weasyprint>=60.0 \
markdown>=3.5 \
pyyaml>=6.0
WORKDIR /docs
ENTRYPOINT ["python3", "/docs/scripts/generate-pdf-docs.py"]

View File

@@ -1,141 +0,0 @@
#!/bin/bash
# Script pour corriger les liens internes dans la documentation après refactorisation DDD
set -e
echo "🔗 Correction des liens internes dans la documentation..."
# Fonction pour corriger les liens dans les fichiers du domaine _shared
fix_shared_links() {
echo " → Correction des liens dans _shared..."
find docs/domains/_shared -type f -name "*.md" -exec sed -i \
-e 's|../../regles-metier/01-authentification-inscription.md|../rules/authentification.md|g' \
-e 's|../../regles-metier/02-conformite-rgpd.md|../rules/rgpd.md|g' \
-e 's|../../regles-metier/10-gestion-erreurs.md|../rules/gestion-erreurs.md|g' \
-e 's|modele-global.md|../entities/modele-global.md|g' \
{} \;
}
# Fonction pour corriger les liens dans les fichiers du domaine recommendation
fix_recommendation_links() {
echo " → Correction des liens dans recommendation..."
find docs/domains/recommendation -type f -name "*.md" -exec sed -i \
-e 's|../../regles-metier/03-centres-interet-jauges.md|../rules/centres-interet-jauges.md|g' \
-e 's|../../regles-metier/04-algorithme-recommandation.md|../rules/algorithme-recommandation.md|g' \
-e 's|../../regles-metier/05-interactions-navigation.md|../rules/interactions-navigation.md|g' \
-e 's|../../docs/regles-metier/04-algorithme-recommandation.md|../rules/algorithme-recommandation.md|g' \
-e 's|modele-global.md|../../_shared/entities/modele-global.md|g' \
{} \;
}
# Fonction pour corriger les liens dans les fichiers du domaine content
fix_content_links() {
echo " → Correction des liens dans content..."
find docs/domains/content -type f -name "*.md" -exec sed -i \
-e 's|../../regles-metier/06-audio-guides-multi-sequences.md|../rules/audio-guides.md|g' \
-e 's|../../regles-metier/07-contenus-geolocalises-voiture.md|../rules/contenus-geolocalises.md|g' \
-e 's|../../regles-metier/11-creation-publication-contenu.md|../rules/creation-publication.md|g' \
-e 's|../../regles-metier/12-radio-live.md|../rules/radio-live.md|g' \
-e 's|../../regles-metier/13-detection-contenu-protege.md|../rules/detection-contenu-protege.md|g' \
-e 's|modele-global.md|../../_shared/entities/modele-global.md|g' \
{} \;
}
# Fonction pour corriger les liens dans les fichiers du domaine advertising
fix_advertising_links() {
echo " → Correction des liens dans advertising..."
find docs/domains/advertising -type f -name "*.md" -exec sed -i \
-e 's|../../regles-metier/16-publicites.md|../rules/publicites.md|g' \
-e 's|modele-global.md|../../_shared/entities/modele-global.md|g' \
{} \;
}
# Fonction pour corriger les liens dans les fichiers du domaine premium
fix_premium_links() {
echo " → Correction des liens dans premium..."
find docs/domains/premium -type f -name "*.md" -exec sed -i \
-e 's|../../regles-metier/08-mode-offline.md|../rules/mode-offline.md|g' \
-e 's|../../regles-metier/09-abonnements-notifications.md|../rules/abonnements-notifications.md|g' \
-e 's|../../regles-metier/17-premium.md|../rules/premium.md|g' \
-e 's|modele-global.md|../../_shared/entities/modele-global.md|g' \
{} \;
}
# Fonction pour corriger les liens dans les fichiers du domaine monetization
fix_monetization_links() {
echo " → Correction des liens dans monetization..."
find docs/domains/monetization -type f -name "*.md" -exec sed -i \
-e 's|../../regles-metier/18-monetisation-createurs.md|../rules/monetisation-createurs.md|g' \
-e 's|modele-global.md|../../_shared/entities/modele-global.md|g' \
{} \;
}
# Fonction pour corriger les liens dans les fichiers du domaine moderation
fix_moderation_links() {
echo " → Correction des liens dans moderation..."
find docs/domains/moderation -type f -name "*.md" -exec sed -i \
-e 's|../../regles-metier/14-moderation-flows.md|../rules/moderation-flows.md|g' \
-e 's|../../regles-metier/15-moderation-communautaire.md|../rules/moderation-communautaire.md|g' \
-e 's|../../regles-metier/19-autres-comportements.md|../rules/autres-comportements.md|g' \
-e 's|modele-global.md|../../_shared/entities/modele-global.md|g' \
{} \;
}
# Fonction pour corriger les liens dans les autres fichiers docs (ADR, etc.)
fix_other_docs_links() {
echo " → Correction des liens dans ADR et autres docs..."
# ADR et autres fichiers qui référencent les anciennes règles métier
find docs/adr docs/mobile docs/compliance docs/architecture -type f -name "*.md" 2>/dev/null -exec sed -i \
-e 's|regles-metier/01-authentification-inscription.md|domains/_shared/rules/authentification.md|g' \
-e 's|regles-metier/02-conformite-rgpd.md|domains/_shared/rules/rgpd.md|g' \
-e 's|regles-metier/03-centres-interet-jauges.md|domains/recommendation/rules/centres-interet-jauges.md|g' \
-e 's|regles-metier/04-algorithme-recommandation.md|domains/recommendation/rules/algorithme-recommandation.md|g' \
-e 's|regles-metier/05-interactions-navigation.md|domains/recommendation/rules/interactions-navigation.md|g' \
-e 's|regles-metier/06-audio-guides-multi-sequences.md|domains/content/rules/audio-guides.md|g' \
-e 's|regles-metier/07-contenus-geolocalises-voiture.md|domains/content/rules/contenus-geolocalises.md|g' \
-e 's|regles-metier/08-mode-offline.md|domains/premium/rules/mode-offline.md|g' \
-e 's|regles-metier/09-abonnements-notifications.md|domains/premium/rules/abonnements-notifications.md|g' \
-e 's|regles-metier/10-gestion-erreurs.md|domains/_shared/rules/gestion-erreurs.md|g' \
-e 's|regles-metier/11-creation-publication-contenu.md|domains/content/rules/creation-publication.md|g' \
-e 's|regles-metier/12-radio-live.md|domains/content/rules/radio-live.md|g' \
-e 's|regles-metier/13-detection-contenu-protege.md|domains/content/rules/detection-contenu-protege.md|g' \
-e 's|regles-metier/14-moderation-flows.md|domains/moderation/rules/moderation-flows.md|g' \
-e 's|regles-metier/15-moderation-communautaire.md|domains/moderation/rules/moderation-communautaire.md|g' \
-e 's|regles-metier/16-publicites.md|domains/advertising/rules/publicites.md|g' \
-e 's|regles-metier/17-premium.md|domains/premium/rules/premium.md|g' \
-e 's|regles-metier/18-monetisation-createurs.md|domains/monetization/rules/monetisation-createurs.md|g' \
-e 's|regles-metier/19-autres-comportements.md|domains/moderation/rules/autres-comportements.md|g' \
-e 's|diagrammes/entites/modele-global.md|domains/_shared/entities/modele-global.md|g' \
-e 's|diagrammes/entites/modele-recommandation.md|domains/recommendation/entities/modele-recommandation.md|g' \
-e 's|diagrammes/entites/modele-audio-guides.md|domains/content/entities/modele-audio-guides.md|g' \
-e 's|diagrammes/entites/modele-radio-live.md|domains/content/entities/modele-radio-live.md|g' \
-e 's|diagrammes/entites/modele-publicites.md|domains/advertising/entities/modele-publicites.md|g' \
-e 's|diagrammes/entites/modele-premium.md|domains/premium/entities/modele-premium.md|g' \
-e 's|diagrammes/entites/modele-monetisation.md|domains/monetization/entities/modele-monetisation.md|g' \
-e 's|diagrammes/entites/modele-moderation.md|domains/moderation/entities/modele-moderation.md|g' \
-e 's|diagrammes/flux/moderation-signalement.md|domains/moderation/flows/moderation-signalement.md|g' \
-e 's|diagrammes/etats/signalement-lifecycle.md|domains/moderation/states/signalement-lifecycle.md|g' \
-e 's|diagrammes/sequence/processus-appel-moderation.md|domains/moderation/sequences/processus-appel-moderation.md|g' \
{} \; 2>/dev/null || true
# Fichier gherkin-moderation-overview.md
if [ -f docs/gherkin-moderation-overview.md ]; then
sed -i \
-e 's|regles-metier/14-moderation-flows.md|domains/moderation/rules/moderation-flows.md|g' \
-e 's|regles-metier/15-moderation-communautaire.md|domains/moderation/rules/moderation-communautaire.md|g' \
docs/gherkin-moderation-overview.md
fi
}
# Exécuter toutes les corrections
fix_shared_links
fix_recommendation_links
fix_content_links
fix_advertising_links
fix_premium_links
fix_monetization_links
fix_moderation_links
fix_other_docs_links
echo "✅ Correction des liens terminée!"

View File

@@ -1,58 +0,0 @@
#!/bin/bash
# Script pour ajouter des lignes vides avant les listes markdown
# qui n'en ont pas, pour un rendu correct dans mkdocs
set -e
echo "🔍 Recherche des fichiers markdown dans docs/..."
# Compteur de fichiers modifiés
modified=0
# Trouver tous les fichiers .md dans docs/
find docs -name "*.md" -type f | while read -r file; do
echo " Traitement de $file..."
# Utiliser awk pour ajouter une ligne vide avant les listes si nécessaire
awk '
BEGIN {
prev = ""
prev_empty = 1
}
{
current = $0
# Si la ligne courante commence par "- " et que la ligne précédente n'\''est pas vide
# et ne commence pas déjà par "- " (déjà dans une liste)
if (current ~ /^- / && prev !~ /^$/ && prev !~ /^- /) {
print prev
print "" # Ajouter une ligne vide
prev = current
}
# Sinon, imprimer la ligne précédente normalement
else if (NR > 1) {
print prev
prev = current
}
else {
prev = current
}
}
END {
# Imprimer la dernière ligne
if (prev != "") print prev
}
' "$file" > "$file.tmp"
# Vérifier si le fichier a changé
if ! cmp -s "$file" "$file.tmp"; then
mv "$file.tmp" "$file"
echo "$file modifié"
modified=$((modified + 1))
else
rm "$file.tmp"
fi
done
echo ""
echo "✨ Terminé ! $modified fichier(s) modifié(s)"

View File

@@ -1,69 +0,0 @@
#!/bin/bash
# Script pour corriger les liens restants après refactorisation DDD
set -e
echo "🔗 Correction des liens restants..."
# 1. Corriger les liens docs/adr/ → adr/ dans index.md et technical.md
echo " → Correction des liens ADR dans index.md et technical.md..."
find docs -name "index.md" -o -name "technical.md" -o -name "TECHNICAL.md" | while read file; do
if [ -f "$file" ]; then
sed -i 's|docs/adr/|adr/|g' "$file"
fi
done
# 2. Corriger les liens vers anciens noms de fichiers numérotés
echo " → Correction des liens vers anciens noms de fichiers..."
# Dans content/
find docs/domains/content -type f -name "*.md" -exec sed -i \
-e 's|05-interactions-navigation\.md|../../recommendation/rules/interactions-navigation.md|g' \
-e 's|18-detection-contenu-protege\.md|detection-contenu-protege.md|g' \
{} \;
# Dans moderation/
find docs/domains/moderation -type f -name "*.md" -exec sed -i \
-e 's|18-detection-contenu-protege\.md|../../content/rules/detection-contenu-protege.md|g' \
-e 's|19-moderation-communautaire\.md|moderation-communautaire.md|g' \
{} \;
# Dans premium/
find docs/domains/premium -type f -name "*.md" -exec sed -i \
-e 's|05-interactions-navigation\.md|../../recommendation/rules/interactions-navigation.md|g' \
-e 's|08-mode-offline\.md|mode-offline.md|g' \
{} \;
# Dans recommendation/
find docs/domains/recommendation -type f -name "*.md" -exec sed -i \
-e 's|05-interactions-navigation\.md|interactions-navigation.md|g' \
-e 's|ANNEXE-POST-MVP\.md|../../_shared/rules/ANNEXE-POST-MVP.md|g' \
{} \;
# 3. Corriger les liens relatifs vers ADR depuis les domaines
echo " → Correction des liens ADR depuis les domaines..."
find docs/domains -type f -name "*.md" -exec sed -i \
-e 's|\.\./adr/|../../../adr/|g' \
{} \;
# 4. Corriger le lien dans recommendation/features/recommendation/README.md
if [ -f "docs/domains/recommendation/features/recommendation/README.md" ]; then
sed -i 's|\.\./rules/|../../rules/|g' docs/domains/recommendation/features/recommendation/README.md
fi
# 5. Corriger les liens dans mobile/ vers adr/
find docs/mobile -type f -name "*.md" -exec sed -i \
-e 's|\.\./adr/010-frontend-mobile\.md|../adr/012-frontend-mobile.md|g' \
{} \;
# 6. Corriger liens dans compliance/
if [ -f "docs/compliance/stores-submission.md" ]; then
sed -i 's|../adr/010-frontend-mobile\.md|../adr/012-frontend-mobile.md|g' docs/compliance/stores-submission.md
fi
# 7. Corriger le lien vers sequences/scoring-recommandation.md qui n'existe pas
find docs/domains/recommendation -type f -name "README.md" -exec sed -i \
-e 's|sequences/scoring-recommandation\.md|(à créer)|g' \
{} \;
echo "✅ Correction des liens restants terminée!"

View File

@@ -1,419 +0,0 @@
#!/usr/bin/env python3
"""
Script pour générer la documentation Markdown à partir des fichiers Gherkin (.feature)
Convertit les fichiers .feature en fichiers .md pour MkDocs Material
"""
import os
import re
from pathlib import Path
from typing import List, Dict, Optional, Tuple
from dataclasses import dataclass, field
@dataclass
class Step:
"""Représente une étape Gherkin"""
keyword: str # Étant donné, Quand, Alors, Et, Mais
text: str
table: List[List[str]] = field(default_factory=list)
@dataclass
class Scenario:
"""Représente un scénario ou plan de scénario"""
title: str
is_outline: bool
steps: List[Step] = field(default_factory=list)
examples: List[List[str]] = field(default_factory=list)
@dataclass
class Feature:
"""Représente une fonctionnalité complète"""
title: str
description: str = ""
context_steps: List[Step] = field(default_factory=list)
scenarios: List[Scenario] = field(default_factory=list)
def parse_table(lines: List[str], start_idx: int) -> Tuple[List[List[str]], int]:
"""Parse une table Gherkin et retourne (table, nouvel index)"""
table = []
idx = start_idx
while idx < len(lines) and lines[idx].strip().startswith('|'):
row = [cell.strip() for cell in lines[idx].strip().strip('|').split('|')]
table.append(row)
idx += 1
return table, idx
def parse_feature_content(content: str) -> Feature:
"""Parse le contenu d'un fichier .feature"""
# Supprimer la ligne de langage
content = re.sub(r'^# language: \w+\n', '', content)
lines = content.split('\n')
feature = Feature(title="")
i = 0
current_scenario: Optional[Scenario] = None
in_context = False
in_examples = False
while i < len(lines):
line = lines[i]
stripped = line.strip()
# Ligne vide
if not stripped:
i += 1
continue
# Fonctionnalité
if stripped.startswith('Fonctionnalité:'):
feature.title = stripped[len('Fonctionnalité:'):].strip()
i += 1
# Capturer la description (lignes indentées qui suivent)
desc_lines = []
while i < len(lines) and lines[i].startswith(' ') and not lines[i].strip().startswith(('Contexte:', 'Scénario:', 'Plan du Scénario:')):
desc_lines.append(lines[i].strip())
i += 1
feature.description = '\n'.join(desc_lines)
continue
# Contexte
if stripped == 'Contexte:':
in_context = True
in_examples = False
i += 1
continue
# Plan du Scénario
if stripped.startswith('Plan du Scénario:'):
if current_scenario:
feature.scenarios.append(current_scenario)
current_scenario = Scenario(
title=stripped[len('Plan du Scénario:'):].strip(),
is_outline=True
)
in_context = False
in_examples = False
i += 1
continue
# Scénario
if stripped.startswith('Scénario:'):
if current_scenario:
feature.scenarios.append(current_scenario)
current_scenario = Scenario(
title=stripped[len('Scénario:'):].strip(),
is_outline=False
)
in_context = False
in_examples = False
i += 1
continue
# Exemples (pour Plan du Scénario)
if stripped == 'Exemples:':
in_examples = True
i += 1
continue
# Table d'exemples
if in_examples and stripped.startswith('|') and current_scenario:
table, i = parse_table(lines, i)
current_scenario.examples = table
in_examples = False
continue
# Étapes Gherkin
keywords = ['Étant donné', 'Quand', 'Alors', 'Et', 'Mais']
matched_keyword = None
for kw in keywords:
if stripped.startswith(kw):
matched_keyword = kw
break
if matched_keyword:
step_text = stripped[len(matched_keyword):].strip()
step = Step(keyword=matched_keyword, text=step_text)
# Vérifier si une table suit
if i + 1 < len(lines) and lines[i + 1].strip().startswith('|'):
table, i = parse_table(lines, i + 1)
step.table = table
else:
i += 1
# Ajouter l'étape au bon endroit
if in_context:
feature.context_steps.append(step)
elif current_scenario:
current_scenario.steps.append(step)
continue
i += 1
# Ajouter le dernier scénario
if current_scenario:
feature.scenarios.append(current_scenario)
return feature
def format_table_markdown(table: List[List[str]], indent: str = "") -> str:
"""Formate une table en Markdown"""
if not table:
return ""
lines = []
# En-tête
lines.append(f"{indent}| " + " | ".join(table[0]) + " |")
# Séparateur
lines.append(f"{indent}|" + "|".join(["---" for _ in table[0]]) + "|")
# Lignes de données
for row in table[1:]:
lines.append(f"{indent}| " + " | ".join(row) + " |")
return '\n'.join(lines)
def get_keyword_color(keyword: str) -> str:
"""Retourne une couleur pour chaque type de mot-clé"""
colors = {
'Étant donné': '#2196F3', # Bleu
'Quand': '#FF9800', # Orange
'Alors': '#4CAF50', # Vert
'Et': '#9E9E9E', # Gris
'Mais': '#F44336' # Rouge
}
return colors.get(keyword, '#000000')
def format_step(step: Step, indent: str = "") -> str:
"""Formate une étape Gherkin avec couleur"""
color = get_keyword_color(step.keyword)
# Ajouter deux espaces à la fin pour forcer un line break en markdown
result = f'{indent}<span style="color: {color}">**{step.keyword}**</span> {step.text} '
if step.table:
result += "\n\n"
# Pas d'indentation pour les tableaux (sinon ils deviennent des blocs de code)
result += format_table_markdown(step.table, "")
result += "\n"
return result
def generate_markdown(feature: Feature) -> str:
"""Génère le contenu Markdown pour une fonctionnalité"""
lines = []
# Titre de la fonctionnalité
lines.append(f"# {feature.title}\n")
# Description en tant que user story (bloc quote formaté proprement)
if feature.description:
desc_lines = feature.description.split('\n')
for desc_line in desc_lines:
lines.append(f"> *{desc_line}*\n")
lines.append("\n")
# Statistiques
scenario_count = len(feature.scenarios)
outline_count = sum(1 for s in feature.scenarios if s.is_outline)
regular_count = scenario_count - outline_count
stats_parts = [f"**{scenario_count} scénario{'s' if scenario_count > 1 else ''}**"]
if outline_count > 0:
stats_parts.append(f" ({regular_count} standard{'s' if regular_count > 1 else ''}, {outline_count} plan{'s' if outline_count > 1 else ''})")
lines.append("".join(stats_parts) + "\n\n")
lines.append("---\n\n")
# Contexte
if feature.context_steps:
lines.append('!!! info "Contexte commun à tous les scénarios"\n\n')
for step in feature.context_steps:
lines.append(format_step(step, " ") + "\n")
lines.append("\n")
# Scénarios
for idx, scenario in enumerate(feature.scenarios, 1):
# Header h2 pour chaque scénario
if scenario.is_outline:
lines.append(f"## {idx}. 📋 Plan: {scenario.title}\n\n")
else:
lines.append(f"## {idx}. {scenario.title}\n\n")
# Regrouper les étapes par section (Given/When/Then)
current_section = None
for step in scenario.steps:
# Déterminer la section
if step.keyword == 'Étant donné':
section = 'given'
elif step.keyword == 'Quand':
section = 'when'
elif step.keyword == 'Alors':
section = 'then'
else:
section = current_section # Et/Mais gardent la section précédente
# Ajouter un séparateur visuel entre les sections
if section != current_section and current_section is not None:
lines.append("\n")
current_section = section
lines.append(format_step(step, "") + "\n")
# Exemples pour les plans de scénario
if scenario.is_outline and scenario.examples:
lines.append("\n**📊 Exemples de données:**\n\n")
lines.append(format_table_markdown(scenario.examples, "") + "\n")
lines.append("\n---\n\n")
return ''.join(lines)
def generate_index(features_dir: Path, output_dir: Path) -> None:
"""Génère un fichier index.md listant toutes les fonctionnalités"""
categories: Dict[str, List[Tuple[str, Path, int, int]]] = {}
for feature_file in features_dir.rglob('*.feature'):
category = feature_file.parent.name
relative_path = feature_file.relative_to(features_dir)
md_path = relative_path.with_suffix('.md')
# Extraire le titre et compter les scénarios
with open(feature_file, 'r', encoding='utf-8') as f:
content = f.read()
feature_match = re.search(r'^Fonctionnalité: (.+)$', content, re.MULTILINE)
title = feature_match.group(1) if feature_match else feature_file.stem
scenario_count = len(re.findall(r'^\s{2}Scénario:', content, re.MULTILINE))
outline_count = len(re.findall(r'^\s{2}Plan du Scénario:', content, re.MULTILINE))
if category not in categories:
categories[category] = []
categories[category].append((title, md_path, scenario_count, outline_count))
# Calculer les totaux
total_features = sum(len(cat) for cat in categories.values())
total_scenarios = sum(s + o for cat in categories.values() for _, _, s, o in cat)
# Générer l'index
index_lines = [
"# Tests BDD - Documentation des fonctionnalités\n\n",
"Cette documentation est générée automatiquement à partir des fichiers Gherkin (`.feature`).\n\n",
"## Vue d'ensemble\n\n",
f"| Métrique | Valeur |\n",
f"|----------|--------|\n",
f"| Fonctionnalités | **{total_features}** |\n",
f"| Scénarios | **{total_scenarios}** |\n",
f"| Domaines métier | **{len(categories)}** |\n\n",
"---\n\n"
]
# Icônes par catégorie
category_icons = {
'authentication': '🔐',
'recommendation': '🎯',
'interest-gauges': '📊',
'content-creation': '🎨',
'navigation': '🧭',
'publicites': '📢',
'radio-live': '📻',
'abonnements': '🔔',
'monetisation': '💰',
'premium': '',
'mode-offline': '📴',
'error-handling': '⚠️',
'rgpd-compliance': '🔒',
'moderation': '🛡️',
'partage': '🔗',
'profil': '👤',
'recherche': '🔍',
'audio-guides': '🎧',
}
for category in sorted(categories.keys()):
category_title = category.replace('-', ' ').replace('_', ' ').title()
icon = category_icons.get(category, '📁')
cat_scenarios = sum(s + o for _, _, s, o in categories[category])
index_lines.append(f"## {icon} {category_title}\n\n")
# Carte de la catégorie
index_lines.append(f"| Fonctionnalité | Scénarios |\n")
index_lines.append(f"|----------------|:---------:|\n")
for title, md_path, scenario_count, outline_count in sorted(categories[category]):
link = str(md_path).replace('\\', '/')
total = scenario_count + outline_count
index_lines.append(f"| [{title}]({link}) | {total} |\n")
index_lines.append(f"\n*{len(categories[category])} fonctionnalités • {cat_scenarios} scénarios*\n\n")
# Écrire l'index
index_file = output_dir / 'index.md'
with open(index_file, 'w', encoding='utf-8') as f:
f.writelines(index_lines)
print(f"✓ Index généré: {index_file}")
def main():
"""Point d'entrée principal"""
# Chemins
project_root = Path(__file__).parent.parent
features_dir = project_root / 'docs' / 'domains'
output_dir = project_root / 'docs' / 'generated' / 'bdd'
# Nettoyer le dossier de sortie
if output_dir.exists():
import shutil
shutil.rmtree(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
print(f"📁 Génération de la documentation BDD...")
print(f" Source: {features_dir}")
print(f" Destination: {output_dir}\n")
# Convertir tous les fichiers .feature
feature_files = list(features_dir.rglob('*.feature'))
count = 0
for feature_file in feature_files:
# Créer la structure de dossiers
relative_path = feature_file.relative_to(features_dir)
md_file = output_dir / relative_path.with_suffix('.md')
md_file.parent.mkdir(parents=True, exist_ok=True)
# Parser et convertir
with open(feature_file, 'r', encoding='utf-8') as f:
content = f.read()
feature = parse_feature_content(content)
markdown = generate_markdown(feature)
# Écrire le fichier
with open(md_file, 'w', encoding='utf-8') as f:
f.write(markdown)
count += 1
print(f"{relative_path}{md_file.relative_to(project_root)}")
print(f"\n{count} fichiers .feature convertis en Markdown")
# Générer l'index
print("\n📋 Génération de l'index...")
generate_index(features_dir, output_dir)
print(f"\n✅ Documentation BDD générée avec succès dans {output_dir}")
if __name__ == '__main__':
main()

View File

@@ -1,376 +0,0 @@
#!/usr/bin/env python3
"""
Script pour générer un PDF complet de toute la documentation MkDocs.
Utilise weasyprint pour convertir le HTML en PDF.
"""
import os
import re
import subprocess
import tempfile
import shutil
from pathlib import Path
from typing import List, Dict, Any
import yaml
class SafeLineLoader(yaml.SafeLoader):
"""Loader YAML qui ignore les tags Python non supportés"""
pass
# Ignorer les tags Python custom (pour les extensions MkDocs)
SafeLineLoader.add_multi_constructor('tag:yaml.org,2002:python/', lambda loader, suffix, node: None)
def parse_nav(nav: List, docs_dir: Path, prefix: str = "") -> List[Path]:
"""Parse la navigation MkDocs et retourne la liste ordonnée des fichiers MD"""
files = []
for item in nav:
if isinstance(item, str):
# Fichier simple ou dossier
path = docs_dir / item
if path.is_file() and path.suffix == '.md':
files.append(path)
elif path.is_dir():
# C'est un dossier, récupérer tous les fichiers MD
files.extend(sorted(path.rglob('*.md')))
elif item.endswith('/'):
# Référence à un dossier (ex: bdd/)
dir_path = docs_dir / item.rstrip('/')
if dir_path.is_dir():
files.extend(sorted(dir_path.rglob('*.md')))
elif isinstance(item, dict):
for title, value in item.items():
if isinstance(value, str):
# Fichier avec titre ou dossier
path = docs_dir / value
if path.is_file() and path.suffix == '.md':
files.append(path)
elif path.is_dir():
files.extend(sorted(path.rglob('*.md')))
elif value.endswith('/'):
dir_path = docs_dir / value.rstrip('/')
if dir_path.is_dir():
files.extend(sorted(dir_path.rglob('*.md')))
elif isinstance(value, list):
# Sous-section
files.extend(parse_nav(value, docs_dir, prefix + " "))
return files
def get_all_md_files(docs_dir: Path, mkdocs_config: Dict[str, Any]) -> List[Path]:
"""Récupère tous les fichiers MD dans l'ordre de navigation"""
if 'nav' in mkdocs_config:
return parse_nav(mkdocs_config['nav'], docs_dir)
else:
# Pas de nav, récupérer tous les fichiers MD
return sorted(docs_dir.rglob('*.md'))
def preprocess_markdown(content: str, file_path: Path, docs_dir: Path) -> str:
"""Prétraite le Markdown pour le PDF"""
# Convertir les liens relatifs
def fix_link(match):
link = match.group(2)
if link.startswith('http'):
return match.group(0)
# Convertir les liens .md en ancres
if link.endswith('.md'):
link = link[:-3]
return f'[{match.group(1)}](#{link})'
content = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', fix_link, content)
# Supprimer les admonitions MkDocs et les convertir en blockquotes
# !!! type "title" -> blockquote
content = re.sub(
r'!!! (\w+) "([^"]+)"\n\n((?: .+\n)+)',
lambda m: f'> **{m.group(2)}**\n>\n' + '\n'.join(f'> {line[4:]}' for line in m.group(3).split('\n') if line),
content
)
# ??? type "title" -> blockquote (collapsible)
content = re.sub(
r'\?\?\??\+? (\w+) "([^"]+)"\n\n((?: .+\n)+)',
lambda m: f'> **{m.group(2)}**\n>\n' + '\n'.join(f'> {line[4:]}' for line in m.group(3).split('\n') if line),
content
)
return content
def create_combined_markdown(files: List[Path], docs_dir: Path, output_path: Path) -> None:
"""Combine tous les fichiers MD en un seul"""
combined = []
# Page de titre
combined.append("# Documentation RoadWave\n\n")
combined.append("---\n\n")
combined.append("## Table des matières\n\n")
# Générer la table des matières
toc_entries = []
for f in files:
with open(f, 'r', encoding='utf-8') as file:
content = file.read()
# Extraire le titre H1
title_match = re.search(r'^# (.+)$', content, re.MULTILINE)
if title_match:
title = title_match.group(1)
anchor = re.sub(r'[^\w\s-]', '', title.lower()).replace(' ', '-')
toc_entries.append(f"- [{title}](#{anchor})")
combined.append('\n'.join(toc_entries))
combined.append("\n\n---\n\n")
combined.append('<div style="page-break-after: always;"></div>\n\n')
# Ajouter chaque fichier
for f in files:
with open(f, 'r', encoding='utf-8') as file:
content = file.read()
# Prétraiter
content = preprocess_markdown(content, f, docs_dir)
combined.append(content)
combined.append("\n\n")
combined.append('<div style="page-break-after: always;"></div>\n\n')
with open(output_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(combined))
def markdown_to_html(md_path: Path, html_path: Path) -> None:
"""Convertit Markdown en HTML avec styles"""
import markdown
from markdown.extensions.tables import TableExtension
from markdown.extensions.fenced_code import FencedCodeExtension
from markdown.extensions.toc import TocExtension
with open(md_path, 'r', encoding='utf-8') as f:
md_content = f.read()
# Convertir en HTML
md = markdown.Markdown(extensions=[
'tables',
'fenced_code',
'toc',
'attr_list',
'md_in_html'
])
html_content = md.convert(md_content)
# Template HTML avec styles
html_template = f'''<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Documentation RoadWave</title>
<style>
@page {{
size: A4;
margin: 2cm;
@bottom-center {{
content: counter(page);
}}
}}
body {{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 11pt;
line-height: 1.6;
color: #333;
max-width: 100%;
}}
h1 {{
color: #1a237e;
border-bottom: 3px solid #3f51b5;
padding-bottom: 10px;
page-break-after: avoid;
font-size: 24pt;
}}
h2 {{
color: #303f9f;
border-bottom: 1px solid #7986cb;
padding-bottom: 5px;
margin-top: 30px;
page-break-after: avoid;
font-size: 16pt;
}}
h3 {{
color: #3949ab;
page-break-after: avoid;
font-size: 13pt;
}}
/* Couleurs Gherkin */
span[style*="#2196F3"] {{ color: #1565c0 !important; font-weight: bold; }} /* Étant donné - Bleu */
span[style*="#FF9800"] {{ color: #e65100 !important; font-weight: bold; }} /* Quand - Orange */
span[style*="#4CAF50"] {{ color: #2e7d32 !important; font-weight: bold; }} /* Alors - Vert */
span[style*="#9E9E9E"] {{ color: #616161 !important; }} /* Et - Gris */
span[style*="#F44336"] {{ color: #c62828 !important; font-weight: bold; }} /* Mais - Rouge */
table {{
border-collapse: collapse;
width: 100%;
margin: 15px 0;
font-size: 10pt;
}}
th, td {{
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}}
th {{
background-color: #3f51b5;
color: white;
}}
tr:nth-child(even) {{
background-color: #f5f5f5;
}}
blockquote {{
border-left: 4px solid #3f51b5;
margin: 15px 0;
padding: 10px 20px;
background-color: #e8eaf6;
font-style: italic;
}}
code {{
background-color: #f5f5f5;
padding: 2px 6px;
border-radius: 3px;
font-family: "Fira Code", "Consolas", monospace;
font-size: 10pt;
}}
pre {{
background-color: #263238;
color: #aed581;
padding: 15px;
border-radius: 5px;
overflow-x: auto;
font-size: 9pt;
}}
pre code {{
background-color: transparent;
padding: 0;
color: inherit;
}}
hr {{
border: none;
border-top: 1px solid #e0e0e0;
margin: 30px 0;
}}
a {{
color: #1976d2;
text-decoration: none;
}}
/* Info box (contexte) */
.info-box {{
background-color: #e3f2fd;
border-left: 4px solid #2196f3;
padding: 15px;
margin: 15px 0;
}}
/* Page breaks */
.page-break {{
page-break-after: always;
}}
/* Cover page */
.cover {{
text-align: center;
padding-top: 200px;
}}
.cover h1 {{
font-size: 36pt;
border: none;
}}
/* TOC */
.toc {{
page-break-after: always;
}}
.toc ul {{
list-style: none;
padding-left: 20px;
}}
.toc li {{
margin: 5px 0;
}}
</style>
</head>
<body>
{html_content}
</body>
</html>'''
with open(html_path, 'w', encoding='utf-8') as f:
f.write(html_template)
def html_to_pdf(html_path: Path, pdf_path: Path) -> None:
"""Convertit HTML en PDF avec weasyprint"""
from weasyprint import HTML, CSS
print(f" Conversion HTML → PDF...")
HTML(filename=str(html_path)).write_pdf(str(pdf_path))
def main():
"""Point d'entrée principal"""
project_root = Path(__file__).parent.parent
docs_dir = project_root / 'docs'
mkdocs_path = project_root / 'mkdocs.yml'
output_dir = project_root / 'docs' / 'generated' / 'pdf'
output_dir.mkdir(parents=True, exist_ok=True)
print("📄 Génération du PDF de la documentation RoadWave...")
# Charger la config MkDocs
with open(mkdocs_path, 'r', encoding='utf-8') as f:
mkdocs_config = yaml.load(f, Loader=SafeLineLoader)
# Récupérer tous les fichiers MD
print(" Collecte des fichiers Markdown...")
md_files = get_all_md_files(docs_dir, mkdocs_config)
print(f"{len(md_files)} fichiers trouvés")
# Créer un fichier MD combiné
combined_md = output_dir / 'documentation_complete.md'
print(" Combinaison des fichiers...")
create_combined_markdown(md_files, docs_dir, combined_md)
# Convertir en HTML
html_path = output_dir / 'documentation_complete.html'
print(" Conversion Markdown → HTML...")
markdown_to_html(combined_md, html_path)
# Convertir en PDF
pdf_path = output_dir / 'RoadWave_Documentation.pdf'
html_to_pdf(html_path, pdf_path)
print(f"\n✅ PDF généré: {pdf_path}")
print(f" Taille: {pdf_path.stat().st_size / 1024 / 1024:.2f} MB")
if __name__ == '__main__':
main()

View File

@@ -1,67 +0,0 @@
#!/bin/bash
# Script pour supprimer les liens cassés vers des fichiers inexistants
set -e
echo "🗑️ Suppression des liens cassés..."
# 1. Supprimer les liens vers ADR inexistants dans index.md
echo " → Nettoyage de index.md..."
if [ -f "docs/index.md" ]; then
sed -i \
-e '/adr\/010-commandes-volant\.md/d' \
-e '/adr\/011-conformite-stores-carplay-android-auto\.md/d' \
docs/index.md
fi
# 2. Corriger les numéros ADR incorrects dans technical.md
echo " → Correction des numéros ADR dans technical.md..."
if [ -f "docs/technical.md" ]; then
sed -i \
-e 's|adr/018-notifications-push\.md|adr/017-notifications-geolocalisees.md|g' \
docs/technical.md
fi
# 3. Supprimer les liens vers fichiers d'analyse inexistants dans ADR
echo " → Nettoyage des liens dans les ADR..."
find docs/adr -type f -name "*.md" -exec sed -i \
-e 's|\[.*\](../architecture/database/schema\.md)||g' \
-e 's|\[.*\](../INCONSISTENCIES-ANALYSIS\.md[^)]*)||g' \
-e 's|\[.*\](\.\.\/ANALYSE_LIBRAIRIES_GO\.md)||g' \
-e 's|\[.*\](../../README\.md)||g' \
-e 's|\[.*\](../../TECHNICAL\.md)||g' \
{} \;
# 4. Corriger les numéros ADR dans adr/014-organisation-monorepo.md
if [ -f "docs/adr/014-organisation-monorepo.md" ]; then
sed -i 's|020-strategie-cicd-monorepo\.md|022-strategie-cicd-monorepo.md|g' docs/adr/014-organisation-monorepo.md
fi
# 5. Corriger les numéros ADR dans adr/020-librairies-flutter.md
if [ -f "docs/adr/020-librairies-flutter.md" ]; then
sed -i \
-e 's|018-librairies-flutter\.md|020-librairies-flutter.md|g' \
-e 's|010-frontend-mobile\.md|012-frontend-mobile.md|g' \
docs/adr/020-librairies-flutter.md
fi
# 6. Corriger le lien ADR dans domains/recommendation/rules/interactions-navigation.md
if [ -f "docs/domains/recommendation/rules/interactions-navigation.md" ]; then
sed -i 's|../../../adr/010-frontend-mobile\.md|../../../adr/012-frontend-mobile.md|g' \
docs/domains/recommendation/rules/interactions-navigation.md
fi
# 7. Corriger les liens dans domains/README.md
if [ -f "docs/domains/README.md" ]; then
sed -i \
-e 's|\[📖 Règles métier par numéro\](../regles-metier/)||g' \
-e 's|\[🏛️ ADR (Architecture Decision Records)\](../../../adr/)|[🏛️ ADR (Architecture Decision Records)](../adr/)|g' \
-e 's|\[⚖️ Documentation légale\](../legal/)|[⚖️ Documentation légale](../legal/README.md)|g' \
-e 's|\[🖥️ Interfaces UI\](../interfaces/)|[🖥️ Interfaces UI](../interfaces/README.md)|g' \
docs/domains/README.md
fi
# 8. Nettoyer les lignes vides créées par les suppressions
find docs -type f -name "*.md" -exec sed -i '/^$/N;/^\n$/d' {} \;
echo "✅ Liens cassés supprimés/corrigés!"