Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add deprecated label to search results #18

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
23 changes: 12 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 36 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,31 @@ function getPrefixes(project: Project): Record<string, string> {
return prefixes;
}

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

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,
deprecated: class_.deprecated,
};
}

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 +256,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 +273,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 +286,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
3 changes: 3 additions & 0 deletions packages/explorer-site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
"build": "esbuild src/main.ts --bundle --minify --platform=browser --target=es2020 --outfile=../cli/src/assets/scripts/site.min.js",
"clean": "rimraf -g \"../cli/src/assets/scripts/*.js\""
},
"dependencies": {
"fuse.js": "^7.0.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
Expand Down
55 changes: 53 additions & 2 deletions packages/explorer-site/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,55 @@
window.onclick = function (ev) {
import Fuse from "fuse.js";

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

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) {
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"] });

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;
if (result.item.deprecated) {
a.classList.add("rdf-deprecated");
}
li.append(a);
ul.appendChild(li);
}
} else {
const li = document.createElement("li");
li.textContent = "No results found";
ul.appendChild(li);
}
resultsDiv.replaceChildren(ul);
});
}
}
}
});

window.addEventListener("click", function (ev) {
let target = ev.target;
while (target instanceof Element) {
if (target.tagName === "A") {
Expand All @@ -14,4 +65,4 @@ window.onclick = function (ev) {
}
target = target.parentElement;
}
}
});
1 change: 1 addition & 0 deletions packages/explorer-views/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface RenderContext {
readonly graph: Graph;
readonly schema: Schema;
readonly rootClasses: ReadonlySet<string> | null;
readonly searchEnabled?: boolean;

getPrefixes(): ReadonlyArray<[string, string]>;
lookupPrefixedName(iri: string): { readonly prefixLabel: string, readonly localName: string } | null;
Expand Down
68 changes: 68 additions & 0 deletions packages/explorer-views/src/pages/navigation.css
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,71 @@ nav {
color: var(--base07);
text-decoration: none;
}

.search-container {
position: relative;
}

#search {
background: inherit;
border: 1px solid var(--base04);
border-radius: .5em;
font-size: smaller;
margin: 1em 0 0 0;
padding: 0.5em;
width: 100%;
outline: none;
}

#search:placeholder-shown {
border-color: var(--base02);
}

#search:placeholder-shown ~ #results {
display: none;
}

#search:focus {
border-color: var(--base04);
}

#results {
background-color: var(--base00);
border: 1px solid var(--base03);
left: 0;
margin: 3px .5em 0;
max-height: 300px;
overflow-y: auto;
overscroll-behavior: none;
position: absolute;
right: 0;
z-index: 1000;
}

#results ul,
#results li {
list-style: none;
margin: 0;
padding: 0;
}

#results li {
border-top: 1px solid var(--base03);
}

#results li:first-child {
border-top: none;
}

#results a {
display: block;
overflow: hidden;
padding: 10px;
text-overflow: ellipsis;
white-space: nowrap;
}

#results a:hover {
text-decoration: none;
background: var(--base01);
}
12 changes: 11 additions & 1 deletion packages/explorer-views/src/pages/navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,17 @@ function createTree<T extends Class | Property | Ontology>(items: Iterable<T>, p

export default function render(title: string | undefined, context: RenderContext): HtmlContent {
return <>
<p class="logo">{title || "\u{1F141}\u{1F133}\u{1F135} Explorer"}</p>
{
context.searchEnabled ? <>
<div class="search-container">
<p class="logo">{title || "\u{1F141}\u{1F133}\u{1F135} Explorer"}</p>
<input id="search" name="search" placeholder="Search..." autocomplete="off" />
<div id="results"></div>
</div>
</> : <>
<p class="logo">{title || "\u{1F141}\u{1F133}\u{1F135} Explorer"}</p>
</>
}
{
renderTabView("navigation", [
{
Expand Down
Loading