Leonel FOFOU
2026-07-28 32450e295200b2bc7896518ff74161ff26772e2d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package com.megatim.manuel.core.generate;
 
import com.megatim.manuel.core.convert.ConversionResult;
import com.megatim.manuel.core.convert.ConverterFactory;
import com.megatim.manuel.core.theme.ManuelTheme;
 
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Stream;
 
/**
 * Orchestration de la génération du manuel : convertit les documents sources
 * (Word/PowerPoint) en bundle HTML + {@code toc.json} dans le répertoire cible
 * fourni par l'application hôte.
 *
 * <p>La génération s'effectue dans un dossier temporaire sibling
 * ({@code <cible>.tmp}) puis remplace le répertoire du manuel en fin de
 * traitement : une génération échouée ne détruit jamais le manuel existant.
 * Les documents importés sont copiés dans {@code <cible>/sources} avec le
 * manifeste {@link SourcesManifest} pour rester régénérables.</p>
 *
 * <p>À exécuter hors du thread JavaFX (les convertisseurs PowerPoint rendent
 * les diapositives en PNG via AWT).</p>
 */
public class ManuelGenerator {
 
    /** Sous-dossier du bundle contenant les copies des documents sources + sources.json. */
    public static final String SOURCES_DIR = "sources";
 
    private final ManuelTheme theme;
 
    /** Générateur au thème par défaut (charte Sirius). */
    public ManuelGenerator() {
        this(ManuelTheme.defaultTheme());
    }
 
    /** Générateur dont le styles.css des pages HTML est rendu par {@code theme}. */
    public ManuelGenerator(ManuelTheme theme) {
        this.theme = theme;
    }
 
    /**
     * Génère le manuel complet.
     *
     * @param repManuel   répertoire cible du bundle (remplacé atomiquement)
     * @param sources     documents sources ordonnés (doivent exister)
     * @param titreProjet titre du projet (affiché dans l'en-tête du viewer)
     * @param version     version du manuel
     * @param titreAide   titre de l'aide (page d'accueil et sommaire)
     * @param log         journal de progression (appelé depuis le thread de génération)
     * @return le chemin du répertoire du manuel généré
     */
    public Path generate(Path repManuel, List<Path> sources, String titreProjet, String version,
                         String titreAide, Consumer<String> log) throws IOException {
        if (repManuel == null) {
            throw new IOException("Répertoire cible du manuel non renseigné.");
        }
        if (sources == null || sources.isEmpty()) {
            throw new IOException("Aucun document source à convertir.");
        }
        for (Path src : sources) {
            if (!Files.isRegularFile(src)) {
                throw new IOException("Fichier source introuvable : " + src);
            }
            if (!ConverterFactory.isSupported(src.getFileName().toString())) {
                throw new IOException("Format non supporté : " + src.getFileName());
            }
        }
 
        final Path tmp = repManuel.resolveSibling(repManuel.getFileName().toString() + ".tmp");
        final Path old = repManuel.resolveSibling(repManuel.getFileName().toString() + ".old");
 
        // Nettoyage d'un éventuel reliquat d'une génération interrompue.
        deleteRecursively(tmp);
        deleteRecursively(old);
        Files.createDirectories(tmp);
        Path tmpSources = tmp.resolve(SOURCES_DIR);
        Files.createDirectories(tmpSources);
 
        // 1. Copie des documents importés (le bundle reste régénérable sans les originaux).
        List<Path> copies = new ArrayList<>();
        for (Path src : sources) {
            Path copy = tmpSources.resolve(src.getFileName().toString());
            Files.copy(src, copy, StandardCopyOption.REPLACE_EXISTING);
            copies.add(copy);
        }
 
        // 2. Conversion des copies en pages HTML.
        List<ConversionResult> results = new ArrayList<>();
        int order = 1;
        for (Path copy : copies) {
            log.accept("Conversion de " + copy.getFileName() + "…");
            results.add(ConverterFactory.forFile(copy, tmp).convert(copy, order++));
        }
 
        // 3. Habillage du bundle : styles (thème), page d'accueil + navigation, manifeste du viewer.
        log.accept("Écriture des styles et de la navigation…");
        HelpStyles.writeStyles(tmp, theme);
        NavBuilder nav = new NavBuilder();
        String home = nav.build(tmp, titreAide, results);
        new HelpExporter().writeToc(tmp, titreAide, results, home, titreProjet, version);
 
        // 4. Manifeste des sources (rechargement de l'écran d'administration entre sessions).
        SourcesManifest manifest = new SourcesManifest();
        for (Path copy : copies) {
            manifest.fichiers.add(copy.getFileName().toString());
        }
        manifest.titreProjet = titreProjet;
        manifest.version = version;
        manifest.titreAide = titreAide;
        manifest.dateGeneration = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").format(new Date());
        manifest.save(tmpSources);
 
        // 5. Swap : l'ancien manuel n'est supprimé qu'une fois le nouveau complet.
        log.accept("Publication du manuel dans " + repManuel + "…");
        if (Files.exists(repManuel)) {
            Files.move(repManuel, old);
        }
        try {
            Files.move(tmp, repManuel);
        } catch (IOException e) {
            // Restaure l'ancien manuel si la publication échoue.
            if (Files.exists(old) && !Files.exists(repManuel)) {
                Files.move(old, repManuel);
            }
            throw e;
        }
        deleteRecursively(old);
 
        log.accept("Manuel généré : " + results.size() + " document(s).");
        return repManuel;
    }
 
    private static void deleteRecursively(Path dir) throws IOException {
        if (!Files.exists(dir)) {
            return;
        }
        try (Stream<Path> walk = Files.walk(dir)) {
            List<Path> paths = walk.sorted(Comparator.reverseOrder()).toList();
            for (Path p : paths) {
                Files.delete(p);
            }
        }
    }
}