Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions build/src/php/FileGenerator/Application/ApiSearchGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,19 @@ public function generateSearchIndex(): array
}

$result[] = [
'id' => 'api_' . $fn->fnName(),
'fnName' => $fn->fnName(),
'fnSignature' => $fn->fnSignature(),
'desc' => $this->formatDescription($fn->description()),
'anchor' => $anchor,
'type' => 'api',
];
}

// Add documentation files to search index
$documentationItems = $this->generateDocumentationSearchItems();
$result = array_merge($result, $documentationItems);

return $result;
}

Expand All @@ -68,4 +74,61 @@ private function formatDescription(string $desc): string
{
return preg_replace('/\[(.*?)\]\((.*?)\)/', '<i>$1</i>', $desc);
}

/**
* Generate search index items for documentation files
*
* @return array<array{id: string, title: string, content: string, url: string, type: string}>
*/
private function generateDocumentationSearchItems(): array
{
$result = [];
$documentationPath = __DIR__ . '/../../../../../content/documentation';

if (!is_dir($documentationPath)) {
error_log("Documentation path not found: " . $documentationPath);
return [];
}

$files = scandir($documentationPath);
if ($files === false) {
error_log("Could not scan documentation directory: " . $documentationPath);
return [];
}

foreach ($files as $file) {
if (pathinfo($file, PATHINFO_EXTENSION) !== 'md' || $file === '_index.md') {
continue;
}

$filePath = $documentationPath . '/' . $file;
$content = file_get_contents($filePath);

// Extract title from frontmatter
$title = pathinfo($file, PATHINFO_FILENAME);
if (preg_match('/title = "([^"]+)"/', $content, $matches)) {
$title = $matches[1];
}

// Remove frontmatter
$content = preg_replace('/\+\+\+.*?\+\+\+/s', '', $content);

// Remove markdown formatting and clean content
$content = preg_replace('/[#`*\[\]()]/', ' ', $content);
$content = preg_replace('/\s+/', ' ', trim($content));

// Limit content length for search index
$content = substr($content, 0, 500);

$result[] = [
'id' => 'doc_' . pathinfo($file, PATHINFO_FILENAME),
'title' => $title,
'content' => $content,
'url' => '/documentation/' . pathinfo($file, PATHINFO_FILENAME),
'type' => 'documentation',
];
}

return $result;
}
}
58 changes: 52 additions & 6 deletions build/tests/php/FileGenerator/Domain/ApiSearchGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,20 @@ public function test_generate_search_index_one_item(): void

$expected = [
[
'id' => 'api_table?',
'fnName' => 'table?',
'fnSignature' => '(table? x)',
'desc' => 'doc for table?',
'anchor' => 'table',
'type' => 'api',
],
];

self::assertEquals($expected, $actual);
// Filter out documentation items for this test
$apiItems = array_filter($actual, fn($item) => $item['type'] === 'api');
$apiItems = array_values($apiItems); // Re-index array

self::assertEquals($expected, $apiItems);
}

public function test_multiple_items_in_different_groups(): void
Expand All @@ -63,20 +69,28 @@ public function test_multiple_items_in_different_groups(): void

$expected = [
[
'id' => 'api_table',
'fnName' => 'table',
'fnSignature' => '(table & xs)',
'desc' => 'doc for table',
'anchor' => 'table',
'type' => 'api',
],
[
'id' => 'api_not',
'fnName' => 'not',
'fnSignature' => '(not x)',
'desc' => 'doc for not',
'anchor' => 'not',
'type' => 'api',
],
];

self::assertEquals($expected, $actual);
// Filter out documentation items for this test
$apiItems = array_filter($actual, fn($item) => $item['type'] === 'api');
$apiItems = array_values($apiItems); // Re-index array

self::assertEquals($expected, $apiItems);
}

public function test_multiple_items_in_the_same_group(): void
Expand All @@ -103,20 +117,28 @@ public function test_multiple_items_in_the_same_group(): void

$expected = [
[
'id' => 'api_table',
'fnName' => 'table',
'fnSignature' => '(table & xs)',
'desc' => 'doc for table',
'anchor' => 'table',
'type' => 'api',
],
[
'id' => 'api_table?',
'fnName' => 'table?',
'fnSignature' => '(table? x)',
'desc' => 'doc for table?',
'anchor' => 'table-1',
'type' => 'api',
],
];

self::assertEquals($expected, $actual);
// Filter out documentation items for this test
$apiItems = array_filter($actual, fn($item) => $item['type'] === 'api');
$apiItems = array_values($apiItems); // Re-index array

self::assertEquals($expected, $apiItems);
}

public function test_fn_name_with_slash_in_the_middle(): void
Expand All @@ -143,20 +165,28 @@ public function test_fn_name_with_slash_in_the_middle(): void

