InjectJavadocThemeMojo.java

package network.ike.plugin;

import org.apache.maven.api.plugin.MojoException;
import org.apache.maven.api.plugin.annotations.Mojo;
import org.apache.maven.api.plugin.annotations.Parameter;

import java.io.File;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Overlay the IKE green stylesheet onto a generated Javadoc apidocs tree.
 *
 * <p>Java 25's stock Javadoc stylesheet exposes a {@code :root} block of
 * CSS custom properties — colours, fonts, spacing. This mojo writes a
 * companion stylesheet {@code resource-files/ike-theme.css} that
 * overrides only those custom properties to the
 * {@code sentry-green} palette used by the project Maven site, then
 * injects a second {@code <link rel="stylesheet">} tag into every HTML
 * page so the override cascades over the stock stylesheet.
 *
 * <p>Same pattern as {@link InjectBreadcrumbMojo} for JaCoCo reports:
 * a single in-source CSS string is the source of truth, written into
 * the apidocs output after the javadoc tool has produced its files,
 * and the existing default stylesheet is left untouched (resilient to
 * Java version updates).
 *
 * <p>Usage in the IKE release flow:
 * <pre>
 * mvn site                                  # produces target/site/apidocs/
 * mvn ike:inject-javadoc-theme              # writes ike-theme.css and patches HTML
 * mvn site:stage                            # collects target/site → target/staging
 * </pre>
 *
 * <p>IKE-Network/ike-issues#518.
 */
@Mojo(name = IkeGoal.NAME_INJECT_JAVADOC_THEME,
      defaultPhase = "site")
public class InjectJavadocThemeMojo implements org.apache.maven.api.plugin.Mojo {

    @org.apache.maven.api.di.Inject
    private org.apache.maven.api.plugin.Log log;
    /**
     * Access the Maven logger.
     *
     * @return the logger
     */
    protected org.apache.maven.api.plugin.Log getLog() { return log; }

    /** Directory containing generated Javadoc HTML reports. */
    @Parameter(property = "targetDir",
               defaultValue = "${project.build.directory}/site/apidocs")
    File targetDir;

    /** Creates this goal instance. */
    public InjectJavadocThemeMojo() {}

    @Override
    public void execute() throws MojoException {
        if (!targetDir.isDirectory()) {
            getLog().info("inject-javadoc-theme: directory does not exist, "
                    + "skipping — " + targetDir);
            return;
        }

        try {
            writeThemeCss(targetDir.toPath());
        } catch (IOException e) {
            throw new MojoException(
                    "Failed to write Javadoc theme CSS in " + targetDir, e);
        }

        int patched;
        try {
            patched = processDirectory(targetDir.toPath());
        } catch (IOException e) {
            throw new MojoException(
                    "Failed to inject Javadoc theme into " + targetDir, e);
        }

        if (patched > 0) {
            getLog().info("inject-javadoc-theme: patched " + patched
                    + " HTML file(s) in " + targetDir);
        } else {
            getLog().info("inject-javadoc-theme: no stylesheet link to "
                    + "patch in " + targetDir);
        }
    }

    /**
     * Write the IKE theme CSS into the Javadoc {@code resource-files}
     * directory. That directory holds the stock {@code stylesheet.css}
     * the javadoc tool generates; placing {@code ike-theme.css} as a
     * sibling lets the {@code <link>} URLs that pages already use
     * (varied by depth — {@code resource-files/}, {@code ../resource-files/},
     * etc.) point at our override with the same relative-path machinery.
     *
     * @param apidocsDir root of the generated apidocs tree
     * @throws IOException on I/O failure
     */
    private void writeThemeCss(Path apidocsDir) throws IOException {
        String css = generateThemeCss();
        Path resources = apidocsDir.resolve("resource-files");
        if (Files.isDirectory(resources)) {
            Files.writeString(resources.resolve("ike-theme.css"), css);
        }
    }

