feat: add IETF RFC search extension

This commit is contained in:
2026-08-01 22:34:01 +09:00
commit f1183e1095
15 changed files with 3042 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
use flake
+2
View File
@@ -0,0 +1,2 @@
node_modules
vicinae-env.d.ts
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 yuta
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+50
View File
@@ -0,0 +1,50 @@
# IETF RFC Search for Vicinae
Search and inspect the complete RFC catalog without leaving Vicinae.
## Features
- Search 9,000+ RFCs by number, title, author, keyword, or abstract
- Use natural number formats such as `9110`, `RFC 9110`, or `rfc9110`
- Filter by standards status, including Internet Standard, Proposed Standard, BCP, Informational, Experimental, and Historic
- Read the abstract and publication metadata in the list detail pane
- See prominent warnings and relationships for obsoleted or updated RFCs
- Open the official HTML, TXT, PDF, or XML document when that format is available
- Open RFC Editor info, IETF Datatracker, errata, and DOI pages
- Copy the canonical URL or a short citation
- Browse the newest RFCs before entering a query
## Data source and caching
The extension uses the official [RFC Editor index](https://www.rfc-editor.org/rfc-index.xml) as its only search data source. The index is downloaded on first launch and cached locally for 24 hours. Search is then performed locally, so typing does not send search terms to a remote service and repeated searches are immediate.
If a refresh fails after a successful download, the extension continues to use the cached index. Use **Refresh RFC Index** from an item's action panel to request the latest catalog manually.
## Development
Requirements:
- Node.js 20 or newer
- A running [Vicinae](https://vicinae.com/) installation
With Nix and direnv, enter the included development shell with `direnv allow` (or `nix develop`).
Install dependencies and start development mode:
```bash
npm install
npm run dev
```
Run formatting, linting, tests, type checking, and a production build:
```bash
npm run format
npm run check
```
The focused commands are also available as `npm run lint`, `npm test`, and `npm run build`.
## License
MIT
Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

+9
View File
@@ -0,0 +1,9 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.2/schema.json",
"linter": {
"enabled": true,
"domains": {
"test": "recommended"
}
}
}
Generated
+25
View File
@@ -0,0 +1,25 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1785454630,
"narHash": "sha256-LQy14TZp77TwbQf40gg1V3jo8FwJG0jGDkAH+zRHqg8=",
"rev": "1559d3daa3ecc813a650b79375ea61b6741b8746",
"revCount": 1044798,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.1044798%2Brev-1559d3daa3ecc813a650b79375ea61b6741b8746/019fb7b4-037e-75a0-a315-35190eb80bd3/source.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://flakehub.com/f/NixOS/nixpkgs/0.1"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
+50
View File
@@ -0,0 +1,50 @@
{
description = "A Nix-flake-based Node.js development environment";
inputs.nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1"; # unstable Nixpkgs
outputs =
{ self, ... }@inputs:
let
supportedSystems = [
"x86_64-linux"
"aarch64-linux"
"aarch64-darwin"
];
forEachSupportedSystem =
f:
inputs.nixpkgs.lib.genAttrs supportedSystems (
system:
f {
inherit system;
pkgs = import inputs.nixpkgs {
inherit system;
overlays = [ inputs.self.overlays.default ];
};
}
);
in
{
overlays.default = final: prev: rec {
nodejs = prev.nodejs;
yarn = (prev.yarn.override { inherit nodejs; });
};
devShells = forEachSupportedSystem (
{ pkgs, system }:
{
default = pkgs.mkShellNoCC {
packages = with pkgs; [
nodejs
pnpm
yarn
self.formatter.${system}
];
};
}
);
formatter = forEachSupportedSystem ({ pkgs, ... }: pkgs.nixfmt);
};
}
+2176
View File
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
{
"$schema": "https://raw.githubusercontent.com/vicinaehq/vicinae/refs/heads/main/extra/schemas/extension.json",
"name": "vicinae-extension-ietf-rfc",
"title": "IETF RFC Search",
"description": "Search and inspect the complete RFC Editor index",
"categories": [],
"license": "MIT",
"author": "yuta",
"contributors": [],
"pastContributors": [],
"icon": "extension_icon-v2.png",
"commands": [
{
"name": "search-rfc",
"title": "Search IETF RFCs",
"description": "Search RFCs by number, title, author, keyword, or abstract",
"mode": "view"
}
],
"preferences": [],
"scripts": {
"build": "vici build",
"dev": "vici develop",
"format": "biome format --write src",
"lint": "biome check src && vici lint",
"test": "vitest run",
"check": "npm run lint && npm run test && npm run build"
},
"dependencies": {
"@vicinae/api": "^0.23.2",
"fast-xml-parser": "^5.10.1"
},
"devDependencies": {
"@biomejs/biome": "2.3.2",
"typescript": "^5.9.2",
"vitest": "^4.1.10"
}
}
+126
View File
@@ -0,0 +1,126 @@
import { describe, expect, it } from "vitest";
import { parseRfcIndex, type Rfc, rfcUrl, searchRfcs } from "./rfc-index";
const fixture = `<?xml version="1.0"?>
<rfc-index xmlns="https://www.rfc-editor.org/rfc-index">
<rfc-entry>
<doc-id>RFC6749</doc-id>
<title>The OAuth 2.0 Authorization Framework</title>
<author><name>D. Hardt</name></author>
<date><month>October</month><year>2012</year></date>
<format><file-format>TXT</file-format><file-format>HTML</file-format></format>
<page-count>76</page-count>
<keywords><kw>authorization</kw><kw>access token</kw></keywords>
<abstract><p>This document specifies OAuth 2.0.</p></abstract>
<obsoletes><doc-id>RFC5849</doc-id></obsoletes>
<updated-by><doc-id>RFC8252</doc-id><doc-id>RFC8996</doc-id></updated-by>
<current-status>PROPOSED STANDARD</current-status>
<stream>IETF</stream>
<area>sec</area>
<wg_acronym>oauth</wg_acronym>
<errata-url>https://www.rfc-editor.org/errata/rfc6749</errata-url>
<doi>10.17487/RFC6749</doi>
</rfc-entry>
<rfc-entry>
<doc-id>RFC8259</doc-id>
<title>The JavaScript Object Notation (JSON) Data Interchange Format</title>
<author><name>T. Bray</name></author>
<date><month>December</month><year>2017</year></date>
<format><file-format>HTML</file-format></format>
<page-count>16</page-count>
<keywords><kw>JSON</kw></keywords>
<abstract><p>A lightweight, text-based data interchange format.</p></abstract>
<is-also><doc-id>STD90</doc-id></is-also>
<current-status>INTERNET STANDARD</current-status>
<stream>IETF</stream>
</rfc-entry>
<rfc-entry>
<doc-id>RFC7159</doc-id>
<title>The JavaScript Object Notation (JSON) Data Interchange Format</title>
<author><name>T. Bray</name></author>
<date><month>March</month><year>2014</year></date>
<format><file-format>HTML</file-format></format>
<page-count>15</page-count>
<abstract><p>An older JSON specification.</p></abstract>
<obsoleted-by><doc-id>RFC8259</doc-id></obsoleted-by>
<current-status>INTERNET STANDARD</current-status>
<stream>IETF</stream>
</rfc-entry>
</rfc-index>`;
const rfcs = parseRfcIndex(fixture);
describe("parseRfcIndex", () => {
it("normalizes RFC Editor fields and relationships", () => {
expect(rfcs[0]).toMatchObject({
number: 6749,
docId: "RFC6749",
title: "The OAuth 2.0 Authorization Framework",
authors: ["D. Hardt"],
month: "October",
year: 2012,
formats: ["TXT", "HTML"],
pageCount: 76,
keywords: ["authorization", "access token"],
abstract: "This document specifies OAuth 2.0.",
obsoletes: ["RFC5849"],
updatedBy: ["RFC8252", "RFC8996"],
status: "PROPOSED STANDARD",
workingGroup: "oauth",
doi: "10.17487/RFC6749",
});
});
it("rejects a document without RFC entries", () => {
expect(() => parseRfcIndex("<rfc-index />")).toThrow(
"The RFC index did not contain any RFC entries.",
);
});
});
describe("searchRfcs", () => {
it.each(["6749", "RFC 6749", "rfc6749", "RFC-06749"])(
"treats %s as an exact RFC number",
(query) => {
expect(searchRfcs(rfcs, query).map((rfc) => rfc.docId)).toEqual([
"RFC6749",
]);
},
);
it("searches titles, authors, keywords, and abstracts", () => {
expect(searchRfcs(rfcs, "D. Hardt")[0]?.docId).toBe("RFC6749");
expect(searchRfcs(rfcs, "access token")[0]?.docId).toBe("RFC6749");
expect(searchRfcs(rfcs, "lightweight text-based")[0]?.docId).toBe(
"RFC8259",
);
});
it("orders equally relevant results from newest RFC number to oldest", () => {
expect(searchRfcs(rfcs, "JSON").map((rfc) => rfc.docId)).toEqual([
"RFC8259",
"RFC7159",
]);
});
it("shows the latest RFCs when the search is empty", () => {
expect(searchRfcs(rfcs, "", "all", 2).map((rfc) => rfc.docId)).toEqual([
"RFC8259",
"RFC7159",
]);
});
it("filters results by current status", () => {
expect(
searchRfcs(rfcs, "", "PROPOSED STANDARD").map((rfc) => rfc.docId),
).toEqual(["RFC6749"]);
});
});
describe("rfcUrl", () => {
it("builds a canonical RFC Editor format URL", () => {
expect(rfcUrl(rfcs[0] as Rfc, "PDF")).toBe(
"https://www.rfc-editor.org/rfc/rfc6749.pdf",
);
});
});
+172
View File
@@ -0,0 +1,172 @@
import { XMLParser } from "fast-xml-parser";
export const RFC_INDEX_URL = "https://www.rfc-editor.org/rfc-index.xml";
export type Rfc = {
number: number;
docId: string;
title: string;
authors: string[];
month?: string;
year?: number;
formats: string[];
pageCount?: number;
keywords: string[];
abstract: string;
status: string;
stream?: string;
area?: string;
workingGroup?: string;
doi?: string;
errataUrl?: string;
isAlso: string[];
obsoletes: string[];
obsoletedBy: string[];
updates: string[];
updatedBy: string[];
};
type XmlValue = string | number | Record<string, unknown> | XmlValue[];
const parser = new XMLParser({
ignoreAttributes: true,
parseTagValue: false,
trimValues: true,
});
function arrayOf<T>(value: T | T[] | undefined): T[] {
if (value === undefined) return [];
return Array.isArray(value) ? value : [value];
}
function text(value: unknown): string | undefined {
if (typeof value === "string" || typeof value === "number") {
return String(value).trim() || undefined;
}
return undefined;
}
function childText(value: unknown, key: string): string[] {
if (!value || typeof value !== "object" || Array.isArray(value)) return [];
const child = (value as Record<string, XmlValue>)[key];
return arrayOf(child)
.map(text)
.filter((item): item is string => Boolean(item));
}
function paragraphs(value: unknown): string {
return childText(value, "p").join("\n\n");
}
function relation(value: unknown): string[] {
return childText(value, "doc-id");
}
function normalizeEntry(raw: Record<string, unknown>): Rfc | undefined {
const docId = text(raw["doc-id"]);
const title = text(raw.title);
const number = docId?.match(/^RFC(\d+)$/)?.[1];
if (!docId || !title || !number) return undefined;
const authors = arrayOf(raw.author)
.flatMap((author) => childText(author, "name"))
.filter(Boolean);
const date = raw.date as Record<string, unknown> | undefined;
const year = Number(text(date?.year));
const pageCount = Number(text(raw["page-count"]));
return {
number: Number(number),
docId,
title,
authors,
month: text(date?.month),
year: Number.isFinite(year) ? year : undefined,
formats: childText(raw.format, "file-format"),
pageCount: Number.isFinite(pageCount) ? pageCount : undefined,
keywords: childText(raw.keywords, "kw"),
abstract: paragraphs(raw.abstract),
status: text(raw["current-status"]) ?? "UNKNOWN",
stream: text(raw.stream),
area: text(raw.area),
workingGroup: text(raw.wg_acronym),
doi: text(raw.doi),
errataUrl: text(raw["errata-url"]),
isAlso: relation(raw["is-also"]),
obsoletes: relation(raw.obsoletes),
obsoletedBy: relation(raw["obsoleted-by"]),
updates: relation(raw.updates),
updatedBy: relation(raw["updated-by"]),
};
}
export function parseRfcIndex(xml: string): Rfc[] {
const parsed = parser.parse(xml) as Record<string, Record<string, unknown>>;
const entries = parsed["rfc-index"]?.["rfc-entry"];
if (!entries)
throw new Error("The RFC index did not contain any RFC entries.");
return arrayOf(entries)
.map((entry) => normalizeEntry(entry as Record<string, unknown>))
.filter((entry): entry is Rfc => Boolean(entry));
}
export type StatusFilter = "all" | Rfc["status"];
function searchableText(rfc: Rfc): string {
return [
rfc.docId,
rfc.title,
...rfc.authors,
...rfc.keywords,
rfc.abstract,
...rfc.isAlso,
]
.join(" ")
.toLocaleLowerCase();
}
function scoreRfc(rfc: Rfc, query: string): number | undefined {
const normalized = query.trim().toLocaleLowerCase();
if (!normalized) return 0;
const exactNumber = normalized.match(/^(?:rfc[\s-]*)?0*(\d{1,5})$/)?.[1];
if (exactNumber) return rfc.number === Number(exactNumber) ? 0 : undefined;
const tokens = normalized.split(/\s+/).filter(Boolean);
const title = rfc.title.toLocaleLowerCase();
const primary = [rfc.docId, rfc.title, ...rfc.keywords, ...rfc.authors]
.join(" ")
.toLocaleLowerCase();
if (title.startsWith(normalized)) return 10;
if (title.includes(normalized)) return 20;
if (tokens.every((token) => primary.includes(token))) return 30;
if (tokens.every((token) => searchableText(rfc).includes(token))) return 40;
return undefined;
}
export function searchRfcs(
rfcs: Rfc[],
query: string,
status: StatusFilter = "all",
limit = 75,
): Rfc[] {
return rfcs
.filter((rfc) => status === "all" || rfc.status === status)
.map((rfc) => ({ rfc, score: scoreRfc(rfc, query) }))
.filter(
(result): result is { rfc: Rfc; score: number } =>
result.score !== undefined,
)
.sort(
(left, right) =>
left.score - right.score || right.rfc.number - left.rfc.number,
)
.slice(0, limit)
.map(({ rfc }) => rfc);
}
export function rfcUrl(rfc: Rfc, format: string): string {
return `https://www.rfc-editor.org/rfc/rfc${rfc.number}.${format.toLocaleLowerCase()}`;
}
+354
View File
@@ -0,0 +1,354 @@
import {
Action,
ActionPanel,
Cache,
Color,
Icon,
List,
showToast,
Toast,
} from "@vicinae/api";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
parseRfcIndex,
RFC_INDEX_URL,
type Rfc,
rfcUrl,
type StatusFilter,
searchRfcs,
} from "./rfc-index";
const cache = new Cache({ namespace: "rfc-index", capacity: 30 * 1024 * 1024 });
const CACHE_KEY = "index-v1";
const MAX_CACHE_AGE = 24 * 60 * 60 * 1000;
type CachedIndex = {
updatedAt: number;
rfcs: Rfc[];
};
const STATUS_OPTIONS = [
["all", "All statuses"],
["INTERNET STANDARD", "Internet Standard"],
["PROPOSED STANDARD", "Proposed Standard"],
["DRAFT STANDARD", "Draft Standard"],
["BEST CURRENT PRACTICE", "Best Current Practice"],
["INFORMATIONAL", "Informational"],
["EXPERIMENTAL", "Experimental"],
["HISTORIC", "Historic"],
["UNKNOWN", "Unknown"],
] as const;
function readCachedIndex(): CachedIndex | undefined {
const stored = cache.get(CACHE_KEY);
if (!stored) return undefined;
try {
const parsed = JSON.parse(stored) as CachedIndex;
if (!Array.isArray(parsed.rfcs) || typeof parsed.updatedAt !== "number") {
throw new Error("Invalid cached index");
}
return parsed;
} catch {
cache.remove(CACHE_KEY);
return undefined;
}
}
function labelForStatus(status: string): string {
return STATUS_OPTIONS.find(([value]) => value === status)?.[1] ?? status;
}
function escapeMarkdown(value: string): string {
return value.replace(/([\\`*_{}[\]()#+.!|>-])/g, "\\$1");
}
function relationText(ids: string[]): string {
return ids.join(", ");
}
function RfcDetail({ rfc }: { rfc: Rfc }) {
const obsoleteWarning = rfc.obsoletedBy.length
? `> **Obsoleted by ${relationText(rfc.obsoletedBy)}.** Prefer the newer specification unless you specifically need this RFC.\n\n`
: "";
const markdown = `# ${rfc.docId}\n\n## ${escapeMarkdown(rfc.title)}\n\n${obsoleteWarning}${escapeMarkdown(rfc.abstract || "No abstract is available for this RFC.")}`;
return (
<List.Item.Detail
markdown={markdown}
metadata={
<List.Item.Detail.Metadata>
<List.Item.Detail.Metadata.Label
title="Status"
text={
rfc.obsoletedBy.length
? { value: "Obsoleted", color: Color.Red }
: labelForStatus(rfc.status)
}
/>
<List.Item.Detail.Metadata.Label
title="Published"
text={[rfc.month, rfc.year].filter(Boolean).join(" ") || "Unknown"}
/>
<List.Item.Detail.Metadata.Label
title="Authors"
text={rfc.authors.join(", ") || "Unknown"}
/>
<List.Item.Detail.Metadata.Label
title="Stream"
text={rfc.stream ?? "Unknown"}
/>
{rfc.pageCount ? (
<List.Item.Detail.Metadata.Label
title="Pages"
text={String(rfc.pageCount)}
/>
) : null}
{rfc.workingGroup ? (
<List.Item.Detail.Metadata.Label
title="Working group"
text={rfc.workingGroup}
/>
) : null}
{rfc.isAlso.length ? (
<List.Item.Detail.Metadata.Label
title="Also"
text={relationText(rfc.isAlso)}
/>
) : null}
{rfc.obsoletes.length ? (
<List.Item.Detail.Metadata.Label
title="Obsoletes"
text={relationText(rfc.obsoletes)}
/>
) : null}
{rfc.obsoletedBy.length ? (
<List.Item.Detail.Metadata.Label
title="Obsoleted by"
text={{ value: relationText(rfc.obsoletedBy), color: Color.Red }}
/>
) : null}
{rfc.updates.length ? (
<List.Item.Detail.Metadata.Label
title="Updates"
text={relationText(rfc.updates)}
/>
) : null}
{rfc.updatedBy.length ? (
<List.Item.Detail.Metadata.Label
title="Updated by"
text={relationText(rfc.updatedBy)}
/>
) : null}
{rfc.keywords.length ? (
<List.Item.Detail.Metadata.TagList title="Keywords">
{rfc.keywords.map((keyword) => (
<List.Item.Detail.Metadata.TagList.Item
key={keyword}
text={keyword}
/>
))}
</List.Item.Detail.Metadata.TagList>
) : null}
</List.Item.Detail.Metadata>
}
/>
);
}
function RfcActions({ rfc, refresh }: { rfc: Rfc; refresh: () => void }) {
const htmlUrl = rfcUrl(rfc, "html");
return (
<ActionPanel>
<ActionPanel.Section>
<Action.OpenInBrowser
title="Open RFC"
url={htmlUrl}
icon={Icon.Book}
shortcut="open"
/>
<Action.CopyToClipboard
title="Copy RFC URL"
content={htmlUrl}
shortcut="copy"
/>
<Action.CopyToClipboard
title="Copy Citation"
content={`${rfc.docId}: ${rfc.title}`}
/>
</ActionPanel.Section>
<ActionPanel.Section title="More Formats">
{rfc.formats
.filter((format) => format !== "HTML")
.map((format) => (
<Action.OpenInBrowser
key={format}
title={`Open ${format}`}
url={rfcUrl(rfc, format)}
icon={Icon.BlankDocument}
/>
))}
<Action.OpenInBrowser
title="Open RFC Editor Info"
url={`https://www.rfc-editor.org/info/rfc${rfc.number}`}
icon={Icon.Globe01}
/>
<Action.OpenInBrowser
title="Open IETF Datatracker"
url={`https://datatracker.ietf.org/doc/rfc${rfc.number}/`}
icon={Icon.Globe01}
/>
{rfc.errataUrl ? (
<Action.OpenInBrowser
title="View Errata"
url={rfc.errataUrl}
icon={Icon.Bug}
/>
) : null}
{rfc.doi ? (
<Action.OpenInBrowser
title="Open DOI"
url={`https://doi.org/${rfc.doi}`}
icon={Icon.Link}
/>
) : null}
</ActionPanel.Section>
<ActionPanel.Section>
<Action
title="Refresh RFC Index"
icon={Icon.ArrowClockwise}
shortcut="refresh"
onAction={refresh}
/>
</ActionPanel.Section>
</ActionPanel>
);
}
export default function SearchRfc() {
const cached = useMemo(readCachedIndex, []);
const [rfcs, setRfcs] = useState<Rfc[]>(cached?.rfcs ?? []);
const [query, setQuery] = useState("");
const [status, setStatus] = useState<StatusFilter>("all");
const [isLoading, setIsLoading] = useState(!cached);
const [error, setError] = useState<string>();
const loadIndex = useCallback(async (force = false, signal?: AbortSignal) => {
const existing = readCachedIndex();
if (!force && existing && Date.now() - existing.updatedAt < MAX_CACHE_AGE) {
setRfcs(existing.rfcs);
setIsLoading(false);
return;
}
setIsLoading(true);
setError(undefined);
try {
const response = await fetch(RFC_INDEX_URL, { signal });
if (!response.ok)
throw new Error(`RFC Editor returned HTTP ${response.status}`);
const nextRfcs = parseRfcIndex(await response.text());
const nextCache = { updatedAt: Date.now(), rfcs: nextRfcs };
cache.set(CACHE_KEY, JSON.stringify(nextCache));
setRfcs(nextRfcs);
} catch (cause) {
if (cause instanceof DOMException && cause.name === "AbortError") return;
const message = cause instanceof Error ? cause.message : "Unknown error";
if (existing) {
setRfcs(existing.rfcs);
await showToast(
Toast.Style.Failure,
"Couldn't refresh the RFC index",
"Showing cached data",
);
} else {
setError(message);
}
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
const controller = new AbortController();
void loadIndex(false, controller.signal);
return () => controller.abort();
}, [loadIndex]);
const results = useMemo(
() => searchRfcs(rfcs, query, status),
[rfcs, query, status],
);
const refresh = () => void loadIndex(true);
return (
<List
isLoading={isLoading}
isShowingDetail={results.length > 0}
filtering={false}
throttle
searchText={query}
onSearchTextChange={setQuery}
searchBarPlaceholder="Search by RFC number, title, author, keyword, or abstract…"
searchBarAccessory={
<List.Dropdown
tooltip="Filter by status"
value={status}
onChange={(value) => setStatus(value as StatusFilter)}
>
{STATUS_OPTIONS.map(([value, label]) => (
<List.Dropdown.Item key={value} title={label} value={value} />
))}
</List.Dropdown>
}
>
{error ? (
<List.EmptyView
icon={Icon.CloudLightning}
title="Couldn't load the RFC index"
description={`${error}. Check your connection and try again.`}
actions={
<ActionPanel>
<Action
title="Retry"
icon={Icon.ArrowClockwise}
onAction={refresh}
/>
</ActionPanel>
}
/>
) : !isLoading && results.length === 0 ? (
<List.EmptyView
icon={Icon.MagnifyingGlass}
title={
query ? `No RFCs found for “${query}”` : "No RFCs match this status"
}
description="Try a broader term, a different status, or an RFC number such as 9110."
/>
) : (
<List.Section
title={query ? `${results.length} results` : "Latest RFCs"}
>
{results.map((rfc) => (
<List.Item
key={rfc.docId}
id={rfc.docId}
icon={rfc.obsoletedBy.length ? Icon.Warning : Icon.Book}
title={rfc.docId}
subtitle={rfc.title}
accessories={[
{
tag: rfc.obsoletedBy.length
? { value: "Obsoleted", color: Color.Red }
: labelForStatus(rfc.status),
},
{ text: rfc.year ? String(rfc.year) : undefined },
]}
detail={<RfcDetail rfc={rfc} />}
actions={<RfcActions rfc={rfc} refresh={refresh} />}
/>
))}
</List.Section>
)}
</List>
);
}
+18
View File
@@ -0,0 +1,18 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Node 16",
"include": ["src/**/*"],
"compilerOptions": {
//"lib": ["es2020"],
"module": "commonjs",
"target": "es2020",
"strict": true,
"isolatedModules": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"jsx": "react-jsx",
"types": ["node"]
}
}