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. * *

La génération s'effectue dans un dossier temporaire sibling * ({@code .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 /sources} avec le * manifeste {@link SourcesManifest} pour rester régénérables.

* *

À exécuter hors du thread JavaFX (les convertisseurs PowerPoint rendent * les diapositives en PNG via AWT).

*/ 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 sources, String titreProjet, String version, String titreAide, Consumer 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 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 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 walk = Files.walk(dir)) { List paths = walk.sorted(Comparator.reverseOrder()).toList(); for (Path p : paths) { Files.delete(p); } } } }