This is an automated email from the ASF dual-hosted git repository.
jbonofre pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/karaf.git
The following commit(s) were added to refs/heads/main by this push:
new ad3339e103 [manual] enhance theme based on karaf-site proposal (#2905)
ad3339e103 is described below
commit ad3339e103447f8e68612c4bd5cda6b038624471
Author: Romain Manni-Bucau <[email protected]>
AuthorDate: Thu Sep 24 06:35:52 2026 +0200
[manual] enhance theme based on karaf-site proposal (#2905)
---
.gitignore | 10 +-
manual/pom.xml | 75 +-
.../developer-guide/karaf-maven-plugin.adoc | 1 -
.../src/main/asciidoc/user-guide/installation.adoc | 2 +-
.../main/asciidoc/user-guide/os-integration.adoc | 66 +-
.../main/frontend/_scss/_manual-components.scss | 778 +++++++++++++++++++++
manual/src/main/frontend/_scss/_tokens.scss | 74 ++
manual/src/main/frontend/_scss/karaf.scss | 21 +
manual/src/main/frontend/build-pdf.sh | 103 +++
manual/src/main/frontend/karaf-logo-light.svg | 17 +
manual/src/main/frontend/karaf-logo.svg | 17 +
manual/src/main/frontend/karaf-manual-footer.html | 169 +++++
manual/src/main/frontend/karaf-manual-head.html | 28 +
manual/src/main/frontend/karaf-pdf-theme.yml | 148 ++++
manual/src/main/frontend/package.json | 17 +
manual/src/main/frontend/postprocess.js | 235 +++++++
manual/src/main/frontend/render-html.js | 131 ++++
pom.xml | 7 +
18 files changed, 1851 insertions(+), 48 deletions(-)
diff --git a/.gitignore b/.gitignore
index 1931760383..0cba8f3cc8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,4 +9,12 @@ atlassian-ide-plugin.xml
bin
dependency-reduced-pom.xml
.apt_generated_tests
-.vscode
\ No newline at end of file
+.vscode
+
+# Manual frontend build (npm/sass)
+manual/src/main/frontend/node
+manual/src/main/frontend/node_modules
+manual/src/main/frontend/package-lock.json
+manual/src/main/asciidoc/karaf-manual.css
+node_modules
+
diff --git a/manual/pom.xml b/manual/pom.xml
index d4c7a5620b..9520f80c43 100644
--- a/manual/pom.xml
+++ b/manual/pom.xml
@@ -211,32 +211,52 @@
</dependencies>
</plugin>
<plugin>
- <groupId>org.asciidoctor</groupId>
-
<artifactId>asciidoctor-maven-plugin</artifactId>
- <version>3.2.0</version>
+ <groupId>com.github.eirslett</groupId>
+ <artifactId>frontend-maven-plugin</artifactId>
+ <configuration>
+
<workingDirectory>src/main/frontend</workingDirectory>
+ <nodeVersion>v22.15.0</nodeVersion>
+ <npmVersion>10.8.2</npmVersion>
+ </configuration>
<executions>
<execution>
- <id>output-html</id>
+ <id>install-node-and-npm</id>
+
<phase>generate-resources</phase>
+ <goals>
+
<goal>install-node-and-npm</goal>
+ </goals>
+ </execution>
+ <execution>
+ <id>npm-install</id>
+
<phase>generate-resources</phase>
+ <goals>
+ <goal>npm</goal>
+ </goals>
+ <configuration>
+
<arguments>install</arguments>
+ </configuration>
+ </execution>
+ <execution>
+ <id>npm-build</id>
<phase>generate-resources</phase>
<goals>
-
<goal>process-asciidoc</goal>
+ <goal>npm</goal>
</goals>
<configuration>
-
<doctype>article</doctype>
- <attributes>
-
<source-highlighter>coderay</source-highlighter>
- <toc />
-
<linkcss>false</linkcss>
- </attributes>
+ <arguments>run
build</arguments>
+ </configuration>
+ </execution>
+ <execution>
+ <id>npm-postprocess</id>
+ <phase>process-resources</phase>
+ <goals>
+ <goal>npm</goal>
+ </goals>
+ <configuration>
+ <arguments>run
postprocess</arguments>
</configuration>
</execution>
</executions>
- <configuration>
-
<sourceDirectory>src/main/asciidoc</sourceDirectory>
- <attributes>
-
<imagesdir>src/main/asciidoc/images</imagesdir>
- </attributes>
- </configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@@ -271,25 +291,20 @@
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
-
<id>attach-manual</id>
+
<id>build-manual-pdf</id>
<phase>install</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
- <tasks>
-
<mkdir dir="target" />
- <copy
file="${project.build.directory}/generated-docs/index.html"
tofile="target/documentation.html" />
- <echo message="Generating PDF using
Prince XML (http://www.princexml.com/)" />
-
<exec executable="prince">
-
<arg value="target/documentation.html" />
-
<arg value="target/documentation.pdf" />
-
<arg value="--log" />
-
<arg value="${project.build.directory}/prince.log" />
+ <target
xmlns:mvn="http://maven.apache.org/ANTRUN">
+
<exec executable="sh" dir="${project.basedir}">
+
<arg value="src/main/frontend/build-pdf.sh" />
</exec>
-
<attachartifact file="target/documentation.html" type="html" />
-
<attachartifact file="target/documentation.pdf" type="pdf" />
- </tasks>
+
<copy file="${project.build.directory}/generated-docs/index.html"
tofile="target/documentation.html" />
+
<mvn:attachartifact file="target/documentation.html" type="html" />
+
<mvn:attachartifact file="target/documentation.pdf" type="pdf" />
+
</target>
</configuration>
</execution>
</executions>
diff --git a/manual/src/main/asciidoc/developer-guide/karaf-maven-plugin.adoc
b/manual/src/main/asciidoc/developer-guide/karaf-maven-plugin.adoc
index 5b62598185..347a7bd0b9 100644
--- a/manual/src/main/asciidoc/developer-guide/karaf-maven-plugin.adoc
+++ b/manual/src/main/asciidoc/developer-guide/karaf-maven-plugin.adoc
@@ -195,7 +195,6 @@ This can be overridden by specifying the bundle in the
source feature.xml with t
|markTransitiveAsDependency
|boolean (false)
|Whether to mark transitive dependencies' with `dependency="true"`.
-|If true, the `src/main/history/dependencies.xml` file will be overwritten if
it has changed.
|===
====== Example
diff --git a/manual/src/main/asciidoc/user-guide/installation.adoc
b/manual/src/main/asciidoc/user-guide/installation.adoc
index 59c8dbb072..8685cd2f49 100644
--- a/manual/src/main/asciidoc/user-guide/installation.adoc
+++ b/manual/src/main/asciidoc/user-guide/installation.adoc
@@ -129,7 +129,7 @@ The installed karaf will run as user karaf, group karaf,
listening to the usual
[NOTE]
====
-When/if the
link:++https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=881297++[Debian
Request For Package (RFP) of karaf] results in an official karaf package
becoming available, the unofficial debian package will be discontinued.
+When/if the https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=881297[Debian
Request For Package (RFP) of karaf] results in an official karaf package
becoming available, the unofficial debian package will be discontinued.
====
==== Post-Installation steps
diff --git a/manual/src/main/asciidoc/user-guide/os-integration.adoc
b/manual/src/main/asciidoc/user-guide/os-integration.adoc
index ed6a16424b..85cf33c447 100644
--- a/manual/src/main/asciidoc/user-guide/os-integration.adoc
+++ b/manual/src/main/asciidoc/user-guide/os-integration.adoc
@@ -402,21 +402,57 @@ You may still need to customize the generated files to
adapt them to your enviro
The utility karaf-service.sh can be configured by defining environment
variables or by passing command line options:
-,===
-Command line option, Environment variable, Description
--k, KARAF_SERVICE_PATH , Karaf installation path (mandatory)
--d, KARAF_SERVICE_DATA , Karaf data path (default to
${KARAF_SERVICE_PATH}/data)
--c, KARAF_SERVICE_CONF , Karaf configuration file (default to
${KARAF_SERVICE_PATH/etc/${KARAF_SERVICE_NAME}.conf
--t, KARAF_SERVICE_ETC , Karaf etc path (default to
${KARAF_SERVICE_PATH/etc}
--p, KARAF_SERVICE_PIDFILE , Karaf pid path (default to
${KARAF_SERVICE_DATA}/${KARAF_SERVICE_NAME}.pid)
--n, KARAF_SERVICE_NAME , Karaf service name (default karaf)
--e, KARAF_ENV , Karaf environment variable (can be repeated)
--u, KARAF_SERVICE_USER , Karaf user
--g, KARAF_SERVICE_GROUP , Karaf group (default ${KARAF_SERVICE_USER)
--l, KARAF_SERVICE_LOG , Karaf console log (default to
${KARAF_SERVICE_DATA}/log/${KARAF_SERVICE_NAME}-console.log)
--f, KARAF_SERVICE_TEMPLATE , Template file to use
--x, KARAF_SERVICE_EXECUTABLE , Karaf executable name (defaul karaf, should
support daemon and stop commands)
-,===
+|===
+|Command line option | Environment variable | Description
+
+|`-k`
+|KARAF_SERVICE_PATH
+|Karaf installation path (mandatory)
+
+|`-d`
+|KARAF_SERVICE_DATA
+|Karaf data path (default to ${KARAF_SERVICE_PATH}/data)
+
+|`-c`
+|KARAF_SERVICE_CONF
+|Karaf configuration file (default to
${KARAF_SERVICE_PATH}/etc/${KARAF_SERVICE_NAME}.conf)
+
+|`-t`
+|KARAF_SERVICE_ETC
+|Karaf etc path (default to ${KARAF_SERVICE_PATH}/etc)
+
+|`-p`
+|KARAF_SERVICE_PIDFILE
+|Karaf pid path (default to ${KARAF_SERVICE_DATA}/${KARAF_SERVICE_NAME}.pid)
+
+|`-n`
+|KARAF_SERVICE_NAME
+|Karaf service name (default karaf)
+
+|`-e`
+|KARAF_ENV
+|Karaf environment variable (can be repeated)
+
+|`-u`
+|KARAF_SERVICE_USER
+|Karaf user
+
+|`-g`
+|KARAF_SERVICE_GROUP
+|Karaf group (default ${KARAF_SERVICE_USER})
+
+|`-l`
+|KARAF_SERVICE_LOG
+|Karaf console log (default to
${KARAF_SERVICE_DATA}/log/${KARAF_SERVICE_NAME}-console.log)
+
+|`-f`
+|KARAF_SERVICE_TEMPLATE
+|Template file to use
+
+|`-x`
+|KARAF_SERVICE_EXECUTABLE
+|Karaf executable name (default karaf, should support daemon and stop commands)
+|===
{nbsp} +
diff --git a/manual/src/main/frontend/_scss/_manual-components.scss
b/manual/src/main/frontend/_scss/_manual-components.scss
new file mode 100644
index 0000000000..036cc5c5f8
--- /dev/null
+++ b/manual/src/main/frontend/_scss/_manual-components.scss
@@ -0,0 +1,778 @@
+/*
+ * 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.
+ */
+// Apache Karaf manual theme - components.
+// Restyles Asciidoctor's generated HTML to match the karaf-site theme.
+// Layout: fixed top bar + sticky left ToC sidebar + content + prev/next rail.
+
+// ---------------------------------------------------------------------------
+// Base
+// ---------------------------------------------------------------------------
+html {
+ scroll-behavior: smooth;
+ scroll-padding-top: 5rem; // keep section anchors clear of the fixed header
+}
+
+body {
+ margin: 0;
+ background-color: var(--k-bg);
+ color: var(--k-text);
+ font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
+ "Helvetica Neue", Arial, sans-serif;
+ font-size: 1rem;
+ line-height: 1.6;
+ -webkit-font-smoothing: antialiased;
+ // clear the fixed 3.5rem header so the document title is visible
+ padding-top: 3.5rem;
+}
+
+a {
+ color: var(--k-accent);
+ text-decoration: none;
+
+ &:hover {
+ color: var(--k-accent-strong);
+ text-decoration: underline;
+ }
+}
+
+h1, h2, h3, h4, h5, h6 {
+ color: var(--k-text);
+ font-weight: 600;
+ line-height: 1.3;
+ margin: 1.6em 0 0.6em;
+}
+
+h1 { font-size: 2.25rem; letter-spacing: -0.02em; }
+h2 { font-size: 1.75rem; letter-spacing: -0.01em; padding-bottom: 0.4rem;
+ border-bottom: 1px solid var(--k-border); }
+h3 { font-size: 1.3rem; }
+h4 { font-size: 1.1rem; }
+h5 { font-size: 1rem; }
+h6 { font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.03em;
+ color: var(--k-muted); }
+
+p { margin: 0 0 1rem; }
+
+code {
+ font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas,
+ "Liberation Mono", monospace;
+ background-color: var(--k-code-bg);
+ color: var(--k-code-text);
+ padding: 0.15em 0.35em;
+ border-radius: 0.25em;
+ font-size: 0.875em;
+}
+
+pre {
+ background-color: var(--k-bg-subtle);
+ color: var(--k-text);
+ border: 1px solid var(--k-border);
+ border-radius: 0.5rem;
+ padding: 1rem;
+ overflow: auto;
+
+ code { background: none; color: inherit; padding: 0; }
+}
+
+// ---------------------------------------------------------------------------
+// Top header bar
+// ---------------------------------------------------------------------------
+.k-manual-header {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 1030;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ height: 3.5rem;
+ padding: 0 1.25rem;
+ background-color: var(--k-bg);
+ border-bottom: 1px solid var(--k-border);
+ box-shadow: var(--k-shadow-sm);
+}
+
+.k-manual-brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-weight: 700;
+ font-size: 1.15rem;
+ letter-spacing: -0.01em;
+ color: var(--k-text);
+
+ &:hover { color: var(--k-accent); text-decoration: none; }
+
+ // logo swap (light shows the dark-mode logo by default, like karaf-site)
+ .k-logo { height: 36px; width: auto; }
+
+ .k-logo.k-logo-light { display: none; }
+
+ .k-logo.k-logo-dark { display: inline-block; }
+}
+
+[data-bs-theme="dark"] .k-manual-brand {
+ .k-logo.k-logo-light { display: inline-block; }
+ .k-logo.k-logo-dark { display: none; }
+}
+
+.k-manual-toggle {
+ margin-left: auto;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 2rem;
+ height: 2rem;
+ border: 1px solid var(--k-border);
+ background: var(--k-surface-muted);
+ color: var(--k-text);
+ border-radius: 50%;
+ cursor: pointer;
+ flex: none;
+
+ svg { width: 1rem; height: 1rem; }
+
+ .icon-moon { display: none; }
+
+ &:hover { border-color: var(--k-accent); color: var(--k-accent); }
+}
+
+[data-bs-theme="dark"] .k-manual-toggle {
+ .icon-sun { display: none; }
+ .icon-moon { display: block; }
+}
+
+// ---------------------------------------------------------------------------
+// Header search box
+// ---------------------------------------------------------------------------
+.k-manual-search {
+ position: relative;
+ margin-left: auto;
+ margin-right: 0.75rem;
+ flex: 0 1 auto;
+ min-width: 0;
+ width: 16rem;
+}
+
+.k-manual-search-input {
+ width: 100%;
+ height: 2.1rem;
+ padding: 0 0.75rem;
+ border: 1px solid var(--k-border);
+ border-radius: 0.375rem;
+ background-color: var(--k-surface-muted);
+ color: var(--k-text);
+ font-size: 0.875rem;
+
+ &::placeholder { color: var(--k-muted); }
+
+ &:focus {
+ outline: none;
+ border-color: var(--k-accent);
+ box-shadow: 0 0 0 0.2rem var(--k-accent-soft);
+ }
+}
+
+.k-manual-search-results {
+ position: absolute;
+ top: calc(100% + 0.35rem);
+ left: 0;
+ right: 0;
+ z-index: 1040;
+ display: none;
+ margin: 0;
+ padding: 0.35rem;
+ list-style: none;
+ border: 1px solid var(--k-border);
+ border-radius: 0.5rem;
+ background-color: var(--k-surface);
+ box-shadow: var(--k-shadow-lg);
+ max-height: 24rem;
+ overflow-y: auto;
+
+ &.open { display: block; }
+
+ .k-manual-search-result {
+ display: block;
+ padding: 0.5rem 0.6rem;
+ border-radius: 0.375rem;
+ color: var(--k-text);
+ text-decoration: none;
+
+ &:hover { background-color: var(--k-accent-soft); color:
var(--k-accent-strong); }
+ }
+
+ .k-manual-search-title {
+ display: block;
+ font-size: 0.85rem;
+ font-weight: 600;
+ }
+
+ .k-manual-search-snippet {
+ display: block;
+ font-size: 0.75rem;
+ color: var(--k-muted);
+ margin-top: 0.15rem;
+ }
+
+ .k-manual-search-empty {
+ padding: 0.5rem 0.6rem;
+ font-size: 0.85rem;
+ color: var(--k-muted);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Layout: fixed left ToC / content / right "On this page" rail
+// A 3-column grid. The page scrolls naturally (window scrollbar); the left
+// and right rails are position:sticky so they stay put while content scrolls.
+// ---------------------------------------------------------------------------
+.k-manual-body {
+ display: grid;
+ grid-template-columns: 16rem minmax(0, 1fr) 14rem;
+ grid-template-areas: 'toc content right';
+ align-items: start;
+ padding: 1.5rem 1.5rem 3rem;
+ gap: 2rem;
+}
+
+// Pages without a left toc (single topic pages): content + right rail only
+.k-manual-body.k-no-toc {
+ grid-template-columns: minmax(0, 1fr) 14rem;
+ grid-template-areas: 'content right';
+ .toc-col { display: none; }
+}
+
+@media (max-width: 1199px) {
+ .k-manual-body {
+ grid-template-columns: 16rem minmax(0, 1fr);
+ grid-template-areas: 'toc content';
+ }
+ .k-manual-body.k-no-toc {
+ grid-template-columns: minmax(0, 1fr);
+ grid-template-areas: 'content';
+ }
+ .k-toc-right { display: none; }
+}
+
+// Left ToC column (filter input + Asciidoctor #toc), sticky below the header
+.toc-col {
+ grid-area: toc;
+ position: sticky;
+ top: 3.5rem;
+ align-self: start;
+ max-height: calc(100vh - 3.5rem);
+ overflow-y: auto;
+ padding: 1.5rem 0.25rem 2rem 0;
+ min-width: 0;
+}
+
+.k-manual-toc-filter {
+ position: sticky;
+ top: 0;
+ z-index: 1020;
+ width: 100%;
+ box-sizing: border-box;
+ height: 2.1rem;
+ margin: 0 0 0.5rem;
+ padding: 0 0.75rem;
+ border: 1px solid var(--k-border);
+ border-radius: 0.375rem;
+ background-color: var(--k-surface-muted);
+ color: var(--k-text);
+ font-size: 0.875rem;
+
+ &::placeholder { color: var(--k-muted); }
+
+ &:focus {
+ outline: none;
+ border-color: var(--k-accent);
+ box-shadow: 0 0 0 0.2rem var(--k-accent-soft);
+ }
+}
+
+// ToC (Asciidoctor #toc.toc2)
+#toc.toc2 {
+ width: 100%;
+
+ .toc-title {
+ font-size: 0.85rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ color: var(--k-muted);
+ margin-bottom: 0.75rem;
+ }
+
+ ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ }
+
+ ul ul { padding-left: 1rem; }
+
+ li { margin: 0.15rem 0; width: 100%; }
+
+ a {
+ display: block;
+ color: var(--k-text-soft);
+ font-size: 0.875rem;
+ line-height: 1.4;
+ padding: 0.25rem 0.5rem;
+ border-left: 2px solid transparent;
+ border-radius: 0.25rem;
+
+ &:hover {
+ color: var(--k-accent);
+ text-decoration: none;
+ background-color: var(--k-bg-subtle);
+ }
+ }
+
+ // scrollspy active state
+ li.active > a {
+ color: var(--k-accent);
+ border-left-color: var(--k-accent);
+ background-color: var(--k-accent-soft);
+ }
+
+ .sectlevel1 > li { margin-top: 0.35rem; }
+ .sectlevel1 > li > a { font-weight: 600; }
+ .sectlevel1 > li > ul { margin-top: 0.25rem; }
+}
+
+// Right "On this page" rail, sticky below the header
+.k-toc-right {
+ grid-area: right;
+ position: sticky;
+ top: 3.5rem;
+ align-self: start;
+ max-height: calc(100vh - 3.5rem);
+ overflow-y: auto;
+ padding: 1.5rem 0 2rem 0.25rem;
+ min-width: 0;
+
+ .k-toc-right-title {
+ font-size: 0.85rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ color: var(--k-muted);
+ margin-bottom: 0.75rem;
+ }
+
+ .k-toc-right-links { display: flex; flex-direction: column; gap: 0.1rem; }
+
+ .k-toc-right-link {
+ display: block;
+ color: var(--k-text-soft);
+ font-size: 0.8rem;
+ line-height: 1.4;
+ padding: 0.2rem 0.5rem;
+ border-left: 2px solid transparent;
+ border-radius: 0.25rem;
+
+ &:hover {
+ color: var(--k-accent);
+ text-decoration: none;
+ background-color: var(--k-bg-subtle);
+ }
+
+ &.k-toc-right-h2 { font-weight: 600; }
+ &.k-toc-right-h3 { padding-left: 1rem; }
+ }
+
+ a.active {
+ color: var(--k-accent);
+ border-left-color: var(--k-accent);
+ }
+}
+
+// Content (flows in the page; the window scrollbar scrolls it)
+#content {
+ grid-area: content;
+ padding: 1.5rem 0 2rem;
+ align-self: start;
+ min-width: 0;
+ width: 100%;
+}
+
+// Document title (removes the "title glitch" from the generic h1)
+.k-manual-title {
+ font-size: 2rem;
+ font-weight: 700;
+ line-height: 1.2;
+ margin: 0 0 1.5rem;
+ padding-bottom: 0.75rem;
+ border-bottom: 1px solid var(--k-border);
+}
+
+// ---------------------------------------------------------------------------
+// Sections / anchors
+// ---------------------------------------------------------------------------
+.sect1 { margin-bottom: 2.5rem; }
+.sect2 { margin-top: 1.5rem; }
+
+.anchor {
+ text-decoration: none;
+
+ &:hover { text-decoration: none; }
+}
+
+// ---------------------------------------------------------------------------
+// Asciidoctor blocks
+// ---------------------------------------------------------------------------
+.admonitionblock {
+ margin: 1.25rem 0;
+
+ table { width: 100%; border-collapse: collapse; }
+
+ td.icon {
+ width: 2.5rem;
+ vertical-align: top;
+ padding: 1rem 0.75rem;
+ font-weight: 700;
+ color: var(--k-white, #ffffff);
+ border-top-left-radius: 0.5rem;
+ border-bottom-left-radius: 0.5rem;
+ }
+
+ td.content {
+ padding: 1rem 1.25rem;
+ border: 1px solid var(--k-border);
+ border-left: none;
+ border-radius: 0 0.5rem 0.5rem 0;
+ font-size: 0.95rem;
+ background-color: var(--k-bg-subtle);
+ }
+}
+
+.admonitionblock td.icon { font-size: 0.8rem; text-transform: uppercase; }
+
+// admonition icon colors (Asciidoctor inline font icons via :icons: font)
+.admonitionblock {
+ .icon-note td.icon, td.icon-note { background: var(--k-accent-strong);
}
+ .icon-tip td.icon, td.icon-tip { background: #0a7a5a; }
+ .icon-warning td.icon, td.icon-warning { background: #b45309; }
+ .icon-caution td.icon, td.icon-caution { background: #a81c1c; }
+ .icon-important td.icon, td.icon-important { background: var(--k-accent); }
+}
+
+.listingblock {
+ margin: 1.25rem 0;
+
+ .content { position: relative; }
+
+ pre { margin: 0; }
+
+ .title {
+ font-size: 0.8rem;
+ font-weight: 600;
+ color: var(--k-muted);
+ margin-bottom: 0.4rem;
+ }
+}
+
+.exampleblock,
+.sidebarblock,
+.quoteblock,
+.literalblock,
+.openblock {
+ margin: 1.25rem 0;
+
+ > .content {
+ padding: 1rem 1.25rem;
+ border: 1px solid var(--k-border);
+ border-radius: 0.5rem;
+ background-color: var(--k-bg-subtle);
+ }
+
+ .title { font-size: 0.85rem; font-weight: 700; color: var(--k-text);
margin-bottom: 0.5rem; }
+}
+
+.sidebarblock {
+ border-left: 4px solid var(--k-accent);
+ > .content { border-left: none; }
+}
+
+.colist {
+ margin: 1.25rem 0;
+ ol { margin: 0 0 0 1.5rem; }
+ table { border-collapse: collapse; }
+ td { padding: 0.25rem 0.5rem; vertical-align: top; }
+}
+
+// Lists
+.ulist, .olist, .dlist, .hdlist {
+ margin: 1rem 0;
+
+ ul, ol { margin: 0 0 1rem 1.5rem; }
+
+ li { margin: 0.3rem 0; }
+}
+
+.dlist dt { font-weight: 600; margin-top: 0.6rem; }
+
+.quoteblock {
+ blockquote { margin: 0; border-left: 3px solid var(--k-accent);
padding-left: 1rem; }
+ .attribution { margin-top: 0.5rem; font-size: 0.85rem; color:
var(--k-muted); }
+}
+
+.imageblock {
+ margin: 1.25rem 0;
+
+ .content { line-height: 0; }
+ img { max-width: 100%; border-radius: 0.5rem; }
+ .title {
+ line-height: 1.4;
+ font-size: 0.8rem;
+ color: var(--k-muted);
+ margin-top: 0.5rem;
+ text-align: center;
+ }
+}
+
+// Tables
+table.tableblock {
+ width: 100%;
+ margin: 1.25rem 0;
+ border-collapse: collapse;
+ font-size: 0.95rem;
+
+ th, td {
+ border: 1px solid var(--k-border);
+ padding: 0.55rem 0.75rem;
+ vertical-align: top;
+ text-align: left;
+ }
+
+ thead th {
+ background-color: var(--k-bg-subtle);
+ font-weight: 600;
+ }
+
+ tbody tr:nth-child(even) td { background-color: var(--k-surface-muted); }
+
+ code { white-space: nowrap; }
+}
+
+table.frame-all { border: 1px solid var(--k-border); }
+table.frame-around { border: 1px solid var(--k-border); }
+
+// Horizontal rules
+hr { border: none; border-top: 1px solid var(--k-border); margin: 2rem 0; }
+
+// ---------------------------------------------------------------------------
+// Footer
+// ---------------------------------------------------------------------------
+#footer {
+ border-top: 1px solid var(--k-border);
+ background-color: var(--k-bg-subtle);
+ color: var(--k-text-soft);
+ font-size: 0.85rem;
+ padding: 2rem 1.5rem 3rem;
+
+ .footer-text {
+ max-width: 52rem;
+ margin-right: auto;
+ margin-left: auto;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Prev / Next rail + back to top
+// ---------------------------------------------------------------------------
+.k-manual-pager {
+ display: flex;
+ gap: 1rem;
+ justify-content: space-between;
+ align-items: stretch;
+ margin-top: 3rem;
+ padding-top: 1.5rem;
+ border-top: 1px solid var(--k-border);
+}
+
+.k-manual-pager-link {
+ flex: 1 1 0;
+ display: block;
+ padding: 0.75rem 1rem;
+ border: 1px solid var(--k-border);
+ border-radius: 0.5rem;
+ color: var(--k-text-soft);
+ text-decoration: none;
+ background-color: var(--k-bg-subtle);
+
+ &:hover {
+ border-color: var(--k-accent);
+ color: var(--k-accent);
+ text-decoration: none;
+ }
+
+ .k-manual-pager-label {
+ display: block;
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ color: var(--k-muted);
+ }
+
+ .k-manual-pager-title { font-weight: 600; color: var(--k-text); }
+
+ &.is-next { text-align: right; }
+}
+
+.k-manual-backtop {
+ flex: none;
+ align-self: center;
+ padding: 0.5rem 0.85rem;
+ border: 1px solid var(--k-border);
+ border-radius: 0.375rem;
+ background: var(--k-surface-muted);
+ color: var(--k-text-soft);
+ font-size: 0.8rem;
+ text-decoration: none;
+
+ &:hover { border-color: var(--k-accent); color: var(--k-accent);
text-decoration: none; }
+}
+
+// ---------------------------------------------------------------------------
+// Floating back-to-top button
+// ---------------------------------------------------------------------------
+.k-back-top {
+ position: fixed;
+ bottom: 1.5rem;
+ right: 1.5rem;
+ z-index: 1050;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 2.75rem;
+ height: 2.75rem;
+ border: 1px solid var(--k-border);
+ border-radius: 50%;
+ background: var(--k-surface-muted);
+ color: var(--k-text);
+ box-shadow: var(--k-shadow-md);
+ text-decoration: none;
+ opacity: 0;
+ visibility: hidden;
+ transform: translateY(0.5rem);
+ transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s ease;
+
+ svg { width: 1.25rem; height: 1.25rem; }
+
+ &:hover {
+ color: var(--k-accent);
+ border-color: var(--k-accent);
+ text-decoration: none;
+ }
+
+ &.is-visible {
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+ }
+}
+
+// Mobile menu (hamburger) button - hidden on desktop
+.k-manual-menu {
+ display: none;
+ flex: none;
+ width: 2.5rem;
+ height: 2.5rem;
+ margin-left: 0.5rem;
+ border: 1px solid var(--k-border);
+ border-radius: 0.375rem;
+ background: var(--k-surface-muted);
+ cursor: pointer;
+ position: relative;
+
+ &::before, &::after, & > span {
+ content: '';
+ position: absolute;
+ left: 0.75rem;
+ right: 0.75rem;
+ height: 2px;
+ background: var(--k-text);
+ }
+
+ &::before { top: 0.85rem; }
+ &::after { bottom: 0.85rem; }
+ & > span { top: 50%; margin-top: -1px; }
+}
+
+// ---------------------------------------------------------------------------
+// Responsive
+// ---------------------------------------------------------------------------
+@media (max-width: 991px) {
+ html, body { height: auto; }
+
+ // single scrollable column; rails hidden
+ .k-manual-body {
+ display: block;
+ height: auto;
+ overflow: visible;
+ padding: 1.5rem 1rem 2rem;
+ }
+
+ .k-manual-menu { display: block; }
+
+ .toc-col {
+ position: fixed;
+ top: 3.5rem;
+ bottom: 0;
+ left: 0;
+ z-index: 1040;
+ width: min(19rem, 85vw);
+ height: auto;
+ padding: 1rem;
+ background: var(--k-bg);
+ border-right: 1px solid var(--k-border);
+ box-shadow: var(--k-shadow-lg);
+ transform: translateX(-105%);
+ transition: transform 0.25s ease;
+ overflow-y: auto;
+ }
+
+ body.k-manual-open .toc-col { transform: translateX(0); }
+
+ .k-toc-right { display: none; }
+
+ #content {
+ height: auto;
+ max-width: none;
+ padding: 0;
+ overflow: visible;
+ }
+
+ .k-manual-title { font-size: 1.6rem; }
+
+ .k-manual-search {
+ width: auto;
+ flex: 1 1 auto;
+ }
+
+ .k-manual-brand .k-logo { height: 30px; }
+
+ .k-back-top { bottom: 1rem; right: 1rem; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ html { scroll-behavior: auto; }
+}
\ No newline at end of file
diff --git a/manual/src/main/frontend/_scss/_tokens.scss
b/manual/src/main/frontend/_scss/_tokens.scss
new file mode 100644
index 0000000000..39f00a62ba
--- /dev/null
+++ b/manual/src/main/frontend/_scss/_tokens.scss
@@ -0,0 +1,74 @@
+/*
+ * 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.
+ */
+// Apache Karaf design tokens as CSS custom properties.
+// Light values on :root, dark overrides under [data-bs-theme="dark"].
+// Mirrors karaf-site _scss/_karaf-tokens.scss so the manual shares the site
theme.
+
+:root,
+[data-bs-theme="light"] {
+ --k-bg: #ffffff;
+ --k-bg-subtle: #f6f7f9;
+ --k-surface: #ffffff;
+ --k-surface-muted: #f2f4f7;
+ --k-border: #e3e6ea;
+ --k-border-strong: #d0d5db;
+
+ --k-text: #1f2328;
+ --k-text-soft: #4b5563;
+ --k-muted: #6b7280;
+
+ --k-accent: #df2a2a;
+ --k-accent-strong: #a81c1c;
+ --k-accent-soft: #fdecec;
+
+ --k-code-bg: #f2f4f7;
+ --k-code-text: #c22020;
+ --k-hero-text: #ffffff;
+ --k-hero-overlay: rgba(24, 26, 32, 0.62);
+
+ --k-github-icon-filter: none;
+ --k-shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
+ --k-shadow-md: 0 4px 12px rgba(16, 24, 40, 0.08);
+ --k-shadow-lg: 0 12px 32px rgba(16, 24, 40, 0.12);
+}
+
+[data-bs-theme="dark"] {
+ --k-bg: #0f1115;
+ --k-bg-subtle: #15181e;
+ --k-surface: #181b22;
+ --k-surface-muted: #1e222b;
+ --k-border: #2a2f3a;
+ --k-border-strong: #3a4150;
+
+ --k-text: #e7e9ee;
+ --k-text-soft: #b3bac7;
+ --k-muted: #8b93a3;
+
+ --k-accent: #f0574f;
+ --k-accent-strong: #ff8a80;
+ --k-accent-soft: #3a1f21;
+
+ --k-code-bg: #22262f;
+ --k-code-text: #ff8a80;
+ --k-hero-text: #ffffff;
+ --k-hero-overlay: rgba(8, 9, 12, 0.72);
+
+ --k-github-icon-filter: invert(1);
+ --k-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
+ --k-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
+ --k-shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.5);
+}
\ No newline at end of file
diff --git a/manual/src/main/frontend/_scss/karaf.scss
b/manual/src/main/frontend/_scss/karaf.scss
new file mode 100644
index 0000000000..092f7e5950
--- /dev/null
+++ b/manual/src/main/frontend/_scss/karaf.scss
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+// Apache Karaf manual theme entry point.
+// Build: `npm run build:css` (sass) outputs ../asciidoc/karaf-manual.css
+
+@use "tokens";
+@use "manual-components";
\ No newline at end of file
diff --git a/manual/src/main/frontend/build-pdf.sh
b/manual/src/main/frontend/build-pdf.sh
new file mode 100755
index 0000000000..01fd511a68
--- /dev/null
+++ b/manual/src/main/frontend/build-pdf.sh
@@ -0,0 +1,103 @@
+#!/usr/bin/env sh
+#
+# 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.
+#
+# Build the Apache Karaf manual as a PDF using the asciidoctor-pdf (Ruby) gem.
+# The script installs the required Ruby toolchain if it is not already present
+# (tested on Ubuntu/Debian and macOS), then renders
src/main/asciidoc/index.adoc
+# into target/documentation.pdf using a theme aligned with the Apache Karaf
+# website colors (see karaf-pdf-theme.yml).
+#
+# Usage: sh build-pdf.sh
+
+set -e
+
+SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
+MAIN_DIR=$(dirname -- "$SCRIPT_DIR")
+ASCIIIDOC_DIR="$MAIN_DIR/asciidoc"
+PROJECT_DIR=$(cd "$MAIN_DIR/../.." && pwd)
+TARGET_DIR="$PROJECT_DIR/target"
+
+install_ruby() {
+ case "$(uname -s)" in
+ Darwin)
+ if command -v brew >/dev/null 2>&1; then
+ printf 'Installing Ruby via Homebrew...\n'
+ brew install ruby
+ else
+ printf 'Homebrew was not found. Please install Ruby
(https://www.ruby-lang.org), then re-run this script.\n' >&2
+ exit 1
+ fi
+ ;;
+ Linux)
+ OS_ID=linux
+ if [ -r /etc/os-release ]; then
+ OS_ID=$(. /etc/os-release; printf '%s' "$ID")
+ fi
+ case "$OS_ID" in
+ ubuntu|debian)
+ printf 'Installing Ruby and build tools via apt...\n'
+ sudo apt-get update
+ sudo apt-get install -y ruby ruby-dev build-essential
+ ;;
+ *)
+ printf 'Unsupported Linux distribution "%s". Please
install Ruby and RubyGems, then re-run this script.\n' "$OS_ID" >&2
+ exit 1
+ ;;
+ esac
+ ;;
+ *)
+ printf 'Unsupported operating system. Please install Ruby and
RubyGems, then re-run this script.\n' >&2
+ exit 1
+ ;;
+ esac
+}
+
+ensure_asciidoctor_pdf() {
+ if ! command -v ruby >/dev/null 2>&1 || ! command -v gem >/dev/null 2>&1;
then
+ install_ruby
+ fi
+ if ! command -v asciidoctor-pdf >/dev/null 2>&1; then
+ printf 'Installing asciidoctor, asciidoctor-pdf and rouge gems...\n'
+ gem install --no-document asciidoctor asciidoctor-pdf rouge
+ fi
+ # The gem bin directory may not be on PATH (e.g. user-local gems). Resolve
it.
+ if ! command -v asciidoctor-pdf >/dev/null 2>&1; then
+ RUBY_GEM_HOME=$(gem env home 2>/dev/null || true)
+ if [ -n "$RUBY_GEM_HOME" ] && [ -x
"$RUBY_GEM_HOME/bin/asciidoctor-pdf" ]; then
+ export PATH="$RUBY_GEM_HOME/bin:$PATH"
+ fi
+ fi
+ if ! command -v asciidoctor-pdf >/dev/null 2>&1; then
+ printf 'asciidoctor-pdf is not available. Re-run the script, or check
the RubyGems installation.\n' >&2
+ exit 1
+ fi
+ printf 'asciidoctor-pdf available: %s\n' "$(command -v asciidoctor-pdf)"
+}
+
+ensure_asciidoctor_pdf
+
+mkdir -p "$TARGET_DIR"
+cd "$ASCIIIDOC_DIR"
+asciidoctor-pdf \
+ -a pdf-theme="$SCRIPT_DIR/karaf-pdf-theme.yml" \
+ -a source-highlighter=rouge \
+ -a imagesdir=images \
+ -a doctype=book \
+ -o "$TARGET_DIR/documentation.pdf" \
+ index.adoc
+
+printf 'pdf generated: %s\n' "$TARGET_DIR/documentation.pdf"
\ No newline at end of file
diff --git a/manual/src/main/frontend/karaf-logo-light.svg
b/manual/src/main/frontend/karaf-logo-light.svg
new file mode 100644
index 0000000000..1ec5f5f244
--- /dev/null
+++ b/manual/src/main/frontend/karaf-logo-light.svg
@@ -0,0 +1,17 @@
+<!--
+ 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.
+-->
+<svg xmlns="http://www.w3.org/2000/svg" width="297mm" height="210mm"
viewBox="0 0 297 210"><defs><linearGradient id="a" x1="-5167.096"
x2="-4570.116" y1="697.555" y2="1395.619" gradientTransform="rotate(-65
-998.095 -1339.204)scale(.26458)" gradientUnits="userSpaceOnUse"><stop
offset="0" style="stop-color:#f69923"/><stop offset=".312"
style="stop-color:#f79a23"/><stop offset=".838"
style="stop-color:#e97826"/></linearGradient><linearGradient id="b"
x1="-9585.342" x2="-5326.209" y1="620.5 [...]
diff --git a/manual/src/main/frontend/karaf-logo.svg
b/manual/src/main/frontend/karaf-logo.svg
new file mode 100644
index 0000000000..341a0d74b9
--- /dev/null
+++ b/manual/src/main/frontend/karaf-logo.svg
@@ -0,0 +1,17 @@
+<!--
+ 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.
+-->
+<svg xmlns="http://www.w3.org/2000/svg" width="297mm" height="210mm"
viewBox="0 0 297 210"><defs><linearGradient id="a" x1="-5167.096"
x2="-4570.116" y1="697.555" y2="1395.619" gradientTransform="rotate(-65
-998.095 -1339.204)scale(.26458)" gradientUnits="userSpaceOnUse"><stop
offset="0" style="stop-color:#f69923"/><stop offset=".312"
style="stop-color:#f79a23"/><stop offset=".838"
style="stop-color:#e97826"/></linearGradient><linearGradient id="b"
x1="-9585.342" x2="-5326.209" y1="620.5 [...]
diff --git a/manual/src/main/frontend/karaf-manual-footer.html
b/manual/src/main/frontend/karaf-manual-footer.html
new file mode 100644
index 0000000000..cf1a59697e
--- /dev/null
+++ b/manual/src/main/frontend/karaf-manual-footer.html
@@ -0,0 +1,169 @@
+<!--
+ 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.
+-->
+<script>
+ (function () {
+ "use strict";
+
+ var html = document.documentElement;
+ var toggle = document.getElementById('karaf-theme-toggle');
+
+ function applyTheme(theme) {
+ html.setAttribute('data-bs-theme', theme);
+ try { localStorage.setItem('karaf-theme', theme); } catch (e) {}
+ }
+ if (toggle) {
+ toggle.addEventListener('click', function () {
+ applyTheme(html.getAttribute('data-bs-theme') === 'dark' ? 'light' :
'dark');
+ });
+ }
+ if (window.matchMedia) {
+ window.matchMedia('(prefers-color-scheme:
dark)').addEventListener('change', function (ev) {
+ var saved = null;
+ try { saved = localStorage.getItem('karaf-theme'); } catch (e) {}
+ if (!saved) { applyTheme(ev.matches ? 'dark' : 'light'); }
+ });
+ }
+
+ // ---- shared ----
+ function throttle(fn, wait) {
+ var t = null;
+ return function () {
+ if (t) { return; }
+ t = setTimeout(function () { t = null; fn(); }, wait);
+ };
+ }
+
+ var searchIndex = [];
+ try {
+ searchIndex =
JSON.parse(document.getElementById('k-manual-search-index').textContent) || [];
+ } catch (e) {}
+
+ // the whole page scrolls (window scrollbar); rails are position:sticky
+ var scroller = window;
+
+ function scrollTop() {
+ return scroller === window
+ ? (window.scrollY || document.documentElement.scrollTop)
+ : scroller.scrollTop;
+ }
+
+ // ---- floating back-to-top (static markup; just toggle visibility) ----
+ var backTop = document.querySelector('.k-back-top');
+ if (backTop) {
+ function syncBackTop() {
+ backTop.classList.toggle('is-visible', scrollTop() > 400);
+ }
+ scroller.addEventListener('scroll', throttle(syncBackTop, 100), {
passive: true });
+ syncBackTop();
+ }
+
+ // ---- scrollspy on the toc + right rail ----
+ function spy() {
+ var items = document.querySelectorAll('#toc a[href^="#"]');
+ items.forEach(function (a) { a.parentNode.classList.remove('active'); });
+ var rightLinks = document.querySelectorAll('.k-toc-right a[href^="#"]');
+ rightLinks.forEach(function (a) { a.classList.remove('active'); });
+ var pos = scrollTop();
+ var currentToc = null;
+ var currentRight = null;
+ items.forEach(function (a) {
+ var el = document.getElementById(a.getAttribute('href').slice(1));
+ if (el && el.offsetTop <= pos + 140) {
+ currentToc = a.parentNode;
+ if (currentRight !== null) { currentRight = null; }
+ }
+ });
+ rightLinks.forEach(function (a) {
+ var el = document.getElementById(a.getAttribute('href').slice(1));
+ if (el && el.offsetTop <= pos + 140) { currentRight = a; }
+ });
+ if (currentToc) { currentToc.classList.add('active'); }
+ if (currentRight) { currentRight.classList.add('active'); }
+ }
+ if (document.getElementById('toc') ||
document.querySelectorAll('.k-toc-right a').length) {
+ scroller.addEventListener('scroll', throttle(spy, 100), { passive: true
});
+ spy();
+ }
+
+ // ---- mobile menu drawer ----
+ var menuBtn = document.getElementById('k-manual-menu');
+ if (menuBtn) {
+ menuBtn.addEventListener('click', function () {
+ document.body.classList.toggle('k-manual-open');
+ });
+ }
+
+ // in-page anchors use native jumps; html { scroll-padding-top: 5rem }
+ // keeps targets clear of the fixed header
+
+ // ---- toc filter ----
+ var tocFilter = document.getElementById('k-manual-toc-filter');
+ if (tocFilter && document.getElementById('toc')) {
+ function applyFilter(query) {
+ query = (query || '').toLowerCase().trim();
+ var items = document.querySelectorAll('#toc li');
+ items.forEach(function (li) {
+ var text = li.textContent || '';
+ li.style.display = (!query || text.toLowerCase().indexOf(query) !==
-1) ? '' : 'none';
+ });
+ }
+ tocFilter.addEventListener('input', function () {
applyFilter(tocFilter.value); });
+ }
+
+ // ---- full content search ----
+ var searchInput = document.getElementById('k-manual-search');
+ var resultsEl = document.getElementById('k-manual-search-results');
+ if (searchInput && resultsEl) {
+ function renderResults(query) {
+ query = (query || '').toLowerCase().trim();
+ resultsEl.innerHTML = '';
+ if (!query) { resultsEl.classList.remove('open'); return; }
+ var hits = [];
+ for (var i = 0; i < searchIndex.length; i++) {
+ var s = searchIndex[i];
+ var hay = ((s.title || '') + ' ' + (s.text || '')).toLowerCase();
+ if (hay.indexOf(query) !== -1) {
+ hits.push(s);
+ if (hits.length >= 12) { break; }
+ }
+ }
+ resultsEl.classList.add('open');
+ if (hits.length === 0) {
+ var none = document.createElement('li');
+ none.className = 'k-manual-search-empty';
+ none.textContent = 'No results';
+ resultsEl.appendChild(none);
+ return;
+ }
+ hits.forEach(function (s) {
+ var li = document.createElement('li');
+ var a = document.createElement('a');
+ a.className = 'k-manual-search-result';
+ a.href = '#' + s.id;
+ a.innerHTML = '<span class="k-manual-search-title">' + s.title +
'</span>' +
+ (s.text ? '<span class="k-manual-search-snippet">' +
s.text.slice(0, 120) + '</span>' : '');
+ li.appendChild(a);
+ resultsEl.appendChild(li);
+ });
+ }
+ searchInput.addEventListener('input', function () {
renderResults(searchInput.value); });
+ document.addEventListener('click', function (e) {
+ if (!e.target.closest('.k-manual-search')) {
resultsEl.classList.remove('open'); }
+ });
+ }
+ })();
+</script>
\ No newline at end of file
diff --git a/manual/src/main/frontend/karaf-manual-head.html
b/manual/src/main/frontend/karaf-manual-head.html
new file mode 100644
index 0000000000..a9a8650845
--- /dev/null
+++ b/manual/src/main/frontend/karaf-manual-head.html
@@ -0,0 +1,28 @@
+<!--
+ 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.
+-->
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap"
rel="stylesheet">
+<script>
+ (function () {
+ var saved = null;
+ try { saved = localStorage.getItem('karaf-theme'); } catch (e) {}
+ var theme = saved || (window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
+ document.documentElement.setAttribute('data-bs-theme', theme);
+ })();
+</script>
\ No newline at end of file
diff --git a/manual/src/main/frontend/karaf-pdf-theme.yml
b/manual/src/main/frontend/karaf-pdf-theme.yml
new file mode 100644
index 0000000000..6d8ec7c787
--- /dev/null
+++ b/manual/src/main/frontend/karaf-pdf-theme.yml
@@ -0,0 +1,148 @@
+#
+# 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.
+#
+# Apache Karaf manual PDF theme.
+# The cover (title) page uses the dark theme colors; the rest is light.
+# Colors are aligned with the Apache Karaf website (see _scss/_tokens.scss):
+# light text #1f2328 accent #df2a2a border #e3e6ea bg #ffffff muted
#6b7280
+# dark bg #0f1115 dark text #e7e9ee dark text-soft #b3bac7
+#
+extends: default
+
+# Light body theme (the "rest is light" part).
+base:
+ font_family: Helvetica
+ font_color: "#1f2328"
+ font_size: 10
+ line_height_length: 15
+ line_height: 1.4
+ align: left
+
+role:
+ lead:
+ font_size: 12
+ font_style: normal
+
+page:
+ margin: [0.6in, 0.7in, 0.65in, 0.7in]
+
+# Dark cover (title) page.
+title-page:
+ background-color: "#0f1115"
+ align: center
+ title:
+ font_color: "#ffffff"
+ font_size: 28
+ font_style: bold
+ line_height: 1.2
+ margin_top: 2in
+ subtitle:
+ font_color: "#b3bac7"
+ font_size: 14
+ font_style: normal
+ authors:
+ font_color: "#b3bac7"
+ font_size: 11
+ revision:
+ font_color: "#8b93a3"
+
+heading:
+ font_color: "#1f2328"
+ font_family: Helvetica
+ font_style: bold
+ line_height: 1.25
+ margin_top: 0.9 * $vertical_rhythm
+ margin_bottom: 0.35 * $vertical_rhythm
+ h2_font_size: 20
+ h3_font_size: 16
+ h4_font_size: 13
+ h5_font_size: 11
+ border_bottom_color: "#e3e6ea"
+ border_bottom_width: 0.5
+ border_bottom_style: solid
+ h2-border-bottom-width: 0.75
+
+title:
+ align: left
+ font_align: left
+ font_color: "#1f2328"
+ font_size: 24
+ font_style: bold
+
+link:
+ font_color: "#a81c1c"
+ text_decoration: underline
+
+codespan:
+ font_family: Courier
+ font_color: "#c22020"
+ background_color: "#f2f4f7"
+
+listing:
+ font_family: Courier
+ font_color: "#1f2328"
+ background_color: "#f2f4f7"
+ border_color: "#e3e6ea"
+ border_width: 0.5
+ padding: 8
+
+code:
+ font_family: Courier
+ font_color: "#c22020"
+ background_color: "#f2f4f7"
+
+table:
+ background_color: "#ffffff"
+ font_color: "#1f2328"
+ font_size: 9
+ grid_width: 0.5
+ grid_color: "#e3e6ea"
+ stripe_background_color: "#f6f7f9"
+ border_color: "#e3e6ea"
+ border_width: 0.5
+ head:
+ background_color: "#f2f4f7"
+ font_color: "#1f2328"
+ font_style: bold
+ foot:
+ background_color: "#f6f7f9"
+ font_color: "#1f2328"
+
+admonition:
+ label_font_style: bold
+ label_font_color: "#6b7280"
+ icon:
+ tip:
+ stroke_color: "#6b7280"
+ note:
+ stroke_color: "#6b7280"
+ warning:
+ stroke_color: "#6b7280"
+ important:
+ stroke_color: "#6b7280"
+ caution:
+ stroke_color: "#6b7280"
+
+caption:
+ font_color: "#6b7280"
+ font_size: 9
+ align: left
+
+footer:
+ font_size: 8
+ font_color: "#6b7280"
+ height: 0.45in
+ line_height: 1
\ No newline at end of file
diff --git a/manual/src/main/frontend/package.json
b/manual/src/main/frontend/package.json
new file mode 100644
index 0000000000..946be2bb96
--- /dev/null
+++ b/manual/src/main/frontend/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "karaf-manual",
+ "version": "4.5.0",
+ "description": "Apache Karaf manual theme - frontend asset build",
+ "private": true,
+ "scripts": {
+ "build:css": "sass --no-source-map
_scss/karaf.scss:../asciidoc/karaf-manual.css",
+ "watch:css": "sass --watch _scss/karaf.scss:../asciidoc/karaf-manual.css",
+ "html": "node render-html.js",
+ "build": "npm run build:css && npm run html",
+ "postprocess": "node postprocess.js"
+ },
+ "devDependencies": {
+ "@asciidoctor/core": "^4.0.11",
+ "sass": "^1.77.0"
+ }
+}
\ No newline at end of file
diff --git a/manual/src/main/frontend/postprocess.js
b/manual/src/main/frontend/postprocess.js
new file mode 100644
index 0000000000..e724d9fa6d
--- /dev/null
+++ b/manual/src/main/frontend/postprocess.js
@@ -0,0 +1,235 @@
+/*
+ * 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.
+ */
+"use strict";
+
+const fs = require("fs");
+const path = require("path");
+
+const baseDir = path.join(__dirname, "..", "..", "..", "target",
"generated-docs");
+const headSnippet = fs.readFileSync(path.join(__dirname,
"karaf-manual-head.html"), "utf8").trim();
+const footerSnippet = fs.readFileSync(path.join(__dirname,
"karaf-manual-footer.html"), "utf8").trim();
+
+function walk(dir) {
+ const out = [];
+ for (const entry of fs.readdirSync(dir)) {
+ const full = path.join(dir, entry);
+ if (fs.statSync(full).isDirectory()) {
+ out.push(...walk(full));
+ } else if (entry.endsWith(".html") && !entry.startsWith("karaf-manual-")) {
+ out.push(full);
+ }
+ }
+ return out;
+}
+
+function stripTags(s) {
+ return (s || "").replace(/<[^>]+>/g, " ")
+ .replace(/ /g, " ").replace(/&/g, "&")
+ .replace(/</g, "<").replace(/>/g, ">")
+ .replace(/’|'|'/g, "'")
+ .replace(/–|—/g, "-")
+ .replace(/\s+/g, " ").trim();
+}
+
+function escapeHtml(s) {
+ return (s || "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g,
">").replace(/"/g, """);
+}
+
+function processFile(file) {
+ let html = fs.readFileSync(file, "utf8");
+ const fileName = path.basename(file);
+
+ // 1. head injection (fonts + pre-paint theme script)
+ if (!html.includes("fonts.googleapis.com/css2") && html.includes("</head>"))
{
+ html = html.replace("</head>", headSnippet + "\n</head>");
+ }
+
+ // 2. capture doc title from Asciidoctor header
+ let docTitle = fileName.replace(/\.html$/, "");
+ const th = /<h1[^>]*>([\s\S]*?)<\/h1>/.exec(html);
+ if (th) { docTitle = stripTags(th[1]); }
+ // drop the Asciidoctor header block (brand replaces it)
+ html = html.replace(/[ \t]*<div id="header">[\s\S]*?<\/div>\r?\n?/, "");
+
+ // 3. extract #toc block (balanced div, includes nested <ul>/<li>/<div>)
+ let tocHtml = "";
+ const tocOpen = /<div id="toc"[^>]*>/i.exec(html);
+ if (tocOpen) {
+ const depthStart = tocOpen.index + tocOpen[0].length;
+ let depth = 1;
+ const re = /<(\/)?div\b[^>]*>/gi;
+ let m;
+ let idx = depthStart;
+ while ((m = re.exec(html))) {
+ if (m.index < depthStart) { continue; }
+ if (m[1]) { depth--; } else { depth++; }
+ if (depth === 0) {
+ idx = m.index + m[0].length;
+ break;
+ }
+ }
+ tocHtml = html.slice(tocOpen.index, idx);
+ }
+
+ // 4. extract #content block + following pager spot; content ends right
before <div id="footer">
+ const contentOpenRe = /<div id="content"[^>]*>/;
+ const co = contentOpenRe.exec(html);
+ let contentHtml = "";
+ let beforeContent = "";
+ let afterContent = "";
+ if (co) {
+ const openEnd = co.index + co[0].length;
+ beforeContent = html.slice(0, co.index);
+ const footerMatch = /<\/div>\s*<div id="footer">[\s\S]*?<\/div>/;
+ const fm = footerMatch.exec(html);
+ if (fm && fm.index >= openEnd) {
+ contentHtml = html.slice(openEnd, fm.index);
+ afterContent = html.slice(openEnd + contentHtml.length);
+ } else {
+ // no footer fallback
+ const closeBody = html.indexOf("</body>");
+ contentHtml = html.slice(openEnd, closeBody >= 0 ? closeBody :
html.length);
+ afterContent = html.slice(closeBody >= 0 ? closeBody : html.length);
+ }
+ }
+
+ // 5. collect sections at every heading level (h2/h3/h4) from content for
search index + pager
+ const sections = [];
+ const headRe = /<(h[234])\s+id="([^"]+)">([\s\S]*?)<\/\1>/g;
+ const heads = [];
+ let hm;
+ while ((hm = headRe.exec(contentHtml)) !== null) {
+ heads.push({ level: parseInt(hm[1].charAt(1), 10), id: hm[2], title:
stripTags(hm[3]), index: hm.index, end: hm.index + hm[0].length });
+ }
+ let part = "";
+ for (let i = 0; i < heads.length; i++) {
+ const h = heads[i];
+ if (h.level === 2) part = h.title;
+ const bodyStart = h.end;
+ const bodyEnd = i + 1 < heads.length ? heads[i + 1].index :
contentHtml.length;
+ const text = stripTags(contentHtml.slice(bodyStart, bodyEnd));
+ if (h.id) {
+ sections.push({ id: h.id, title: h.title, level: h.level, text: text,
part: part });
+ }
+ }
+
+ // 6. build static shell
+ const brandHtml =
+ '<a class="k-manual-brand" href="/" aria-label="Apache Karaf">' +
+ '<img class="k-logo k-logo-dark" src="images/karaf-logo.svg" alt="">' +
+ '<img class="k-logo k-logo-light" src="images/karaf-logo-light.svg"
alt="">' +
+ '<span class="k-manual-brand-name">Apache Karaf</span></a>';
+
+ const searchHtml =
+ '<div class="k-manual-search">' +
+ '<input type="search" id="k-manual-search" class="k-manual-search-input"
placeholder="Search documentation..." autocomplete="off">' +
+ '<ul id="k-manual-search-results" class="k-manual-search-results"
aria-live="polite"></ul>' +
+ "</div>";
+
+ const filterHtml =
+ '<label class="visually-hidden" for="k-manual-toc-filter">Filter
sections</label>' +
+ '<input type="search" id="k-manual-toc-filter" class="k-manual-toc-filter"
placeholder="Filter sections..." autocomplete="off">';
+
+ const hasToc = tocHtml.indexOf('id="toc"') !== -1;
+ const tocCol = '<div class="toc-col">' + (hasToc ? filterHtml : "") +
tocHtml + "</div>";
+
+ // right "On this page" rail: the page's own h2/h3 outline (skip when huge,
e.g. consolidated index)
+ let rightCol = "";
+ const pageOutline = sections.filter(function (s) { return s.level <= 3; });
+ if (pageOutline.length > 0 && pageOutline.length < 40) {
+ const links = pageOutline.map(function (s) {
+ const cls = s.level === 2 ? "k-toc-right-h2" : "k-toc-right-h3";
+ return '<a class="k-toc-right-link ' + cls + '" href="#' +
escapeHtml(s.id) + '">' + escapeHtml(s.title) + "</a>";
+ }).join("\n");
+ rightCol =
+ '<div class="k-toc-right">' +
+ '<div class="k-toc-right-title">On this page</div>' +
+ '<div class="k-toc-right-links">' + links + "</div>" +
+ "</div>";
+ }
+
+ // static prev/next pager over the page's top-level (h2) parts
+ const parts = sections.filter(function (s) { return s.level === 2; });
+ let pagerInner = "";
+ if (parts.length > 1) {
+ const cur = parts[0];
+ const curIdx = parts.indexOf(cur);
+ const prev = curIdx > 0 ? parts[curIdx - 1] : null;
+ const next = curIdx + 1 < parts.length ? parts[curIdx + 1] : null;
+ const link = function (s, cls, label) {
+ return '<a class="k-manual-pager-link ' + cls + '" href="#' +
escapeHtml(s.id) + '">' +
+ '<span class="k-manual-pager-label">' + label + "</span>" +
+ '<span class="k-manual-pager-title">' + escapeHtml(s.title) +
"</span></a>";
+ };
+ const backTop = '<a class="k-manual-backtop"
href="#k-manual-title">Top</a>';
+ pagerInner =
+ (prev ? link(prev, "is-prev", "Previous") : '<span
class="k-manual-pager-empty"></span>') +
+ backTop +
+ (next ? link(next, "is-next", "Next") : '<span
class="k-manual-pager-empty"></span>');
+ }
+
+ const titleEl = '<h1 class="k-manual-title" id="k-manual-title">' +
escapeHtml(docTitle) + "</h1>";
+ const fixedContent = titleEl + contentHtml;
+
+ const searchIndexJson = JSON.stringify(sections);
+
+ const shell =
+ '<header class="k-manual-header">' + brandHtml + searchHtml +
+ '<button type="button" class="k-manual-toggle" id="karaf-theme-toggle"
aria-label="Toggle dark mode">' +
+ '<svg class="icon-sun" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12"
r="4"></circle><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41
1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"></path></svg>' +
+ '<svg class="icon-moon" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3
7 7 0 0 0 21 12.79z"></path></svg>' +
+ "</button>" +
+ '<button type="button" class="k-manual-menu" id="k-manual-menu"
aria-label="Toggle menu" aria-controls="toc"><span></span></button>' +
+ "</header>" +
+ '<div class="k-manual-body' + (hasToc ? "" : " k-no-toc") + '">' + tocCol
+ rightCol +
+ '<div id="content" class="content">' + fixedContent +
+ (pagerInner ? '<nav class="k-manual-pager" id="k-manual-pager"
aria-label="Documentation sections">' + pagerInner + "</nav>" : "") +
+ "</div>" +
+ '</div>' +
+ '<a class="k-back-top" href="#k-manual-title" title="Back to top"
aria-label="Back to top">' +
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"></path></svg>' +
+ "</a>" +
+ '<script type="application/json" id="k-manual-search-index">' +
searchIndexJson + "</script>";
+
+ // 7. assemble final document
+ const bodyOpenEnd = html.indexOf(">", html.indexOf("<body")) + 1;
+ const tail = "</body></html>";
+ html = html.slice(0, bodyOpenEnd) + "\n" + shell + "\n" + footerSnippet +
"\n" + tail;
+
+ fs.writeFileSync(file, html, "utf8");
+ return fileName;
+}
+
+if (!fs.existsSync(baseDir)) {
+ throw new Error("generated-docs not found: " + baseDir);
+}
+
+const logoDir = path.join(baseDir, "images");
+fs.mkdirSync(logoDir, { recursive: true });
+for (const logo of ["karaf-logo.svg", "karaf-logo-light.svg"]) {
+ const src = path.join(__dirname, logo);
+ if (fs.existsSync(src)) {
+ fs.copyFileSync(src, path.join(logoDir, logo));
+ }
+}
+
+let count = 0;
+for (const file of walk(baseDir)) {
+ processFile(file);
+ count++;
+}
+console.log("postprocess: processed " + count + " file(s).");
\ No newline at end of file
diff --git a/manual/src/main/frontend/render-html.js
b/manual/src/main/frontend/render-html.js
new file mode 100644
index 0000000000..0aae241d91
--- /dev/null
+++ b/manual/src/main/frontend/render-html.js
@@ -0,0 +1,131 @@
+/*
+ * 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.
+ */
+
+'use strict';
+
+const { convert, convertFile, SafeMode } = require('@asciidoctor/core');
+const fs = require('fs');
+const path = require('path');
+
+const FRONTEND = __dirname;
+const ASCIIIDOC = path.join(FRONTEND, '..', 'asciidoc');
+const TARGET = path.join(FRONTEND, '..', '..', '..', 'target');
+const OUT_DIR = path.join(TARGET, 'generated-docs');
+
+const IMAGES_SRC = path.join(ASCIIIDOC, 'images');
+
+function copyDir(from, to) {
+ if (!fs.existsSync(from)) {
+ return;
+ }
+ fs.mkdirSync(to, { recursive: true });
+ for (const entry of fs.readdirSync(from)) {
+ const src = path.join(from, entry);
+ const dst = path.join(to, entry);
+ if (fs.statSync(src).isDirectory()) {
+ copyDir(src, dst);
+ } else {
+ fs.copyFileSync(src, dst);
+ }
+ }
+}
+
+function collectAdoc(dir, out) {
+ for (const entry of fs.readdirSync(dir)) {
+ const full = path.join(dir, entry);
+ if (fs.statSync(full).isDirectory()) {
+ collectAdoc(full, out);
+ } else if (entry.endsWith('.adoc')) {
+ out.push(full);
+ }
+ }
+ return out;
+}
+
+// Asciidoctor.js does not expand include:: macros from disk reliably under
+// convertFile, so resolve them ourselves (recursively, relative to each file).
+function expandIncludes(file, seen) {
+ const resolved = path.resolve(file);
+ if (seen.has(resolved)) {
+ return '';
+ }
+ seen.add(resolved);
+ const dir = path.dirname(resolved);
+ let content = fs.readFileSync(resolved, 'utf8');
+ content = content.replace(/^include::([^\[]+)\[\]\s*$/gm, function (match,
target) {
+ const includeFile = path.join(dir, target.trim());
+ if (fs.existsSync(includeFile)) {
+ return expandIncludes(includeFile, seen);
+ }
+ return match;
+ });
+ return content;
+}
+
+async function main() {
+ fs.mkdirSync(OUT_DIR, { recursive: true });
+ copyDir(IMAGES_SRC, path.join(OUT_DIR, 'images'));
+ const stylesheet = path.join(ASCIIIDOC, 'karaf-manual.css');
+ if (fs.existsSync(stylesheet)) {
+ fs.copyFileSync(stylesheet, path.join(OUT_DIR, 'karaf-manual.css'));
+ }
+
+ const attributes = {
+ 'source-highlighter': 'highlightjs',
+ toc: '',
+ 'toclevels': 3,
+ 'toc-position': 'left',
+ doctype: 'article',
+ linkcss: '',
+ stylesheet: 'karaf-manual.css',
+ copycss: '',
+ imagesdir: 'images'
+ };
+
+ const adocs = collectAdoc(ASCIIIDOC, []);
+ for (const adoc of adocs) {
+ const content = fs.readFileSync(adoc, 'utf8');
+ let html;
+ if (/^include::/m.test(content)) {
+ const expanded = expandIncludes(adoc, new Set());
+ html = await convert(expanded, {
+ safe: SafeMode.UNSAFE,
+ standalone: true,
+ base_dir: ASCIIIDOC,
+ attributes
+ });
+ } else {
+ html = await convertFile(adoc, {
+ safe: SafeMode.UNSAFE,
+ to_file: false,
+ standalone: true,
+ attributes
+ });
+ }
+ const outFile = path.join(OUT_DIR, path.basename(adoc, '.adoc') + '.html');
+ fs.writeFileSync(outFile, html);
+ console.log('rendered ' + path.basename(outFile));
+ }
+ console.log('rendered ' + adocs.length + ' adoc files');
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exitCode = 1;
+});
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 3533c36be6..67ba227612 100644
--- a/pom.xml
+++ b/pom.xml
@@ -612,6 +612,11 @@
</execution>
</executions>
</plugin>
+ <plugin>
+ <groupId>com.github.eirslett</groupId>
+ <artifactId>frontend-maven-plugin</artifactId>
+ <version>1.15.1</version>
+ </plugin>
</plugins>
</pluginManagement>
@@ -804,6 +809,8 @@
<inputExclude>.repository/**</inputExclude>
<!-- jar files -->
<inputExclude>**/*.jar</inputExclude>
+ <!-- node -->
+ <inputExclude>**/node_modules/**</inputExclude>
</inputExcludes>
</configuration>
</plugin>