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