$expected = [
[
'id' => 'api_http/response',
'fnName' => 'http/response',
'fnSignature' => '',
'desc' => '',
'anchor' => 'http-response',
'type' => 'api',
],
[
'id' => 'api_http/response?',
'fnName' => 'http/response?',
'fnSignature' => '',
'desc' => '',
'anchor' => 'http-response-1',
'type' => 'api',
],
];

self::assertEquals($expected, $actual);
// Filter out documentation items for this test
$apiItems = array_filter($actual, fn($item) => $item['type'] === 'api');
$apiItems = array_values($apiItems); // Re-index array

self::assertEquals($expected, $apiItems);
}

public function test_fn_name_ending_with_minus(): void
Expand All @@ -183,20 +213,28 @@ public function test_fn_name_ending_with_minus(): void

$expected = [
[
'id' => 'api_defn',
'fnName' => 'defn',
'fnSignature' => '',
'desc' => '',
'anchor' => 'defn',
'type' => 'api',
],
[
'id' => 'api_defn-',
'fnName' => 'defn-',
'fnSignature' => '',
'desc' => '',
'anchor' => 'defn-1',
'type' => 'api',
],
];

self::assertEquals($expected, $actual);
// Filter out documentation items for this test
$apiItems = array_filter($actual, fn($item) => $item['type'] === 'api');
$apiItems = array_values($apiItems); // Re-index array

self::assertEquals($expected, $apiItems);
}

public function test_fn_name_with_upper_case(): void
Expand All @@ -223,19 +261,27 @@ public function test_fn_name_with_upper_case(): void

$expected = [
[
'id' => 'api_NAN',
'fnName' => 'NAN',
'fnSignature' => '',
'desc' => '',
'anchor' => 'nan',
'type' => 'api',
],
[
'id' => 'api_nan?',
'fnName' => 'nan?',
'fnSignature' => '',
'desc' => '',
'anchor' => 'nan-1',
'type' => 'api',
],
];

self::assertEquals($expected, $actual);
// Filter out documentation items for this test
$apiItems = array_filter($actual, fn($item) => $item['type'] === 'api');
$apiItems = array_values($apiItems); // Re-index array

self::assertEquals($expected, $apiItems);
}
}
30 changes: 23 additions & 7 deletions static/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ function initSearch() {
const index = elasticlunr(function () {
this.addField("fnName");
this.addField("desc");
this.setRef("anchor");
this.addField("title");
this.addField("content");
this.setRef("id");
elasticlunr.stopWordFilter.stopWords = {};
elasticlunr.Pipeline.registerFunction(elasticlunr.trimmer, "trimmer");
elasticlunr.tokenizer.seperator = /[\s~~]+/;
Expand Down Expand Up @@ -174,10 +176,12 @@ function showResults(index) {
}

const options = {
bool: "AND",
bool: "OR",
fields: {
fnName: {boost: 3},
title: {boost: 2},
desc: {boost: 1},
content: {boost: 1}
},
expand: true
};
Expand All @@ -188,6 +192,7 @@ function showResults(index) {
fnSignature: "",
desc: "Cannot provide any Phel symbol. Try something else",
anchor: "#",
type: "api"
};

createMenuItem(emptyResult, null);
Expand Down Expand Up @@ -215,11 +220,22 @@ function createMenuItem(result, index) {
}

function formatSearchResultItem(item) {
return `<a href="/documentation/api/#${item.anchor}">`
+ `<div class="search-results__item">${item.fnName} `
+ `<small class="fn-signature">${item.fnSignature}</small>`
+ `<span class="desc">${item.desc}</span>`
+ `</div></a>`;
if (item.type === "documentation") {
return `<a href="${item.url}">`
+ `<div class="search-results__item">`
+ `<span class="result-type">Documentation: </span>`
+ `<strong>${item.title}</strong>`
+ `<span class="desc">${item.content}</span>`
+ `</div></a>`;
} else {
return `<a href="/documentation/api/#${item.anchor}">`
+ `<div class="search-results__item">`
+ `<span class="result-type">API: </span>`
+ `${item.fnName} `
+ `<small class="fn-signature">${item.fnSignature}</small>`
+ `<span class="desc">${item.desc}</span>`
+ `</div></a>`;
}
}

function removeSelectedClassFromSearchResult() {
Expand Down
2 changes: 1 addition & 1 deletion templates/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</div>

<div class="site-header__search">
<input type="search" id="search" autocomplete="off" placeholder="Search in the API...">
<input type="search" id="search" autocomplete="off" placeholder="Search in the docs & API...">

<div id="search-results" class="search-results">
<ul id="search-results__items" class="search-results__items"></ul>
Expand Down
Loading