Skip to content

Commit

Permalink
Dynamically build and fetch search index
Browse files Browse the repository at this point in the history
  • Loading branch information
ektrah committed Jul 4, 2024
1 parent 78d762c commit 7979a48
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 332 deletions.
37 changes: 34 additions & 3 deletions packages/cli/src/commands/make-site.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Ix } from "@rdf-toolkit/iterable";
import { Graph } from "@rdf-toolkit/rdf/graphs";
import { BlankNode, IRI, IRIOrBlankNode } from "@rdf-toolkit/rdf/terms";
import { ParsedTriple } from "@rdf-toolkit/rdf/triples";
import { Schema } from "@rdf-toolkit/schema";
import { Class, Schema } from "@rdf-toolkit/schema";
import { DiagnosticBag, TextDocument } from "@rdf-toolkit/text";
import { SyntaxTree } from "@rdf-toolkit/turtle";
import * as fs from "node:fs";
Expand Down Expand Up @@ -37,6 +37,7 @@ const ERROR_FILE_NAME = "404.html";
const CSS_FILE_NAME = "style.css";
const FONT_FILE_NAME = path.basename(fontAssetFilePath);
const SCRIPT_FILE_NAME = path.basename(scriptAssetFilePath);
const SEARCH_FILE_NAME = "index.json";

