Skip to content

Commit 6e5ec62

Browse files
committed
docs(html): add in-browser topbar search via pagefind
Build a per-language pagefind index at htmldocs time (optional: only when pagefind is on PATH, which CI installs) and add a topbar search box that loads its language's index. Topbar chrome is excluded from the index. Includes theme-aware styling and a color-scheme dark-theme fix for the translucent bar.
1 parent 853878b commit 6e5ec62

5 files changed

Lines changed: 290 additions & 9 deletions

File tree

.github/workflows/ci.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,16 @@ jobs:
171171
run: |
172172
set -x
173173
.github/scripts/install-deps.sh
174+
- name: Install pagefind (docs search index)
175+
run: |
176+
set -x
177+
PAGEFIND_VERSION=v1.5.2
178+
tmp="$(mktemp -d)"
179+
curl --no-progress-meter -fL \
180+
"https://github.com/Pagefind/pagefind/releases/download/$PAGEFIND_VERSION/pagefind-$PAGEFIND_VERSION-x86_64-unknown-linux-musl.tar.gz" \
181+
| tar -xz -C "$tmp"
182+
sudo install -m 0755 "$tmp/pagefind" /usr/local/bin/pagefind
183+
rm -rf "$tmp"
174184
- name: Build HTML docmentation
175185
run: |
176186
set -x

docs/src/Submakefile

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,15 @@ DOC_MAN := $(DOC_BUILD)/man
6767
# PDFs live under the html tree (docs/build/html/<lang>/pdf/) so the html
6868
# subtree is self-contained and can link them with relative paths.
6969

70+
# Pagefind powers the in-browser docs search box in the topbar. It is a
71+
# Rust binary distributed via npm/GitHub, not a Debian package, so it cannot
72+
# be a hard Build-Depends: the build detects it on PATH and only wires search
73+
# in when present. Official .deb builds without pagefind simply ship docs
74+
# without the search box; CI installs it explicitly for the htmldocs job.
75+
# Defined here (early) because the search-box substitution and the index
76+
# stamp below both gate on it via parse-time ifeq/ifneq.
77+
PAGEFIND := $(shell command -v pagefind 2>/dev/null)
78+
7079
ASCIIDOCTOR_DEFAULT_CSS := $(shell ruby -e 'require "asciidoctor"; print Asciidoctor::DATA_DIR' 2>/dev/null)/stylesheets/asciidoctor-default.css
7180

7281
# The following line determines for the Makefile what languages should be addressed.
@@ -595,6 +604,31 @@ endif
595604
.htmldoc-stamp: .copy-asciidoc-stamp $(DOC_DIR)/.gen_complist-stamp $(HTML_TARGETS) .images-stamp .include-stamp $(DOC_OUT_HTML)/asciidoctor.css $(DOC_OUT_HTML)/rouge-github.css .lang-switcher-stamp
596605
touch $@
597606

607+
# In-browser search index. Runs over the finished HTML tree (after all
608+
# post-processing), so it depends on .htmldoc-stamp. One index is built per
609+
# language subtree (en/_pagefind, de/_pagefind, ...) rather than a single
610+
# index at the tree root: a merged index makes pagefind treat its largest
611+
# (primary) language as a catch-all that returns every language, so the topbar
612+
# search on an English page would surface German, Chinese, ... hits. A
613+
# per-language index physically contains only that language, so each page
614+
# loads its own subtree's index and results stay in one language. Each
615+
# _pagefind bundle (wasm + index shards + UI js/css) is self-contained; the
616+
# HTML is never modified, so a second build is a no-op. Gated on HTML docs
617+
# being enabled and pagefind installed; when absent the topbar carries no
618+
# search box (see @SEARCH_BOX@ above).
619+
ifeq ($(BUILD_DOCS_HTML),yes)
620+
ifneq ($(PAGEFIND),)
621+
htmldocs: .pagefind-stamp
622+
.pagefind-stamp: .htmldoc-stamp
623+
$(Q)rm -rf $(DOC_OUT_HTML)/pagefind
624+
$(Q)for lang in en $(LANGUAGES); do \
625+
rm -rf $(DOC_OUT_HTML)/$$lang/_pagefind; \
626+
$(PAGEFIND) --site $(DOC_OUT_HTML)/$$lang --output-subdir _pagefind || exit $$?; \
627+
done
628+
@touch $@
629+
endif
630+
endif
631+
598632
# Inject the whole-document sidebar/topbar and grey out missing language-
599633
# switcher entries. Runs last (depends on every HTML target) and is
600634
# idempotent. Gated on BUILD_DOCS_HTML, not translations: the sidebar comes
@@ -626,12 +660,13 @@ SHARED_HTML_ASSETS = \
626660

