This is an automated email from the ASF dual-hosted git repository.

ddekany pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/freemarker-docgen.git

commit ade43f68e8da006395fea910b89c20b45f6e7076
Author: ddekany <[email protected]>
AuthorDate: Sat Jun 21 20:53:28 2025 +0200

    Added Pagefind support (and some other *.less cleanup/fixing). More work is 
needed to improve the pagefind index content.
---
 .../freemarker/docgen/core/ExceptionUtils.java}    |  53 +++-----
 .../org/freemarker/docgen/core/PageFindRunner.java | 150 +++++++++++++++++++++
 .../java/org/freemarker/docgen/core/Transform.java |  30 ++++-
 .../org/freemarker/docgen/core/less/lib/base.less  |   2 +-
 .../core/less/lib/components/search-form.less      |   6 +-
 .../docgen/core/less/lib/layout/content.less       |   4 +-
 .../docgen/core/less/lib/layout/header.less        |  23 ++--
 .../freemarker/docgen/core/less/lib/pagefind.less  |  69 ++++++++++
 .../freemarker/docgen/core/less/lib/utilities.less |   4 +-
 .../freemarker/docgen/core/less/lib/variables.less |   7 +
 .../org/freemarker/docgen/core/less/styles.less    |   3 +
 .../freemarker/docgen/core/statics/js/pagefind.js  | 110 +++++++++++++++
 .../freemarker/docgen/core/templates/header.ftlh   |  42 +++---
 .../org/freemarker/docgen/core/templates/page.ftlh |   6 +
 14 files changed, 439 insertions(+), 70 deletions(-)

diff --git 
a/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/utilities.less
 
b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/ExceptionUtils.java
similarity index 53%
copy from 
freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/utilities.less
copy to 
freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/ExceptionUtils.java
index 308b12c..f751133 100644
--- 
a/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/utilities.less
+++ 
b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/ExceptionUtils.java
@@ -6,9 +6,9 @@
  * to you 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