class Website implements RenderContext {
readonly dataset: ParsedTriple[][] = [];
Expand All @@ -55,13 +56,15 @@ class Website implements RenderContext {

readonly outputs: Record<string, string> = {};
readonly rootClasses: ReadonlySet<string> | null;
readonly searchEnabled: boolean;

constructor(readonly title: string, readonly baseURL: string, prefixes: Record<string, string>, readonly cleanUrls: boolean, rootClasses: Iterable<string> | undefined, readonly diagnostics: DiagnosticBag) {
constructor(readonly title: string, readonly baseURL: string, prefixes: Record<string, string>, readonly cleanUrls: boolean, rootClasses: Iterable<string> | undefined, searchEnabled: boolean, readonly diagnostics: DiagnosticBag) {
this.graph = Graph.from(this.dataset);
this.schema = Schema.decompile(this.dataset, this.graph);
this.namespaces.push(prefixes);
this.prefixes = new PrefixTable(this.namespaces);
this.rootClasses = rootClasses ? new Set(rootClasses) : null;
this.searchEnabled = searchEnabled;
}

getPrefixes(): ReadonlyArray<[string, string]> {
Expand Down Expand Up @@ -220,6 +223,29 @@ function getPrefixes(project: Project): Record<string, string> {
return prefixes;
}

interface SearchEntry {
readonly "href"?: string;
readonly "id": string;
readonly "name": string;
readonly "description"?: string;
}

function buildSearchEntryForClass(class_: Class, context: RenderContext): SearchEntry {
const prefixedName = context.lookupPrefixedName(class_.id.value);
return {
href: context.rewriteHrefAsData ? context.rewriteHrefAsData(class_.id.value) : undefined,
id: class_.id.value,
name: prefixedName ? prefixedName.prefixLabel + ":" + prefixedName.localName : class_.id.value,
description: class_.description,
};
}

function buildSearchIndex(schema: Schema, context: RenderContext): Array<SearchEntry> {
return Array.from(schema.classes.values())
.sort((a, b) => a.id.compareTo(b.id))
.map(class_ => buildSearchEntryForClass(class_, context));
}

export default function main(options: Options): void {
const moduleFilePath = url.fileURLToPath(import.meta.url);
const modulePath = path.dirname(moduleFilePath);
Expand All @@ -228,7 +254,7 @@ export default function main(options: Options): void {
const icons = project.json.siteOptions?.icons || [];
const assets = project.json.siteOptions?.assets || {};

const context = new Website(project.json.siteOptions?.title || DEFAULT_TITLE, new URL(options.base || project.json.siteOptions?.baseURL || DEFAULT_BASE, DEFAULT_BASE).href, getPrefixes(project), !!project.json.siteOptions?.cleanUrls, project.json.siteOptions?.roots, project.diagnostics);
const context = new Website(project.json.siteOptions?.title || DEFAULT_TITLE, new URL(options.base || project.json.siteOptions?.baseURL || DEFAULT_BASE, DEFAULT_BASE).href, getPrefixes(project), !!project.json.siteOptions?.cleanUrls, project.json.siteOptions?.roots, true, project.diagnostics);
const site = new Workspace(project.package.resolve(options.output || project.json.siteOptions?.outDir || "public"));

context.beforecompile();
Expand All @@ -245,6 +271,7 @@ export default function main(options: Options): void {
const links = <>
{icons.map(iconConfig => <link rel="icon" type={iconConfig.type} sizes={iconConfig.sizes} href={resolveHref(path.basename(iconConfig.asset), context.baseURL)} />)}
<link rel="stylesheet" href={resolveHref(CSS_FILE_NAME, context.baseURL)} />
{context.searchEnabled ? <link rel="preload" type="application/json" href={resolveHref(SEARCH_FILE_NAME, context.baseURL)} as="fetch" crossorigin="anonymous" /> : <></>}
</>;

const scripts = <>
Expand All @@ -257,6 +284,10 @@ export default function main(options: Options): void {
site.write(FONT_FILE_NAME, fs.readFileSync(path.resolve(modulePath, fontAssetFilePath)));
site.write(SCRIPT_FILE_NAME, fs.readFileSync(path.resolve(modulePath, scriptAssetFilePath)));

if (context.searchEnabled) {
site.writeJSON(SEARCH_FILE_NAME, buildSearchIndex(context.schema, context));
}

for (const iconConfig of icons) {
site.write(path.basename(iconConfig.asset), project.package.read(iconConfig.asset));
}
Expand Down
62 changes: 33 additions & 29 deletions packages/explorer-site/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,47 @@
import Fuse from "fuse.js";
import searchData from "./search.json";

interface SearchEntry {
"id": string,
"name": string,
"description": string,
"type": string
readonly "href"?: string;
readonly "id": string;
readonly "name": string;
readonly "description"?: string;
}

const fuse = new Fuse<SearchEntry>(searchData, {
keys: ["name", "description"],
});

document.addEventListener("DOMContentLoaded", function () {
document.addEventListener("DOMContentLoaded", async function () {
const links = document.getElementsByTagName("link");
const searchInput = document.getElementById("search") as HTMLInputElement;
const resultsDiv = document.getElementById("results") as HTMLDivElement;

if (searchInput && resultsDiv) {
searchInput.addEventListener("input", function () {
const searchResults = fuse.search(searchInput.value);
if (searchResults.length) {
const ul = document.createElement("ul");
for (const result of searchResults) {
const li = document.createElement("li");
const a = document.createElement("a");
a.href = result.item.id;
a.dataset.href = "/" + result.item.name.replace(/:/g, "/");
a.textContent = result.item.name;
li.append(a);
ul.appendChild(li);
};
for (let i = 0; i < links.length; i++) {
const link = links.item(i);
if (link && link.rel === "preload" && link.type === "application/json" && link.as === "fetch") {
const response = await fetch(new URL(link.href, document.location.href).href);
const searchData = await response.json() as ReadonlyArray<SearchEntry>;
const fuse = new Fuse<SearchEntry>(searchData, { keys: ["name", "description"] });

resultsDiv.replaceChildren(ul);
resultsDiv.style.display = "block";
} else {
resultsDiv.textContent = "No results found";
resultsDiv.style.display = "none";
searchInput.addEventListener("input", function () {
const searchResults = fuse.search(searchInput.value);
const ul = document.createElement("ul");
if (searchResults.length) {
for (const result of searchResults) {
const li = document.createElement("li");
const a = document.createElement("a");
a.href = result.item.id;
a.dataset.href = result.item.href;
a.textContent = result.item.name;
li.append(a);
ul.appendChild(li);
}
} else {
const li = document.createElement("li");
li.textContent = "No results found";
ul.appendChild(li);
}
resultsDiv.replaceChildren(ul);
});
}
});
}
}
});

Expand Down
Loading

0 comments on commit 7979a48

Please sign in to comment.