diff --git a/bun.lock b/bun.lock index fb0d16d..12bc3ba 100644 --- a/bun.lock +++ b/bun.lock @@ -417,7 +417,7 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - "@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], + "@types/node": ["@types/node@26.1.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw=="], "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.7", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ=="], @@ -565,7 +565,7 @@ "seroval-plugins": ["seroval-plugins@1.5.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw=="], - "solid-js": ["solid-js@1.9.13", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ=="], + "solid-js": ["solid-js@1.9.14", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.4", "seroval-plugins": "~1.5.4" } }, "sha512-sAEXC0Kk0S1EDg+8ysEWJDbYhA3RRoEjwuySUGlKIemeo0I5YZfOyumNjNs9Sv3y2nmhD+0rW66ag2HsMuQiGQ=="], "solid-refresh": ["solid-refresh@0.6.3", "", { "dependencies": { "@babel/generator": "^7.23.6", "@babel/helper-module-imports": "^7.22.15", "@babel/types": "^7.23.6" }, "peerDependencies": { "solid-js": "^1.3" } }, "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA=="], diff --git a/package.json b/package.json index 59f185a..4eff162 100644 --- a/package.json +++ b/package.json @@ -48,14 +48,14 @@ "@takumi-rs/wasm": "^1.8.7", "@tanstack/solid-virtual": "^3.13.31", "codemirror": "^6.0.2", - "solid-js": "^1.9.13", + "solid-js": "^1.9.14", "valibot": "^1.4.2" }, "devDependencies": { "@iconify-json/lucide": "^1.2.115", "@iconify/tailwind4": "^1.2.3", "@tailwindcss/vite": "^4.3.2", - "@types/node": "^26.0.1", + "@types/node": "^26.1.0", "esbuild": "^0.28.1", "oxfmt": "^0.57.0", "tailwindcss": "^4.3.2", diff --git a/src/components/hover-card/lexicon.tsx b/src/components/hover-card/lexicon.tsx index e838c38..9085a5e 100644 --- a/src/components/hover-card/lexicon.tsx +++ b/src/components/hover-card/lexicon.tsx @@ -3,7 +3,7 @@ import type { AtprotoDid } from "@atcute/lexicons/syntax"; import { A } from "@solidjs/router"; import { Show } from "solid-js"; -import { resolveLexicon } from "../../lib/schema-tab"; +import { parseLexiconRef, resolveLexicon, schemaHash } from "../../lib/lexicon"; import { LexiconSchemaView, type LexiconSchema } from "../lexicon-schema"; import HoverCard, { HoverCardError, type HoverTriggerRenderer } from "./base"; import { createHoverResource } from "./resource"; @@ -21,15 +21,6 @@ interface LexiconState { schema: LexiconSchema; } -const lexiconCache = new Map>(); - -const parseLexiconRef = (lexicon: string) => { - const [nsid, defName] = lexicon.split("#"); - return { nsid, defName }; -}; - -const schemaHash = (defName?: string) => (defName ? `#schema:${defName}` : "#schema"); - const fetchLexiconPreview = async (nsid: string): Promise => { const { authority, schema } = await resolveLexicon(nsid as Nsid); return { authority, schema: schema.rawSchema as LexiconSchema }; @@ -39,7 +30,6 @@ const LexiconHoverCard = (props: LexiconHoverCardProps) => { const parsed = () => parseLexiconRef(props.lexicon); const href = () => `/lexicon/${parsed().nsid}${schemaHash(parsed().defName)}`; const preview = createHoverResource(() => parsed().nsid, fetchLexiconPreview, { - cache: lexiconCache, getErrorMessage: (err) => (err instanceof Error ? err.message : "Failed to resolve schema"), }); const loading = preview.visibleLoading; @@ -74,6 +64,7 @@ const LexiconHoverCard = (props: LexiconHoverCardProps) => { schema={data().schema} authority={data().authority} preview + inertRefs focusDef={parsed().defName} /> )} diff --git a/src/components/lexicon-schema.tsx b/src/components/lexicon-schema.tsx index bab539c..706fc4c 100644 --- a/src/components/lexicon-schema.tsx +++ b/src/components/lexicon-schema.tsx @@ -1,7 +1,11 @@ -import { AtprotoDid } from "@atcute/lexicons/syntax"; +import type { Nsid } from "@atcute/lexicons"; +import type { AtprotoDid } from "@atcute/lexicons/syntax"; import { A, useLocation } from "@solidjs/router"; -import { createEffect, For, type JSX, Show } from "solid-js"; +import { createContext, createEffect, For, type JSX, Show, useContext } from "solid-js"; +import { parseLexiconRef, resolveLexicon, schemaHash, schemaHref } from "../lib/lexicon"; +import HoverCard, { HoverCardError } from "./hover-card/base"; +import { createHoverResource } from "./hover-card/resource"; import Tooltip from "./tooltip.jsx"; // Style constants @@ -44,17 +48,13 @@ const hasConstraints = (property: LexiconProperty | LexiconDef) => property.knownValues || property.closed; -const schemaDefId = (defName: string) => `schema:${defName}`; -const schemaHash = (defName?: string) => (defName ? `#${schemaDefId(defName)}` : "#schema"); -const schemaHref = (nsid: string, defName?: string) => `/lexicon/${nsid}${schemaHash(defName)}`; -const refHref = (refType: string) => { - if (refType.startsWith("#")) return schemaHash(refType.slice(1)); +const LexiconSchemaContext = createContext<{ inertRefs?: boolean }>(); +const useLexiconSchemaContext = () => useContext(LexiconSchemaContext) ?? {}; - const [nsid, defName] = refType.split("#"); - return schemaHref(nsid, defName); -}; -const SCHEMA_LINK_CLASS = - "inline-block cursor-pointer truncate font-mono text-xs text-blue-500 hover:underline dark:text-blue-400"; +const schemaDefId = (defName: string) => `schema:${defName}`; +const SCHEMA_REF_TEXT_CLASS = + "inline-block truncate font-mono text-xs text-blue-500 dark:text-blue-400"; +const SCHEMA_LINK_CLASS = `${SCHEMA_REF_TEXT_CLASS} cursor-pointer hover:underline`; const keepLocalHashNavigationNative = (event: MouseEvent) => event.stopPropagation(); @@ -65,6 +65,12 @@ const LocalSchemaLink = (props: { defName: string; class: string; children: JSX. ); const SchemaRefLink = (props: { refType: string; children: JSX.Element }) => { + const context = useLexiconSchemaContext(); + + if (context.inertRefs) { + return {props.children}; + } + if (props.refType.startsWith("#")) { return ( @@ -73,11 +79,7 @@ const SchemaRefLink = (props: { refType: string; children: JSX.Element }) => { ); } - return ( - - {props.children} - - ); + return {props.children}; }; export interface LexiconSchema { @@ -89,6 +91,59 @@ export interface LexiconSchema { }; } +interface LexiconPreviewState { + authority: AtprotoDid; + schema: LexiconSchema; +} + +const fetchLexiconPreview = async (nsid: string): Promise => { + const { authority, schema } = await resolveLexicon(nsid as Nsid); + return { authority, schema: schema.rawSchema as LexiconSchema }; +}; + +function SchemaRefHoverCard(props: { refType: string; children: JSX.Element }) { + const parsed = () => parseLexiconRef(props.refType); + const preview = createHoverResource(() => parsed().nsid, fetchLexiconPreview, { + getErrorMessage: (err) => (err instanceof Error ? err.message : "Failed to resolve schema"), + }); + const loading = preview.visibleLoading; + const hasPreview = () => Boolean(preview.state().data || preview.state().error); + const trigger = () => ( + + {props.children} + + ); + + return ( + + + + + + {(data) => ( + + )} + + + ); +} + interface LexiconPermission { type: "permission"; // NOTE: blob, account, and identity are not supported in lexicon schema context @@ -332,14 +387,7 @@ const PropertyRow = (props: { }; const NsidLink = (props: { nsid: string }) => { - return ( - - {props.nsid} - - ); + return {props.nsid}; }; const resourceColor = (resource: string) => @@ -693,6 +741,7 @@ export const LexiconSchemaView = (props: { schema: LexiconSchema; authority?: AtprotoDid; preview?: boolean; + inertRefs?: boolean; focusDef?: string; }) => { const location = useLocation(); @@ -733,52 +782,54 @@ export const LexiconSchemaView = (props: { }); return ( -
- {/* Header */} -
-
-

- {props.schema.id} -

- - v{props.schema.lexicon} - - - - - - - + +
+ {/* Header */} +
+
+

+ {props.schema.id} +

+ + v{props.schema.lexicon} + + + + + + + + +
+ +

+ {props.schema.description} +

- -

- {props.schema.description} -

-
-
- {/* Definitions */} -
- - {([name, def]) => } - - 0}> -
- +{hiddenDefinitionCount()} more definitions -
-
+ {/* Definitions */} +
+ + {([name, def]) => } + + 0}> +
+ +{hiddenDefinitionCount()} more definitions +
+
+
-
+ ); }; diff --git a/src/lib/lexicon.ts b/src/lib/lexicon.ts new file mode 100644 index 0000000..6759e19 --- /dev/null +++ b/src/lib/lexicon.ts @@ -0,0 +1,37 @@ +import { ResolvedSchema } from "@atcute/lexicon-resolver"; +import { Nsid } from "@atcute/lexicons"; +import { AtprotoDid } from "@atcute/lexicons/syntax"; + +import { resolveLexiconAuthority, resolveLexiconSchema } from "./api.js"; + +interface CachedLexicon { + authority: AtprotoDid; + schema: ResolvedSchema; +} + +const cache = new Map>(); + +export const parseLexiconRef = (lexicon: string) => { + const [nsid, defName] = lexicon.split("#"); + return { nsid, defName }; +}; + +export const schemaHash = (defName?: string) => (defName ? `#schema:${defName}` : "#schema"); + +export const schemaHref = (nsid: string, defName?: string) => + `/lexicon/${nsid}${schemaHash(defName)}`; + +export const resolveLexicon = (nsid: Nsid): Promise => { + let cached = cache.get(nsid); + if (cached) return cached; + + const promise = (async () => { + const authority = await resolveLexiconAuthority(nsid); + const schema = await resolveLexiconSchema(authority, nsid); + return { authority, schema }; + })(); + + cache.set(nsid, promise); + promise.catch(() => cache.delete(nsid)); + return promise; +}; diff --git a/src/lib/schema-tab.tsx b/src/lib/schema-tab.tsx index 2fbaefb..8a1be22 100644 --- a/src/lib/schema-tab.tsx +++ b/src/lib/schema-tab.tsx @@ -5,29 +5,7 @@ import { useLocation } from "@solidjs/router"; import { createEffect, createSignal, ErrorBoundary, Show } from "solid-js"; import { LexiconSchemaView } from "../components/lexicon-schema.jsx"; -import { resolveLexiconAuthority, resolveLexiconSchema } from "./api.js"; - -interface CachedLexicon { - authority: AtprotoDid; - schema: ResolvedSchema; -} - -const cache = new Map>(); - -export const resolveLexicon = (nsid: Nsid): Promise => { - let cached = cache.get(nsid); - if (cached) return cached; - - const promise = (async () => { - const authority = await resolveLexiconAuthority(nsid); - const schema = await resolveLexiconSchema(authority, nsid); - return { authority, schema }; - })(); - - cache.set(nsid, promise); - promise.catch(() => cache.delete(nsid)); - return promise; -}; +import { resolveLexicon } from "./lexicon.js"; export const useLexiconSchema = (collection: () => string | undefined) => { const location = useLocation();