@@ -16,40 +16,25 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-// determines the max-width of the site at various resolutions
-.site-width {
-  padding-left: @gutter-width / 4 * 3;
-  padding-right: @gutter-width / 4 * 3;
 
-  @media (min-width: @screen-sm-min) {
-    padding-left: @gutter-width;
-    padding-right: @gutter-width;
-  }
+package org.freemarker.docgen.core;
 
-  @media (min-width: @screen-lg-min) {
-    margin-left: auto;
-    margin-right: auto;
-    width: @container-lg;
-  }
-}
-
-// Only display content to screen readers
-// See: http://a11yproject.com/posts/how-to-hide-content/
-.sr-only {
-  position: absolute;
-  width: 1px;
-  height: 1px;
-  margin: -1px;
-  padding: 0;
-  overflow: hidden;
-  clip: rect(0,0,0,0);
-  border: 0;
-}
+final class ExceptionUtils {
+    private ExceptionUtils() {
+        throw new AssertionError();
+    }
 
-.center-img {
-  text-align: center;
+    static String toCauseTrace(Throwable e) {
+        StringBuilder sb = new StringBuilder();
+        Throwable currentE = e;
+        while (currentE != null) {
+            if (sb.length() != 0) {
+                sb.append("\nCaused by: ");
+            }
+            sb.append(currentE);
 
-  img {
-    max-width: 100%;
-  }
+            currentE = currentE.getCause();
+        }
+        return sb.toString();
+    }
 }
diff --git 
a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/PageFindRunner.java
 
b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/PageFindRunner.java
new file mode 100644
index 0000000..3d8a4bd
--- /dev/null
+++ 
b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/PageFindRunner.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.freemarker.docgen.core;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Utility class to install Pagefind and run its indexing process
+ * as part of a Java build or site generation.
+ */
+final class PageFindRunner {
+    // See latest version here: https://www.npmjs.com/package/pagefind
+    private static final String PAGEFIND_VERSION = "1.3.0";
+
+    private final Path indexedDir;
+
+    /**
+     * Constructs a PageFindRunner instance.
+     *
+     * @param indexedDir The path to the directory containing the fully 
generated static HTML site
+     * that needs to be indexed by Pagefind.
+     * @param workDir    The path to a working directory where Node.js (npm) 
will install Pagefind.
+     * This directory will contain `node_modules` and potentially 
`package.json`.
+     */
+    private PageFindRunner(Path indexedDir) {
+        this.indexedDir = indexedDir.toAbsolutePath().normalize();
+    }
+
+    /**
+     * Static method to initiate the Pagefind installation and indexing 
process.
+     *
+     * @param indexedDir The path to the directory containing the fully 
generated static HTML site.
+     * @param workDir    The path to a working directory for Pagefind 
installation.
+     * @throws RuntimeException If an IOException, InterruptedException, or an 
error during
+     * Pagefind command execution occurs.
+     */
+    static void run(Path indexedDir) {
+        try {
+            new PageFindRunner(indexedDir).run();
+        } catch (IOException | InterruptedException e) {
+            // Wrap checked exceptions in a RuntimeException for simpler API 
usage
+            throw new RuntimeException("Pagefind execution failed (see cause 
exception)", e);
+        }
+    }
+
+    /**
+     * Executes the Pagefind installation and indexing.
+     */
+    private void run() throws IOException, InterruptedException {
+        if (!Files.isDirectory(indexedDir)) {
+            throw new IllegalArgumentException("Indexed directory must be a 
valid directory: " + indexedDir);
+        }
+
+        System.out.println("Invoking Pagefind on indexed directory: " + 
indexedDir);
+
+        int exitCode = runNodeProcess();
+        if (exitCode != 0) {
+            throw new RuntimeException("Pagefind indexing command failed with 
exit code: " + exitCode);
+        }
+
+        System.out.println("Pagefind indexing completed successfully.");
+    }
+
+    private int runNodeProcess() throws IOException, InterruptedException {
+        String npxCommand = findNpxCommand();
+
+        ProcessBuilder processBuilder = new ProcessBuilder(
+                npxCommand,
+                "pagefind@" + PAGEFIND_VERSION,
+                "--site",
+                indexedDir.toString()
+        );
+
+        // Just to have a consistent work directory...
+        processBuilder.directory(indexedDir.toFile());
+
+        // Redirect standard output and standard error to the Java 
application's console
+        processBuilder.inheritIO();
+
+        Process process = processBuilder.start();
+
+        return process.waitFor();
+    }
+
+    private String findNpxCommand() throws IOException, InterruptedException {
+        Map<String, Exception> failedAttempts = new LinkedHashMap<>();
+        for (String npxCommand : List.of("npx", "npx.cmd", "npx.ps1")) {
+            ProcessBuilder processBuilder = new ProcessBuilder(
+                    npxCommand, "--version"
+            );
+
+            // Just to have a consistent work directory...
+            processBuilder.directory(indexedDir.toFile());
+            try {
+                Process process = processBuilder.start();
+                String output = new 
String(process.getInputStream().readAllBytes());
+                process.waitFor();
+                if (process.exitValue() != 0) {
+                    throw new IllegalStateException(npxCommand + " --version 
command failed with exit code " + process.exitValue());
+                }
+                System.out.println(npxCommand + " --version output:\n" + 
output);
+                return npxCommand;
+            } catch (IOException e) {
+                failedAttempts.put(npxCommand, e);
+            }
+        }
+
+        StringBuilder sb = new StringBuilder("Can't find a working npx 
command. "
+                + "Ensure that Node.js is installed, and it's in the system 
path!\n");
+        for (Map.Entry<String, Exception> entry : failedAttempts.entrySet()) {
+            StringWriter stackTrace = new StringWriter();
+            sb.append("\nAttempted command: 
").append(entry.getKey()).append("\n")
+                    .append("Failed with: 
").append(ExceptionUtils.toCauseTrace(entry.getValue()))
+                    .append("\n");
+        }
+        throw new IOException(sb.toString());
+    }
+
+    // For testing purposes only!
+    public static void main(String[] args) {
+        if (args.length != 1) {
+            throw new IllegalArgumentException("Command-line arguments should 
be: <indexedDir>");
+        }
+        PageFindRunner.run(Path.of(args[0]));
+    }
+}
diff --git 
a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/Transform.java
 
b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/Transform.java
index a7f6f61..7c45efd 100644
--- 
a/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/Transform.java
+++ 
b/freemarker-docgen-core/src/main/java/org/freemarker/docgen/core/Transform.java
@@ -142,6 +142,7 @@ public final class Transform {
     static final String SETTING_GENERATE_ECLIPSE_TOC = "generateEclipseTOC";
     static final String SETTING_SHOW_XXE_LOGO = "showXXELogo";
     static final String SETTING_SEARCH_KEY = "searchKey";
+    static final String SETTING_PAGEFIND_BASED_SEARCH = "pagefindBasedSearch";
     static final String SETTING_DISABLE_JAVASCRIPT = "disableJavaScript";
     static final String SETTING_TIME_ZONE = "timeZone";
     static final String SETTING_LOCALE = "locale";
@@ -226,6 +227,10 @@ public final class Transform {
             = SETTING_SHOW_XXE_LOGO;
     private static final String VAR_SEARCH_KEY
             = SETTING_SEARCH_KEY;
+    private static final String VAR_PAGEFIND_BASED_SEARCH
+            = SETTING_PAGEFIND_BASED_SEARCH;
+    private static final String VAR_SHOW_SEARCH_FORM
+            = "hasSearch";
     private static final String VAR_DISABLE_JAVASCRIPT
             = SETTING_DISABLE_JAVASCRIPT;
     private static final String VAR_ECLIPSE_LINK_TO = SETTING_ECLIPSE_LINK_TO;
@@ -426,8 +431,12 @@ public final class Transform {
 
     private String searchKey;
 
+    private boolean hasSearch;
+
     private boolean disableJavaScript;
 
+    private boolean pagefindBasedSearch;
+
     private boolean validate = true;
 
     private Locale locale = Locale.US;
@@ -561,6 +570,7 @@ public final class Transform {
         if (!offline && searchKey != null) {
             generateSearchResultsHtmlFile(doc);
             htmlFileCounter++;
+            pagefindBasedSearch = true;
         }
 
         copyStandardStatics();
@@ -571,6 +581,12 @@ public final class Transform {
             generateEclipseTOC(doc);
         }
 
+        // Unfortunately, Pagefind doesn't work offline, as it uses ES6 
modules.
+        if (!offline && pagefindBasedSearch) {
+            PageFindRunner.run(destDir.toPath());
+            pagefindBasedSearch = true;
+        }
+
         logger.info(
                 "Done: "
                 + htmlFileCounter + " HTML-s + "
@@ -832,6 +848,8 @@ public final class Transform {
                     showXXELogo = castSetting(settingName, settingValue, 
Boolean.class);
                 } else if (topSettingName.equals(SETTING_SEARCH_KEY)) {
                     searchKey = castSetting(settingName, settingValue, 
String.class);
+                }else if 
(topSettingName.equals(SETTING_PAGEFIND_BASED_SEARCH)) {
+                    pagefindBasedSearch = castSetting(settingName, 
settingValue, Boolean.class);
                 }else if (topSettingName.equals(SETTING_DISABLE_JAVASCRIPT)) {
                     disableJavaScript = castSetting(settingName, settingValue, 
Boolean.class);
                 } else if (topSettingName.equals(SETTING_CONTENT_DIRECTORY)) {
@@ -953,7 +971,7 @@ public final class Transform {
         TemplateLoader templateLoader = new ClassTemplateLoader(
                 Transform.class, "templates");
         File templatesDir = new File(srcDir, DIR_TEMPLATES);
-        if (!templatesDir.exists()) {
+        if (templatesDir.exists()) {
             templateLoader = new MultiTemplateLoader(
                     new TemplateLoader[] { new 
FileTemplateLoader(templatesDir), templateLoader });
         }
@@ -1120,6 +1138,8 @@ public final class Transform {
             fmConfig.setSharedVariable(VAR_SHOW_EDITORAL_NOTES, 
showEditoralNotes);
             fmConfig.setSharedVariable(VAR_SHOW_XXE_LOGO, showXXELogo);
             fmConfig.setSharedVariable(VAR_SEARCH_KEY, searchKey);
+            fmConfig.setSharedVariable(VAR_PAGEFIND_BASED_SEARCH, 
pagefindBasedSearch);
+            fmConfig.setSharedVariable(VAR_SHOW_SEARCH_FORM, 
pagefindBasedSearch);
             fmConfig.setSharedVariable(VAR_DISABLE_JAVASCRIPT, 
disableJavaScript);
             fmConfig.setSharedVariable(VAR_OLINKS, olinks);
             fmConfig.setSharedVariable(VAR_NUMBERED_SECTIONS, 
numberedSections);
@@ -1286,6 +1306,9 @@ public final class Transform {
         }
         if (!disableJavaScript) {
             copyCommonStatic("main.min.js");
+            if (pagefindBasedSearch) {
+                copyCommonStatic("js/pagefind.js");
+            }
         }
     }
 
@@ -1348,8 +1371,9 @@ public final class Transform {
                 }
 
                 Path destSubdir = destDir.toPath().resolve("docgen-resources");
-                Files.createDirectories(destSubdir);
-                Files.write(destSubdir.resolve(staticFileName), 
content.getBytes(fileCharset));
+                Path destFile = destSubdir.resolve(staticFileName);
+                Files.createDirectories(destFile.getParent());
+                Files.write(destFile, content.getBytes(fileCharset));
             } else {
                 FileUtil.copyResourceIntoFile(
                         Transform.class, "statics", staticFileName,
diff --git 
a/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/base.less
 
b/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/base.less
index 8fdd6be..e33d032 100644
--- 
a/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/base.less
+++ 
b/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/base.less
@@ -22,7 +22,7 @@ html {
   color: @text-color;
   line-height: 1.5;
   font-weight: normal;
-  background-color: #fff;
+  background-color: @background-color;
   text-size-adjust: 100%;
   height: 100%;
 }
diff --git 
a/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/components/search-form.less
 
b/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/components/search-form.less
index c3ed0b1..d068a81 100644
--- 
a/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/components/search-form.less
+++ 
b/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/components/search-form.less
@@ -42,7 +42,7 @@
     color: @brand-color;
     line-height: 1;
 
-    background-color: #fff;
+    background-color: @background-color;
     background-image: none;
 
     border: 0;
@@ -89,7 +89,7 @@
     font-size: 16px; // prevent zoom in on mobile
     line-height: 21px;
 
-    border: 1px solid #aec0d6;
+    border: 1px solid @input-border-color;
     border-radius: 0;
 
     box-sizing: border-box;
@@ -101,7 +101,7 @@
 
     &:focus {
       outline: 0;
-      border-color: @link-color;
+      border-color: @input-border-color-focus;
     }
   }
 }
diff --git 
a/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/layout/content.less
 
b/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/layout/content.less
index 3c2e087..2c3a741 100644
--- 
a/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/layout/content.less
+++ 
b/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/layout/content.less
@@ -57,7 +57,7 @@
     left: 0px;
     top: @hamburger-icon-top + @hamburger-icon-height;
     z-index: 100;
-    background: #fff; // TODO variable?
+    background: @background-color;
     border: 1px solid #000; // TODO variable?
     box-shadow: 5px 5px 5px 0px rgba(0,0,0,0.33);
 
@@ -74,7 +74,7 @@
       // For xs width:
       flex: 0 0 160px;
       max-width: 160px; // set max-width to prevent flicker
-      margin-right: @gutter-width / 4 * 3;
+      margin-right: (@gutter-width / 4 * 3);
     }
 
     @media (min-width: @screen-sm-min) {
diff --git 
a/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/layout/header.less
 
b/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/layout/header.less
index 6941237..a09adee 100644
--- 
a/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/layout/header.less
+++ 
b/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/layout/header.less
@@ -58,15 +58,21 @@
   z-index: 1;
 }
 
-.search-row {
-  padding-top: 12px;
+.navigation-header-font-size() {
+  font-size: 18px;
 
   @media (min-width: @screen-xs-min) {
-    display: flex;
-    justify-content: space-between;
-    flex-wrap: wrap;
-    align-items: flex-end;
+    font-size: 27px;
   }
+}
+
+.search-row {
+  padding-top: @header-panel-top-padding;
+
+  display: flex;
+  justify-content: space-between;
+  flex-wrap: wrap;
+  align-items: flex-end;
 
   .navigation-header,
   .search-form {
@@ -76,13 +82,10 @@
 
   .navigation-header {
     display: block;
-    font-size: 18px;
     line-height: 1;
     flex: 1 1 auto;
 
-    @media (min-width: @screen-xs-min) {
-      font-size: 27px;
-    }
+    .navigation-header-font-size();
 
     @media (min-width: @screen-sm-min) {
       flex-grow: 0;
diff --git 
a/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/pagefind.less
 
b/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/pagefind.less
new file mode 100644
index 0000000..9db5e78
--- /dev/null
+++ 
b/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/pagefind.less
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+#pagefind-panel {
+  padding-top: @header-panel-top-padding;
+  display: none;
+
+  --pagefind-ui-border-width: 0;
+  --pagefind-ui-border-radius: 0;
+  --pagefind-ui-text: @text-color;
+  --pagefind-ui-background: @background-color;
+  --pagefind-ui-font: @font-sans-serif;
+
+  input {
+    --pagefind-ui-border: @input-border-color;
+    --pagefind-ui-border-width: 1px;
+
+    &:focus {
+      outline: 0;
+      border-color: @input-border-color-focus;
+    }
+  }
+}
+
+#pagefind-panel-toggler {
+  .search-btn {
+    color: @brand-color;
+
+    background: none;
+
+    border: 0;
+    border-radius: 0;
+
+    padding: 0;
+
+    &:hover,
+    &:focus {
+      color: lighten(@link-color, 30%);
+      cursor: pointer;
+      outline: 0;
+    }
+
+    &::before {
+      .icon();
+      .icon-search();
+
+      box-sizing: border-box;
+      display: block;
+
+      .navigation-header-font-size();
+    }
+  }
+}
diff --git 
a/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/utilities.less
 
b/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/utilities.less
index 308b12c..d1a1af6 100644
--- 
a/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/utilities.less
+++ 
b/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/utilities.less
@@ -18,8 +18,8 @@
  */
 // determines the max-width of the site at various resolutions
 .site-width {
-  padding-left: @gutter-width / 4 * 3;
-  padding-right: @gutter-width / 4 * 3;
+  padding-left: (@gutter-width / 4 * 3);
+  padding-right: (@gutter-width / 4 * 3);
 
   @media (min-width: @screen-sm-min) {
     padding-left: @gutter-width;
diff --git 
a/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/variables.less
 
b/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/variables.less
index 64bbcdf..37c7283 100644
--- 
a/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/variables.less
+++ 
b/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/lib/variables.less
@@ -19,6 +19,8 @@
 // fonts
 @font-sans-serif: "Roboto", "Helvetica Neue", Arial, sans-serif;
 
+@background-color: #fff;
+
 // colors
 @brand-color: #0050b2;
 
@@ -29,6 +31,9 @@
 @link-hover: #0973f5;
 @link-visited: #AF09AF; //used for toc
 
+// form inputs
+@input-border-color: #aec0d6;
+@input-border-color-focus: @link-color;
 
 // headers
 @header-color: @brand-color;
@@ -64,3 +69,5 @@
 @breadcrumb-row-font-size-xs: 11px;
 @breadcrumb-row-font-size-sm: 13px;
 @breadcrumb-row-padding-top: 6px;
+
+@header-panel-top-padding: 12px;
diff --git 
a/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/styles.less
 
b/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/styles.less
index 613ad1b..df32f82 100644
--- 
a/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/styles.less
+++ 
b/freemarker-docgen-core/src/main/resources-gulp/org/freemarker/docgen/core/less/styles.less
@@ -49,3 +49,6 @@
 
 // generic utilities
 @import "lib/utilities.less";
+
+// pagefind
+@import "lib/pagefind.less";
diff --git 
a/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/statics/js/pagefind.js
 
b/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/statics/js/pagefind.js
new file mode 100644
index 0000000..2ba281b
--- /dev/null
+++ 
b/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/statics/js/pagefind.js
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/* jshint esversion: 6 */
+/* jshint browser: true */
+
+'use strict';
+
+var memoizedPagefindPanel = null;
+function getPagefindPanel() {
+    if (memoizedPagefindPanel == null) {
+        const pagefindPanel = document.getElementById('pagefind-panel');
+        if (!pagefindPanel) {
+            window.alert('Pagefind panel not found!');
+            throw new Error('Pagefind panel not found!');
+        }
+
+        memoizedPagefindPanel = pagefindPanel;
+    }
+
+    return memoizedPagefindPanel;
+}
+
+var memoizedPagefindInput = null;
+function getPagefindInput() {
+    if (memoizedPagefindInput == null) {
+        const pagefindInput = 
getPagefindPanel().querySelector('input[type="text"]');
+        if (!pagefindInput) {
+            window.alert('Search input not found in the Pagefind panel!');
+            throw new Error('Search input not found in the Pagefind panel!');
+        }
+
+        memoizedPagefindInput = pagefindInput;
+    }
+    return memoizedPagefindInput;
+}
+
+var memoizedPagefindPanelToggleButton = null;
+function getPagefindPanelToggleButton() {
+    if (memoizedPagefindPanelToggleButton == null) {
+        const button = document.getElementById('pagefind-panel-toggle-button');
+        if (!button) {
+            window.alert('Search toggle button not found!');
+            throw new Error('Search toggle button not found!');
+        }
+
+        memoizedPagefindPanelToggleButton = button;
+    }
+    return memoizedPagefindPanelToggleButton;
+}
+
+function hidePagefindPanel() {
+    getPagefindPanel().style.display = 'none';
+    getPagefindInput().blur();
+}
+
+function showPagefindPanel() {
+    getPagefindPanel().style.display = 'block';
+    getPagefindInput().focus();
+}
+
+function togglePagefindPanel() {
+    const pagefindPanel = getPagefindPanel();
+    if (pagefindPanel.style.display === 'none' || pagefindPanel.style.display 
=== '') {
+        showPagefindPanel();
+    } else {
+        hidePagefindPanel();
+    }
+}
+
+function addDocgenPagefindCustomizations() {
+    const searchInput = getPagefindInput();
+
+    // Input auto-hiding:
+    searchInput.addEventListener('blur', () => {
+        if (searchInput.value === '') {
+            hidePagefindPanel();
+        }
+    });
+    searchInput.addEventListener('keydown', (event) => {
+        if (event.key === 'Escape') {
+            hidePagefindPanel();
+        }
+    });
+
+    hidePagefindPanel();
+}
+
+window.addEventListener('DOMContentLoaded', (event) => {
+    /*jshint -W117 */
+    new PagefindUI({ element: '#pagefind-panel', showSubResults: true });
+    /*jshint -W117 */
+    addDocgenPagefindCustomizations();
+});
diff --git 
a/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/header.ftlh
 
b/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/header.ftlh
index ad5e96b..fb514de 100644
--- 
a/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/header.ftlh
+++ 
b/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/header.ftlh
@@ -60,7 +60,10 @@
 </#macro>
 
 <#macro navigationHeader>
-  <div class="header-bottom-bg"><#t>
+  <div class="header-bottom-bg">
+    <#if hasSearch && pagefindBasedSearch>
+      <div class="site-width" id="pagefind-panel"></div>
+    </#if>
     <div class="site-width search-row"><#t>
       <#local book = .node?root.*>
       <a href="${CreateLinkFromNode(book)}" class="navigation-header"><#t>
@@ -68,24 +71,33 @@
       </a><#t>
       <#-- empty div to maintain layout -->
       <div class="navigation-header"></div><#t>
-      <@searchForm /><#t>
-    </div><#t>
+      <@searchAccessor /><#t>
+    </div>
     <div class="site-width breadcrumb-row"><#t>
       <@nav.breadcrumb /><#t>
       <@nav.bookmarks /><#t>
-    </div><#t>
-  </div><#t>
+    </div>
+  </div>
 </#macro>
 
-<#macro searchForm>
-  <#if searchKey?? && !offline>
-    <form method="get" class="search-form<#if offline> offline</#if>" 
action="search-results.html"><#t>
-      <fieldset><#t>
-        <legend class="sr-only">Search form</legend><#t>
-        <label for="search-field" class="sr-only">Search query</label><#t>
-        <input id="search-field" name="q" type="search" class="search-input" 
placeholder="Search" spellcheck="false" autocorrect="off" 
autocomplete="off"><#t>
-        <button type="submit" class="search-btn"><span 
class="sr-only">Search</span></button><#t>
-      </fieldset><#t>
-    </form><#t>
+<#macro searchAccessor>
+  <#if hasSearch>
+    <#if pagefindBasedSearch>
+      <div id="pagefind-panel-toggler"><#t>
+        <button onclick="togglePagefindPanel()" 
id="pagefind-panel-toggle-button" class="search-btn"><span 
class="sr-only">Search</span></button>
+      </div>
+    <#else>
+      <div class="search-form"><#t>
+        <div id="search-field"></div>
+        <form method="get" action="search-results.html"><#t>
+          <fieldset><#t>
+            <legend class="sr-only">Search form</legend><#t>
+            <label for="search-field" class="sr-only">Search query</label><#t>
+            <input id="search-field" name="q" type="search" 
class="search-input" placeholder="Search" spellcheck="false" autocorrect="off" 
autocomplete="off"><#t>
+            <button type="submit" class="search-btn"><span 
class="sr-only">Search</span></button><#t>
+          </fieldset><#t>
+        </form><#t>
+      </div>
+    </#if>
   </#if>
 </#macro>
diff --git 
a/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/page.ftlh
 
b/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/page.ftlh
index 60c7a84..c5be7b2 100644
--- 
a/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/page.ftlh
+++ 
b/freemarker-docgen-core/src/main/resources/org/freemarker/docgen/core/templates/page.ftlh
@@ -80,6 +80,12 @@
       <#if !offline && cookieConsentScriptURL??>
         <script type="text/javascript" 
src="${cookieConsentScriptURL}"></script>
       </#if>
+
+      <#if hasSearch && pagefindBasedSearch>
+        <link href="pagefind/pagefind-ui.css" rel="stylesheet">
+        <script src="pagefind/pagefind-ui.js"></script>
+        <script src="docgen-resources/js/pagefind.js"></script>
+      </#if>
     </head>
   </#compress>
 </#macro>

Reply via email to