    /**
     * Recursively process all HTML files in {@code dir}, injecting
     * a stylesheet link to {@code ike-theme.css} after the existing
     * link to {@code stylesheet.css}.
     *
     * @param dir directory to walk
     * @return number of files that were modified
     * @throws IOException on I/O failure
     */
    private int processDirectory(Path dir) throws IOException {
        int count = 0;
        try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
            for (Path entry : stream) {
                if (Files.isDirectory(entry)) {
                    count += processDirectory(entry);
                } else if (entry.toString().endsWith(".html")) {
                    String html = Files.readString(entry);
                    String patched = injectThemeCssLink(html);
                    if (!html.equals(patched)) {
                        Files.writeString(entry, patched);
                        count++;
                    }
                }
            }
        }
        return count;
    }

    // ── Pure testable functions ──────────────────────────────────────

    /**
     * Pattern matching the stock Javadoc stylesheet link. The capture
     * group holds the relative-path prefix (varies by page depth —
     * empty for {@code apidocs/index.html}, {@code ../} for one level
     * down, etc.) so the injected ike-theme.css link uses the same
     * prefix.
     */
    private static final Pattern STYLESHEET_LINK = Pattern.compile(
            "(<link rel=\"stylesheet\" type=\"text/css\" "
                    + "href=\")([^\"]*?)resource-files/stylesheet\\.css\">");

    /**
     * Inject a stylesheet link to {@code resource-files/ike-theme.css}
     * after the existing link to {@code resource-files/stylesheet.css}.
     * The injected URL inherits the same relative-path prefix as the
     * stock stylesheet, so it resolves correctly from any page depth.
     *
     * @param html the HTML content
     * @return HTML with the theme link injected, or unchanged if no
     *         stock stylesheet link is present
     */
    public static String injectThemeCssLink(String html) {
        Matcher m = STYLESHEET_LINK.matcher(html);
        if (!m.find()) {
            return html;
        }
        // Already injected — avoid duplicating on re-run.
        if (html.contains("resource-files/ike-theme.css")) {
            return html;
        }
        String prefix = m.group(2);
        String injection = m.group()
                + "<link rel=\"stylesheet\" type=\"text/css\" "
                + "href=\"" + prefix + "resource-files/ike-theme.css\">";
        return m.replaceFirst(Matcher.quoteReplacement(injection));
    }

    /**
     * Generate the IKE theme override CSS for Javadoc.
     *
     * <p>Overrides only the {@code :root} custom properties exposed by
     * the stock Java 25 stylesheet — the cascade does the rest. Palette
     * constants come from {@link IkePalette} so this theme,
     * {@link InjectBreadcrumbMojo}'s, and {@code ike-base-parent}'s
     * {@code site.css} all share one source.
     *
     * @return CSS content as a string
     */
    public static String generateThemeCss() {
        return """
                /* IKE Theme Override for Javadoc Reports                  */
                /* Overrides the Java 25 default stylesheet's :root        */
                /* custom properties to the sentry-green palette.          */
                /* Palette source: network.ike.plugin.IkePalette.          */

                :root {
                    --navbar-background-color: %VERDANT%;
                    --navbar-text-color: #ffffff;

                    --subnav-background-color: %CLOUD%;
                    --subnav-link-color: %SEA%;
                    --member-heading-background-color: var(--subnav-background-color);

                    --selected-background-color: %ACCENT%;
                    --selected-text-color: %TWILIGHT%;
                    --selected-link-color: %SEA%;

                    --table-header-color: %CLOUD%;
                    --title-color: %TWILIGHT%;

                    --link-color: %SEA%;
                    --link-color-active: %ACCENT%;

                    --toc-highlight-color: var(--subnav-background-color);
                    --toc-hover-color: %CLOUD%;
                }
                """
                .replace("%VERDANT%", IkePalette.VERDANT)
                .replace("%TWILIGHT%", IkePalette.TWILIGHT)
                .replace("%CLOUD%", IkePalette.CLOUD)
                .replace("%SEA%", IkePalette.SEA)
                .replace("%ACCENT%", IkePalette.ACCENT);
    }
}