diff --git a/apps/desktop/src/renderer/src/components/home/GlyphCatalogOverlay.ts b/apps/desktop/src/renderer/src/components/home/GlyphCatalogOverlay.ts index 734aefbc..052da045 100644 --- a/apps/desktop/src/renderer/src/components/home/GlyphCatalogOverlay.ts +++ b/apps/desktop/src/renderer/src/components/home/GlyphCatalogOverlay.ts @@ -73,7 +73,11 @@ export class GlyphCatalogOverlay { context.textAlign = "center"; context.textBaseline = "middle"; context.fillText( - fitCanvasText(context, cell.glyph.name, cell.nameRect.width - 2 * LABEL_HORIZONTAL_INSET), + fitCanvasText( + context, + cell.glyph.displayName, + cell.nameRect.width - 2 * LABEL_HORIZONTAL_INSET, + ), cell.nameRect.x + cell.nameRect.width / 2, cell.nameRect.y + cell.nameRect.height / 2, ); diff --git a/apps/desktop/src/renderer/src/components/home/glyphCatalogLayout.test.ts b/apps/desktop/src/renderer/src/components/home/glyphCatalogLayout.test.ts index 50c4b77e..d2fdddba 100644 --- a/apps/desktop/src/renderer/src/components/home/glyphCatalogLayout.test.ts +++ b/apps/desktop/src/renderer/src/components/home/glyphCatalogLayout.test.ts @@ -7,6 +7,7 @@ function catalog(count: number): GlyphCatalogItem[] { return Array.from({ length: count }, (_, index) => ({ id: `glyph-${index}` as GlyphId, name: `name-${index}` as GlyphName, + displayName: `name-${index}`, unicode: index, })); } diff --git a/apps/desktop/src/renderer/src/context/GlyphCatalogProvider.tsx b/apps/desktop/src/renderer/src/context/GlyphCatalogProvider.tsx index 669f6e89..91d10265 100644 --- a/apps/desktop/src/renderer/src/context/GlyphCatalogProvider.tsx +++ b/apps/desktop/src/renderer/src/context/GlyphCatalogProvider.tsx @@ -1,5 +1,5 @@ import { useMemo, useState, type ReactNode } from "react"; -import type { GlyphCategory, GlyphCategoryCatalog } from "@shift/glyph-info"; +import type { GlyphCategory, GlyphCategoryCatalog, GlyphInfo } from "@shift/glyph-info"; import type { GlyphName, GlyphRecord } from "@shift/types"; import { useSignalState } from "@/lib/signals"; import { useEditor } from "@/workspace/WorkspaceContext"; @@ -24,8 +24,8 @@ const useGlyphCatalogState = (): GlyphCatalogState => { const [selectedSubCategoryKey, setSelectedSubCategoryKey] = useState(null); const availableGlyphs = useMemo( - () => glyphRecords.map(glyphCatalogItemFromRecord), - [glyphRecords], + () => glyphRecords.map((record) => glyphCatalogItemFromRecord(record, glyphInfo)), + [glyphInfo, glyphRecords], ); const availableUnicodes = useMemo( @@ -54,7 +54,9 @@ const useGlyphCatalogState = (): GlyphCatalogState => { return availableGlyphs.filter((glyph) => { const unicodeMatched = glyph.unicode !== null && categoryFilteredUnicodes.has(glyph.unicode); const nameMatched = - normalizedQuery !== "" && glyph.name.toLowerCase().includes(normalizedQuery); + normalizedQuery !== "" && + (glyph.name.toLowerCase().includes(normalizedQuery) || + glyph.displayName.toLowerCase().includes(normalizedQuery)); if (filteringByCategory) return unicodeMatched; if (normalizedQuery !== "") return unicodeMatched || nameMatched; @@ -100,10 +102,13 @@ const useGlyphCatalogState = (): GlyphCatalogState => { }; }; -function glyphCatalogItemFromRecord(record: GlyphRecord): GlyphCatalogItem { +function glyphCatalogItemFromRecord(record: GlyphRecord, glyphInfo: GlyphInfo): GlyphCatalogItem { + const unicode = record.unicodes[0] ?? null; + return { id: record.id, name: record.name, - unicode: record.unicodes[0] ?? null, + displayName: glyphInfo.resolveGlyphName(record.name, unicode), + unicode, }; } diff --git a/apps/desktop/src/renderer/src/types/glyphCatalog.ts b/apps/desktop/src/renderer/src/types/glyphCatalog.ts index 4581f8fa..89dbb027 100644 --- a/apps/desktop/src/renderer/src/types/glyphCatalog.ts +++ b/apps/desktop/src/renderer/src/types/glyphCatalog.ts @@ -10,6 +10,7 @@ export type GlyphCatalogCellArea = "preview" | "name"; export interface GlyphCatalogItem { readonly id: GlyphId; readonly name: GlyphName; + readonly displayName: string; readonly unicode: number | null; } diff --git a/package.json b/package.json index fe856ee6..e45b38b9 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test:unit": "pnpm turbo run test --filter='!shift-bridge'", "test:integration": "pnpm test:native && cargo test --workspace", "test:lint": "pnpm --filter @shift/desktop run lint:check", - "test:perf": "pnpm --filter @shift/desktop exec vitest bench --run", + "test:perf": "pnpm generate:glyph-info && pnpm --filter @shift/desktop exec vitest bench --run", "test:playwright": "pnpm --filter @shift/desktop test:e2e", "test:watch": "turbo run test:watch", "test:native": "turbo run test --filter=shift-bridge", diff --git a/packages/glyph-info/src/GlyphInfo.test.ts b/packages/glyph-info/src/GlyphInfo.test.ts index d63c2a5c..6d1c55b6 100644 --- a/packages/glyph-info/src/GlyphInfo.test.ts +++ b/packages/glyph-info/src/GlyphInfo.test.ts @@ -103,6 +103,25 @@ describe("getGlyphName", () => { }); }); +describe("resolveGlyphName", () => { + it("maps legacy aliases from the glyph database", () => { + expect(db.resolveGlyphName("afii10072")).toBe("zhe-cy"); + expect(db.resolveGlyphName("afii10081")).toBe("pe-cy"); + }); + + it("uses the assigned codepoint when the authored name is noncanonical", () => { + expect(db.resolveGlyphName("custom-zhe", 0x0436)).toBe("zhe-cy"); + }); + + it("maps production names and preserves dot suffixes", () => { + expect(db.resolveGlyphName("uni01C9.sc")).toBe("lj.sc"); + }); + + it("preserves unknown authored names", () => { + expect(db.resolveGlyphName("custom.unencoded")).toBe("custom.unencoded"); + }); +}); + describe("getAllGlyph", () => { it("returns many entries", () => { const all = db.getAllGlyph(); diff --git a/packages/glyph-info/src/GlyphInfo.ts b/packages/glyph-info/src/GlyphInfo.ts index 849b3e40..28f81432 100644 --- a/packages/glyph-info/src/GlyphInfo.ts +++ b/packages/glyph-info/src/GlyphInfo.ts @@ -127,6 +127,7 @@ function toSortedCategorySummaries( export class GlyphInfo { #glyphData: Map; #glyphDataByName: Map; + #glyphDataByAlias: Map; #decomposed: Map; #usedBy: Map; #charsets: CharsetDefinition[]; @@ -135,6 +136,19 @@ export class GlyphInfo { constructor(resources: GlyphInfoResources) { this.#glyphData = new Map(resources.glyphData.map((g) => [g.codepoint, g])); this.#glyphDataByName = new Map(resources.glyphData.map((g) => [g.name, g])); + this.#glyphDataByAlias = new Map(this.#glyphDataByName); + for (const glyph of resources.glyphData) { + if (glyph.production && !this.#glyphDataByAlias.has(glyph.production)) { + this.#glyphDataByAlias.set(glyph.production, glyph); + } + + for (const alias of glyph.altNames?.split(",") ?? []) { + const name = alias.trim(); + if (name && !this.#glyphDataByAlias.has(name)) { + this.#glyphDataByAlias.set(name, glyph); + } + } + } this.#decomposed = new Map( Object.entries(resources.decomposition.decomposed).map(([k, v]) => [Number(k), v]), @@ -169,11 +183,26 @@ export class GlyphInfo { return data.name; } - /** Look up the full metadata record for a production glyph name. */ + /** Look up the full metadata record for a canonical glyph name. */ getGlyphByName(name: string): Glyph | null { return this.#glyphDataByName.get(name) ?? null; } + /** + * Resolve an authored glyph name to the canonical glyph database name. + * Dot suffixes are preserved, and unknown names pass through unchanged. + */ + resolveGlyphName(name: string, codepoint?: number | null): string { + const suffixStart = name.indexOf("."); + const baseName = suffixStart > 0 ? name.slice(0, suffixStart) : name; + const suffix = suffixStart > 0 ? name.slice(suffixStart) : ""; + const glyph = + (codepoint === null || codepoint === undefined ? null : this.#glyphData.get(codepoint)) ?? + this.#glyphDataByAlias.get(baseName); + + return glyph ? `${glyph.name}${suffix}` : name; + } + getAllGlyph(): Glyph[] { return Array.from(this.#glyphData.values()); }