+NAV;
+ return $header . $nav;
+ }
+
+ public function footer($id)
+ {
+ $oldFooter = parent::footer($id);
+ $oldFooter = str_replace('', '', $oldFooter);
+
+ $this->fetchJS();
+
+ $footer = '';
+ $footer .= '';
+ foreach ($this->js as $jsFile) {
+ $footer .= '';
+ }
+
+ $footer .= <<
+
+
+
+
+
+ ↑ and ↓ to navigate •
+ Enter to select •
+ Esc to close
+
+
+
+
+
+ HTML;
+
+ return $oldFooter . $footer . '';
+ }
+
+ private function mkdir(string $dir): void
+ {
+ if (file_exists($dir)) {
+ if (!is_dir($dir)) {
+ trigger_error("The styles/ directory is a file?", E_USER_ERROR);
+ }
+ } else {
+ if (!mkdir($dir, 0777, true)) {
+ trigger_error("Can't create the styles/ directory.", E_USER_ERROR);
+ }
+ }
+ }
+
+ private function fetchJS(): void
+ {
+ if ($this->js !== []) {
+ return;
+ }
+ $outputDir = $this->getOutputDir();
+ if (!$outputDir) {
+ $outputDir = $this->config->outputDir;
+ }
+ $jsDir = 'js/';
+ $outputDir .= '/' . $jsDir;
+ $this->mkdir($outputDir);
+
+ $files = [
+ 'https://www.php.net/js/ext/FuzzySearch.min.js',
+ __DIR__ . '/search.js',
+ ];
+ foreach ($files as $sourceFile) {
+ $basename = basename($sourceFile);
+ $dest = md5(substr($sourceFile, 0, -strlen($basename))) . '-' . $basename;
+ if (@copy($sourceFile, $outputDir . $dest)) {
+ $this->js[] = $jsDir . $dest;
+ } else {
+ trigger_error(vsprintf('Impossible to copy the %s file.', [$sourceFile]), E_USER_WARNING);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/phpdotnet/phd/Package/PHP/search.js b/phpdotnet/phd/Package/PHP/search.js
new file mode 100644
index 00000000..3967852c
--- /dev/null
+++ b/phpdotnet/phd/Package/PHP/search.js
@@ -0,0 +1,438 @@
+/**
+ * Initialize the PHP search functionality with a given language.
+ * Loads the search index, sets up FuzzySearch, and returns a search function.
+ *
+ * @param {string} language The language for which the search index should be
+ * loaded.
+ * @returns {Promise<(query: string) => Array>} A function that takes a query
+ * and performs a search using the loaded index.
+ */
+const initPHPSearch = async (language) => {
+ const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
+ const CACHE_DAYS = 14;
+
+ /**
+ * Looks up the search index cached in localStorage.
+ *
+ * @returns {Array|null}
+ */
+ const lookupIndexCache = () => {
+ const key = `search2-${language}`;
+ const cache = window.localStorage.getItem(key);
+
+ if (!cache) {
+ return null;
+ }
+
+ const { data, time: cachedDate } = JSON.parse(cache);
+
+ const expireDate = cachedDate + CACHE_DAYS * MILLISECONDS_PER_DAY;
+
+ if (Date.now() > expireDate) {
+ return null;
+ }
+
+ return data;
+ };
+
+ /**
+ * Fetch the search index.
+ *
+ * @returns {Promise