627661
# docinfo-header.html: generated from .in template. The @LANGUAGE_SWITCHER@
628662
# placeholder expands to one <li> per language (English plus everything
629-
# in $(LANGUAGES)), labelled via $(LANG_LABEL_<lang>). asciidoctor then
630-
# substitutes the {lcnc-cssrel} / {lcnc-lang-label} / {lcnc-subpath}
631-
# attributes per page. po4a.cfg drives the language list, lang-labels
632-
# the display names.
633-
$(DOC_SRCDIR)/docinfo-header.html: $(DOC_SRCDIR)/docinfo-header.html.in $(DOC_DIR)/po4a.cfg $(LANG_LABEL_FILE) $(DOC_SRCDIR)/Submakefile
634-
@block=$$(mktemp); \
663+
# in $(LANGUAGES)), labelled via $(LANG_LABEL_<lang>). @SEARCH_BOX@ expands
664+
# to the pagefind search widget when pagefind is installed, else to nothing.
665+
# asciidoctor then substitutes the {lcnc-cssrel} / {lcnc-lang-label} /
666+
# {lcnc-subpath} attributes per page. po4a.cfg drives the language list,
667+
# lang-labels the display names.
668+
$(DOC_SRCDIR)/docinfo-header.html: $(DOC_SRCDIR)/docinfo-header.html.in $(DOC_SRCDIR)/search-box.html $(DOC_DIR)/po4a.cfg $(LANG_LABEL_FILE) $(DOC_SRCDIR)/Submakefile
669+
@block=$$(mktemp); sbox=$$(mktemp); \
635670
printf ' <div class="lcnc-lang-switcher">\n' > $$block ; \
636671
printf ' <input type="checkbox" id="lcnc-lang-toggle" class="lcnc-lang-toggle">\n' >> $$block ; \
637672
printf ' <label for="lcnc-lang-toggle" class="lcnc-lang-label">{lcnc-lang-label}</label>\n' >> $$block ; \
@@ -640,11 +675,13 @@ $(DOC_SRCDIR)/docinfo-header.html: $(DOC_SRCDIR)/docinfo-header.html.in $(DOC_DI
640675
$(foreach L,$(LANGUAGES),printf ' <li><a href="{lcnc-cssrel}$(L)/{lcnc-subpath}">$(LANG_LABEL_$(L))</a></li>\n' >> $$block ;) \
641676
printf ' </ul>\n' >> $$block ; \
642677
printf ' </div>\n' >> $$block ; \
643-
awk -v block="$$block" ' \
678+
$(if $(PAGEFIND),cat $(DOC_SRCDIR)/search-box.html > $$sbox,: > $$sbox) ; \
679+
awk -v block="$$block" -v sbox="$$sbox" ' \
644680
/@LANGUAGE_SWITCHER@/ { while ((getline line < block) > 0) print line; next } \
681+
/@SEARCH_BOX@/ { while ((getline line < sbox) > 0) print line; next } \
645682
{ print } \
646683
' $< > $@ ; \
647-
rm -f $$block
684+
rm -f $$block $$sbox
648685

649686
# Stamp-gated asset copy; copy_asciidoc_files stays as a phony alias.
650687
# The en/gcode.html copy lives in its own rule above so it fires for PDF-only

docs/src/docinfo-header.html.in

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<header id="lcnc-topbar" class="lcnc-topbar">
1+
<header id="lcnc-topbar" class="lcnc-topbar" data-pagefind-ignore>
22
<a class="lcnc-topbar-home" href="{lcnc-cssrel}index.html">
33
<img src="{lcnc-cssrel}lcnc-docs.svg" alt="LinuxCNC Documentation">
44
</a>
@@ -13,5 +13,6 @@
1313
<span class="lcnc-sep" aria-hidden="true">&bull;</span>
1414
<a data-lcnc-link="4" href="{lcnc-cssrel}en/gcode.html">G-Code Quick Reference</a>
1515
</nav>
16+
@SEARCH_BOX@
1617
@LANGUAGE_SWITCHER@
1718
</header>

docs/src/lcnc-overrides.css

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@
1010
index.html / gcode.html.
1111
*/
1212

13+
/* Declare both schemes and pin the root background per theme so the canvas
14+
behind the translucent topbar is deterministic (no auto-dark flash). */
15+
:root { color-scheme: light dark; }
16+
html { background: #fff; }
17+
@media (prefers-color-scheme: dark) {
18+
html { background: #1e1e1e; }
19+
}
20+
1321
/* ======================================================================
1422
* Asciidoctor pages (light)
1523
* ====================================================================== */
@@ -769,4 +777,182 @@ h4,h5{font-size:revert}}
769777
.imageblock.text-center > .title { text-align: center; }
770778
.imageblock.text-right > .title { text-align: right; }
771779
.imageblock > .title { margin-top: 0.5em;}
780+
781+
/* Pagefind search box in the topbar (present only when built with pagefind).
782+
Theming flows through pagefind's CSS variables. */
783+
.lcnc-topbar-search {
784+
margin-left: auto;
785+
margin-right: 0.75rem;
786+
position: relative;
787+
display: flex;
788+
align-items: center;
789+
height: 100%;
790+
font-family: "Open Sans","DejaVu Sans",sans-serif;
791+
/* The bar is always dark, so the box sits on dark in both themes. Size via
792+
pagefind's scale, not an input height override (that desyncs the
793+
absolutely-positioned magnifier/clear). */
794+
--pagefind-ui-scale: 0.6;
795+
--pagefind-ui-background: transparent;
796+
--pagefind-ui-text: #fff; /* input text + magnifier on the dark bar */
797+
--pagefind-ui-primary: #fff;
798+
--pagefind-ui-border: hsla(0,0%,100%,.35);
799+
--pagefind-ui-tag: hsla(0,0%,100%,.15);
800+
--pagefind-ui-border-width: 1px;
801+
--pagefind-ui-border-radius: 4px;
802+
--pagefind-ui-font: "Open Sans","DejaVu Sans",sans-serif;
803+
}
804+
/* Search + language switcher form one right-aligned cluster; zero the
805+
switcher's own margin-left:auto so it does not split the free space. */
806+
.lcnc-topbar-search + .lcnc-lang-switcher { margin-left: 0; }
807+
.lcnc-topbar-search #lcnc-search { width: 13rem; }
808+
/* #lcnc-topbar (id) outranks pagefind-ui.css (injected after this sheet).
809+
The input is a faint white inset on the dark bar. */
810+
#lcnc-topbar .lcnc-topbar-search .pagefind-ui__search-input {
811+
background: hsla(0,0%,100%,.1);
812+
color: #d8d8d8; /* typed text: a smudge off pure white, both themes */
813+
border-color: hsla(0,0%,100%,.35);
814+
}
815+
#lcnc-topbar .lcnc-topbar-search .pagefind-ui__search-input:focus {
816+
background: hsla(0,0%,100%,.16);
817+
border-color: hsla(0,0%,100%,.55);
818+
/* replace the OS-accent (blue) focus ring with a neutral white halo */
819+
outline: none;
820+
box-shadow: 0 0 0 2px hsla(0,0%,100%,.18);
821+
}
822+
#lcnc-topbar .lcnc-topbar-search .pagefind-ui__search-input::placeholder { color: hsla(0,0%,100%,.7); }
823+
/* neutral selection tint (avoid the OS-accent blue) */
824+
.lcnc-topbar-search ::selection { background: hsla(0,0%,100%,.25); color: #fff; }
825+
/* Pagefind's clear control is a "Clear" text button; hide the word and draw a
826+
small white x (background-image SVG, font-independent). */
827+
#lcnc-topbar .lcnc-topbar-search .pagefind-ui__search-clear {
828+
font-size: 0;
829+
background: transparent;
830+
display: flex;
831+
align-items: center;
832+
}
833+
#lcnc-topbar .lcnc-topbar-search .pagefind-ui__search-clear::after {
834+
content: "";
835+
display: block;
836+
width: 13px;
837+
height: 13px;
838+
opacity: 0.7;
839+
background-image: var(--lcnc-x-icon);
840+
background-repeat: no-repeat;
841+
background-position: center;
842+
background-size: contain;
843+
}
844+
#lcnc-topbar .lcnc-topbar-search .pagefind-ui__search-clear:hover::after { opacity: 1; }
845+
.lcnc-topbar-search {
846+
--lcnc-x-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M5 5 L15 15 M15 5 L5 15' stroke='white' stroke-width='2.2' stroke-linecap='round'/%3E%3C/svg%3E");
847+
--lcnc-search-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2'%3E%3Ccircle cx='10' cy='10' r='7'/%3E%3Cline x1='15' y1='15' x2='20.5' y2='20.5' stroke-linecap='round'/%3E%3C/svg%3E");
848+
}
849+
/* Pagefind masks the magnifier with -webkit-mask, which Firefox/Zen render
850+
blank. Use a plain background-image SVG instead (works in every engine);
851+
pagefind's ::before size/position are kept. */
852+
#lcnc-topbar .lcnc-topbar-search .pagefind-ui__form::before {
853+
background-color: transparent;
854+
-webkit-mask: none;
855+
mask: none;
856+
background-image: var(--lcnc-search-icon);
857+
background-repeat: no-repeat;
858+
background-position: center;
859+
background-size: contain;
860+
opacity: 0.9;
861+
}
862+
/* Results drawer floats below the input. It is themed to match the page
863+
(light/dark), unlike the always-dark bar; redefining --pagefind-ui-text on
864+
the drawer recolours all result text in one shot. */
865+
.lcnc-topbar-search .pagefind-ui__drawer {
866+
position: absolute;
867+
top: 100%;
868+
right: 0;
869+
width: 30rem;
870+
max-height: 75vh;
871+
overflow-y: auto;
872+
border-radius: 0 0 6px 6px;
873+
padding: 0.5em 0.85em 0.85em;
874+
z-index: 1200;
875+
--pagefind-ui-text: #222;
876+
background: #fff;
877+
color: #222;
878+
border: 1px solid #d4d4d4;
879+
border-top: none;
880+
box-shadow: 0 6px 16px rgba(0,0,0,.18);
881+
}
882+
#lcnc-topbar .lcnc-topbar-search .pagefind-ui__result-link,
883+
#lcnc-topbar .lcnc-topbar-search .pagefind-ui__result-link:link { color: #2156a5; }
884+
#lcnc-topbar .lcnc-topbar-search .pagefind-ui__result-link:visited { color: #1d4b8f; }
885+
.lcnc-topbar-search .pagefind-ui__result-excerpt { color: #555; }
886+
.lcnc-topbar-search .pagefind-ui__message { color: #555; }
887+
/* Landing pages style bare p/ul/li generically and that leaks into the drawer;
888+
re-assert result metrics at id specificity so it looks the same everywhere. */
889+
#lcnc-topbar .lcnc-topbar-search .pagefind-ui__result-title {
890+
font-size: 1rem;
891+
line-height: 1.25;
892+
}
893+
#lcnc-topbar .lcnc-topbar-search .pagefind-ui__result-excerpt,
894+
#lcnc-topbar .lcnc-topbar-search .pagefind-ui__message {
895+
font-size: 0.85rem;
896+
line-height: 1.45;
897+
}
898+
#lcnc-topbar .lcnc-topbar-search .pagefind-ui__drawer ul {
899+
font-family: inherit;
900+
margin: 0;
901+
padding: 0;
902+
}
903+
#lcnc-topbar .lcnc-topbar-search .pagefind-ui__drawer li {
904+
line-height: normal;
905+
margin-top: 0;
906+
}
907+
/* soften pagefind's hard-yellow match highlight to a translucent wash */
908+
.lcnc-topbar-search .pagefind-ui__result mark,
909+
.lcnc-topbar-search mark {
910+
background: hsla(48,100%,50%,.30);
911+
color: inherit;
912+
padding: 0 1px;
913+
border-radius: 2px;
914+
}
915+
@media (prefers-color-scheme: dark) {
916+
/* Pin the pocket to solid fill + border. A translucent pocket on the
917+
translucent bar cold-paints inconsistently dark in Firefox/Zen; solid
918+
values matching the settled look keep it backdrop-independent. The bar
919+
itself stays translucent. */
920+
#lcnc-topbar .lcnc-topbar-search .pagefind-ui__search-input {
921+
background: #1f1f1f;
922+
border-color: #5d5d5d;
923+
}
924+
#lcnc-topbar .lcnc-topbar-search .pagefind-ui__search-input:focus {
925+
background: #2e2e2e;
926+
border-color: #8a8a8a;
927+
}
928+
.lcnc-topbar-search .pagefind-ui__drawer {
929+
--pagefind-ui-text: #e0e0e0;
930+
background: #1e1e1e;
931+
color: #e0e0e0;
932+
border-color: #444;
933+
box-shadow: 0 6px 16px rgba(0,0,0,.5);
934+
}
935+
#lcnc-topbar .lcnc-topbar-search .pagefind-ui__result-link,
936+
#lcnc-topbar .lcnc-topbar-search .pagefind-ui__result-link:link { color: #6fa8dc; }
937+
#lcnc-topbar .lcnc-topbar-search .pagefind-ui__result-link:visited { color: #b48ead; }
938+
.lcnc-topbar-search .pagefind-ui__result-excerpt { color: #aaa; }
939+
.lcnc-topbar-search .pagefind-ui__message { color: #aaa; }
940+
.lcnc-topbar-search .pagefind-ui__result mark,
941+
.lcnc-topbar-search mark {
942+
background: hsla(48,90%,55%,.25);
943+
color: inherit;
944+
}
945+
}
946+
/* The nav links are absolutely centred and ignore the right-hand cluster's
947+
width, so the search box needs more clearance than the bare 900px the nav
948+
normally hides at. Hide the nav earlier, but only in builds that actually
949+
carry a search box (pagefind installed); :has keeps the 900px default for
950+
search-less builds. */
951+
@media (max-width: 1300px) {
952+
.lcnc-topbar:has(.lcnc-topbar-search) .lcnc-topbar-links { display: none; }
953+
}
954+
@media (max-width: 1100px) { .lcnc-topbar-search #lcnc-search { width: 10rem; } }
955+
@media (max-width: 700px) {
956+
.lcnc-topbar-search .pagefind-ui__drawer { width: 88vw; }
957+
}
772958

docs/src/search-box.html

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<div class="lcnc-topbar-search">
2+
<div id="lcnc-search"></div>
3+
<script>
4+
window.addEventListener("DOMContentLoaded", function () {
5+
// One pagefind index is built per language subtree (en/_pagefind,
6+
// de/_pagefind, ...) so search results stay in a single language. Load
7+
// the index for THIS page's language, picked from <html lang>: the UI
8+
// assets and the index live in <lang>/_pagefind/ off the html root.
9+
var cssrel = "{lcnc-cssrel}";
10+
var pageLang = document.documentElement.lang || "en";
11+
var pfBase = cssrel + pageLang + "/_pagefind/";
12+
13+
// Result URLs are stored root-relative with the language dir included
14+
// ("/en/man/..."), so rewrite them against {lcnc-cssrel} to be relative
15+
// to the current page; that keeps links working under any deployment
16+
// path prefix. sub_results carry their own URLs and need it too.
17+
var rel = function (u) { return cssrel + u.replace(/^\//, ""); };
18+
19+
var link = document.createElement("link");
20+
link.rel = "stylesheet";
21+
link.href = pfBase + "pagefind-ui.css";
22+
document.head.appendChild(link);
23+
24+
var script = document.createElement("script");
25+
script.src = pfBase + "pagefind-ui.js";
26+
script.onload = function () {
27+
new PagefindUI({
28+
element: "#lcnc-search",
29+
// No bundlePath: pagefind-ui.js auto-derives the index location from
30+
// its own script src (loaded from pfBase above). An explicit
31+
// page-relative bundlePath would be re-resolved against the script
32+
// URL and double-count the path.
33+
showImages: false,
34+
showSubResults: true,
35+
processResult: function (result) {
36+
result.url = rel(result.url);
37+
if (result.sub_results) {
38+
result.sub_results.forEach(function (s) { s.url = rel(s.url); });
39+
}
40+
return result;
41+
}
42+
});
43+
};
44+
document.body.appendChild(script);
45+
});
46+
</script>
47+
</div>

0 commit comments

Comments
 (0)