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);
}
}
}
}