All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.yupiik.tools.slides.Slides Maven / Gradle / Ivy

Go to download

Extracts slides logic in a reusable module. Note that it requires an explicit configuration for now.

There is a newer version: 1.2.3
Show newest version
/*
 * Copyright (c) 2020 - Yupiik SAS - https://www.yupiik.com
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package io.yupiik.tools.slides;

import io.yupiik.tools.common.http.StaticHttpServer;
import io.yupiik.tools.common.watch.Watch;
import io.yupiik.tools.slides.slider.RevealjsSlider;
import io.yupiik.tools.slides.slider.Slider;
import lombok.RequiredArgsConstructor;
import org.asciidoctor.Asciidoctor;
import org.asciidoctor.AttributesBuilder;
import org.asciidoctor.Options;
import org.asciidoctor.OptionsBuilder;
import org.asciidoctor.SafeMode;

import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;

import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;

@RequiredArgsConstructor
public class Slides implements Runnable {
    private final SlidesConfiguration configuration;

    @Override
    public void run() {
        final var slider = configuration.getSlider().getSupplier().get();
        doRun(slider, createOptions(slider), configuration.getAsciidoctor());
    }

    private void doRun(final Slider slider, final Options options, final Asciidoctor adoc) {
        final var mode = getMode();
        switch (mode) {
            case DEFAULT:
                render(options, adoc, slider);
                break;
            case SERVE:
                final AtomicReference server = new AtomicReference<>();
                final Watch watch = new Watch(
                        configuration.getLogInfo(), configuration.getLogDebug(), configuration.getLogDebugWithException(), configuration.getLogError(),
                        getWatchedPath(), options, adoc, configuration.getWatchDelay(),
                        (o, a) -> render(o, a, slider), () -> onFirstRender(server.get()));
                final StaticHttpServer staticHttpServer = new StaticHttpServer(
                        configuration.getLogInfo(), (m, e) -> configuration.getLogError().accept(m),
                        configuration.getPort(), configuration.getTargetDirectory(),
                        configuration.getSource().getFileName().toString().replaceFirst(".adoc$", ".html"),
                        watch);
                server.set(staticHttpServer);
                server.get().run();
                break;
            case WATCH:
                new Watch(
                        configuration.getLogInfo(), configuration.getLogDebug(), configuration.getLogDebugWithException(), configuration.getLogError(),
                        getWatchedPath(), options, adoc, configuration.getWatchDelay(),
                        (o, a) -> render(o, a, slider), () -> onFirstRender(null))
                        .run();
                break;
            default:
                throw new IllegalArgumentException("Unsupported mode '" + mode + "'");
        }
    }

    private List getWatchedPath() {
        if (configuration.getSource().getParent() == null) {
            configuration.getLogInfo().accept("Watching '" + configuration.getSource() + "'");
            return List.of(configuration.getSource());
        }
        final var parent = configuration.getSource().getParent();
        final List watched = new ArrayList<>();
        watched.add(parent);
        if (configuration.getSynchronizationFolders() != null) {
            for (final var synchronization : configuration.getSynchronizationFolders()) { // no lambda since we really iterate
                if (synchronization.getSource() == null) {
                    continue;
                }
                final var src = synchronization.getSource().toPath().normalize().toAbsolutePath();
                if (watched.stream().anyMatch(src::startsWith)) { // already watched
                    continue;
                }
                watched.removeIf(it -> it.startsWith(src)); // reverse test on already added dirs
                watched.add(src);
            }
        }
        if (configuration.getCustomScripts() != null) {
            for (final var js : configuration.getCustomScripts()) { // no lambda since we really iterate
                if (isRemote(js)) {
                    continue;
                }
                final var script = configuration.getSource().getParent().resolve(js);
                if (watched.stream().anyMatch(script::startsWith)) { // already watched
                    continue;
                }
                // no need to remove since it is files
                watched.add(script);
            }
        }
        watched.addAll(Stream.of(configuration.getCustomCss(), configuration.getTemplateDirs())
                .filter(Objects::nonNull)
                .filter(it -> watched.stream().noneMatch(it::startsWith))
                .collect(toList()));
        configuration.getLogInfo().accept("Watching " + watched);
        return watched;
    }

    private boolean isRemote(final String js) {
        return js.startsWith("//") || js.startsWith("http:") || js.startsWith("https:");
    }

    protected SlidesConfiguration.Mode getMode() {
        return configuration.getMode();
    }

    protected void onFirstRender(final StaticHttpServer server) {
        // no-op
    }

    private synchronized void render(final Options options, final Asciidoctor adoc, final Slider slider) {
        adoc.convertFile(configuration.getSource().toFile(), options);
        slider.postProcess(
                toOutputPath(),
                configuration.getCustomCss() != null ? configuration.getCustomCss() : null,
                configuration.getTargetDirectory(),
                configuration.getCustomScripts(),
                options);
        if (configuration.getSynchronizationFolders() != null) {
            configuration.getSynchronizationFolders().forEach(s -> {
                final Path root = s.getSource().toPath();
                if (Files.exists(root)) {
                    try {
                        Files.walkFileTree(root, new SimpleFileVisitor() {
                            @Override
                            public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
                                final String relative = root.relativize(file).toString();
                                final Path target = configuration.getTargetDirectory().resolve(s.getTarget()).resolve(relative);
                                if (target.getParent() != null) {
                                    Files.createDirectories(target.getParent());
                                }
                                Files.copy(file, target, StandardCopyOption.REPLACE_EXISTING);
                                return super.visitFile(file, attrs);
                            }
                        });
                    } catch (final IOException e) {
                        throw new IllegalStateException(e);
                    }
                }
            });
        }
        if (configuration.getCustomScripts() != null) {
            try {
                final var targetBase = Files.createDirectories(configuration.getTargetDirectory());
                Stream.of(configuration.getCustomScripts()).filter(it -> !isRemote(it)).forEach(script -> {
                    final var target = targetBase.resolve(targetBase.resolve(script)).normalize().toAbsolutePath();
                    try {
                        Files.createDirectories(target.getParent());
                        Files.copy(configuration.getSource().getParent().resolve(script), target, StandardCopyOption.REPLACE_EXISTING);
                    } catch (final IOException e) {
                        throw new IllegalArgumentException(e);
                    }
                });
            } catch (final IOException e) {
                throw new IllegalStateException(e);
            }
        }
        configuration.getLogInfo().accept("Rendered '" + configuration.getSource().getFileName() + "'");
    }

    private Options createOptions(final Slider slider) {
        // ensure js is copied
        slider.js().forEach(js -> stage(configuration.getWorkDir().resolve(js).normalize(), "js/"));

        // ensure images are copied
        Stream.of("background", "title").forEach(it ->
                stage(configuration.getWorkDir().resolve("slides/" + it + "." + slider.imageDir() + ".svg").normalize(), "img/"));

        // copy favicon
        stage(configuration.getWorkDir().resolve("slides/favicon.ico").normalize(), "img/");

        // finally create the options now the target folder is ready
        final OptionsBuilder base = OptionsBuilder.options()
                .safe(SafeMode.UNSAFE)
                .backend(slider.backend())
                .inPlace(false)
                .toDir(configuration.getTargetDirectory().toFile())
                .destinationDir(configuration.getTargetDirectory().toFile())
                .mkDirs(true)
                .toFile(toOutputPath().toFile())
                .baseDir(configuration.getSource().getParent().toAbsolutePath().normalize().toFile())
                .attributes(slider.append(AttributesBuilder.attributes()
                        .linkCss(false)
                        .dataUri(true)
                        .attribute("stem")
                        .attribute("favicon", "img/favicon.ico")
                        .attribute("source-highlighter", "highlightjs")
                        .attribute("highlightjsdir", "//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.1")
                        .attribute("highlightjs-theme", "//cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.1/styles/idea.min.css")
                        .attribute("customcss", findCss(slider))
                        .attribute("partialsdir", configuration.getSource().getParent().resolve("_partials").toAbsolutePath().normalize().toString())
                        .attribute("imagesdir", configuration.getSource().getParent().resolve("images").toAbsolutePath().normalize().toString())
                        .attributes(configuration.getAttributes() == null ? Map.of() : configuration.getAttributes())));

        final Path builtInTemplateDir = configuration.getWorkDir().resolve("slides/template." + slider.templateDir());
        if (configuration.getTemplateDirs() == null) {
            base.templateDirs(builtInTemplateDir.toFile());
        } else {
            base.templateDirs(Stream.of(builtInTemplateDir, configuration.getTemplateDirs()).filter(Files::exists).map(Path::toFile).toArray(File[]::new));
        }
        return base.get();
    }

    private void stage(final Path src, final String outputFolder) {
        if (Files.exists(src)) {
            final String relative = outputFolder + src.getFileName();
            final Path target = configuration.getTargetDirectory().resolve(relative);
            try {
                Files.createDirectories(target.getParent());
                Files.copy(src, target, StandardCopyOption.REPLACE_EXISTING);
            } catch (final IOException e) {
                throw new IllegalStateException(e);
            }
        }
    }

    private Path toOutputPath() {
        return configuration.getTargetDirectory()
                .resolve(configuration.getSource().getFileName().toString().replaceFirst(".adoc$", ".html"));
    }

    private String findCss(final Slider slider) {
        final var css = (RevealjsSlider.class.isInstance(slider) && configuration.getCustomCss() != null ?
                Stream.of(configuration.getCustomCss()) :
                Stream.concat(
                        slider.css().map(it -> configuration.getWorkDir().resolve(it).normalize()),
                        configuration.getCustomCss() != null && Files.exists(configuration.getCustomCss()) ?
                                Stream.of(configuration.getCustomCss()) : Stream.empty()))
                .collect(toList());

        if (css.size() == 1) {
            final var cssSource = css.iterator().next();
            final String relative = "css/" + cssSource.getFileName();
            final Path target = configuration.getTargetDirectory().resolve(relative);
            try {
                Files.createDirectories(target.getParent());
                Files.copy(cssSource, target, StandardCopyOption.REPLACE_EXISTING);
            } catch (final IOException e) {
                throw new IllegalStateException(e);
            }
            return relative;
        }
        // merge them all in one
        final var content = css.stream()
                .map(it -> {
                    try {
                        return Files.readString(it);
                    } catch (final IOException e) {
                        throw new IllegalArgumentException(e);
                    }
                })
                .collect(joining("\n"));
        final String relative = "css/slides.generated." + Math.abs(content.hashCode()) + ".css";
        final Path target = configuration.getTargetDirectory().resolve(relative);
        try {
            Files.createDirectories(target.getParent());
            Files.writeString(target, content);
        } catch (final IOException e) {
            throw new IllegalStateException(e);
        }
        return relative;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy