From 5627e76fe02b8a30fe2268c09dacbbe4ff2a5c21 Mon Sep 17 00:00:00 2001 From: Viktor Renkema Date: Mon, 2 Nov 2026 11:30:56 +0100 Subject: [PATCH] fix: ignore category references when resolving assigned tags --- .../gitbook/src/lib/document-sections.test.ts | 7 ++- packages/gitbook/src/lib/references.test.ts | 24 ++++++++++ packages/gitbook/src/lib/tags.ts | 6 ++- packages/gitbook/src/lib/updates.test.ts | 46 +++++++++++++++++-- 4 files changed, 77 insertions(+), 6 deletions(-) diff --git a/packages/gitbook/src/lib/document-sections.test.ts b/packages/gitbook/src/lib/document-sections.test.ts index fd47fbfca..d03d6c957 100644 --- a/packages/gitbook/src/lib/document-sections.test.ts +++ b/packages/gitbook/src/lib/document-sections.test.ts @@ -145,7 +145,11 @@ describe('getDocumentSections', () => { type: 'update', data: { date: '2026-04-08', - tags: [{ kind: 'tag', tag: 'improvements' }], + tags: [ + { kind: 'tag', tag: 'improvements' }, + { kind: 'tag', tag: 'category' }, + { kind: 'tag', tag: 'missing' }, + ], }, isVoid: false, nodes: [ @@ -197,6 +201,7 @@ describe('getDocumentSections', () => { ...context, revision: { tags: [ + { slug: 'category', label: 'Products', color: 'default', kind: 'category' }, { slug: 'improvements', label: 'Improvements', diff --git a/packages/gitbook/src/lib/references.test.ts b/packages/gitbook/src/lib/references.test.ts index d8bce5bc4..2409b64af 100644 --- a/packages/gitbook/src/lib/references.test.ts +++ b/packages/gitbook/src/lib/references.test.ts @@ -143,6 +143,30 @@ describe('resolveStringContentRef', () => { }); describe('resolveContentRef', () => { + it('does not resolve category or missing assignments as tag links', async () => { + const normal = { slug: 'constructor', label: 'Constructor', color: 'default' }; + const category = { + slug: 'products', + label: 'Products', + color: 'default', + kind: 'category', + }; + const context = { + space: { id: 'local' }, + revision: { tags: [normal, category] }, + linker: createLinker({ + host: 'docs.example.com', + spaceBasePath: '/', + siteBasePath: '/', + }), + } as unknown as GitBookAnyContext; + expect(await resolveContentRef({ kind: 'tag', tag: 'products' }, context)).toBeNull(); + expect(await resolveContentRef({ kind: 'tag', tag: 'missing' }, context)).toBeNull(); + expect( + await resolveContentRef({ kind: 'tag', tag: 'constructor', space: 'local' }, context) + ).toMatchObject({ text: 'Constructor' }); + }); + it("resolves a page in a different space using that space's page data, not the current context page", async () => { const sharedPageId = 'page-shared'; diff --git a/packages/gitbook/src/lib/tags.ts b/packages/gitbook/src/lib/tags.ts index 51611b1aa..69046ea7e 100644 --- a/packages/gitbook/src/lib/tags.ts +++ b/packages/gitbook/src/lib/tags.ts @@ -8,10 +8,12 @@ export function getRevisionTags(revision: Revision | undefined): RevisionTag[] { } /** - * Resolve a tag slug to its full RevisionTag. + * Resolve an assignment to a normal tag, ignoring categories and missing definitions. */ export function resolveTag(slug: string, revisionTags: RevisionTag[]): RevisionTag | undefined { - return revisionTags.find((t) => t.slug === slug); + return revisionTags.find( + (tag) => tag.slug === slug && !('kind' in tag && tag.kind === 'category') + ); } /** diff --git a/packages/gitbook/src/lib/updates.test.ts b/packages/gitbook/src/lib/updates.test.ts index a132e60e6..f5ccf3d18 100644 --- a/packages/gitbook/src/lib/updates.test.ts +++ b/packages/gitbook/src/lib/updates.test.ts @@ -1,7 +1,13 @@ import { describe, expect, it } from 'bun:test'; -import type { JSONDocument, Revision } from '@gitbook/api'; +import { + DocumentTextColor, + type JSONDocument, + type Revision, + type RevisionTag, +} from '@gitbook/api'; +import { getRevisionTags, resolveBlockTags, resolveTag } from './tags'; import { UPDATES_TAG_FILTER_CAP, getDocumentFilterableTags, @@ -9,6 +15,40 @@ import { } from './updates'; describe('getDocumentFilterableTags', () => { + it('ignores category and deleted references without removing their stored assignments', () => { + const document = createUpdatesDocument([['category', 'constructor', 'missing']]); + const category = { ...createTag('category', 'Products'), kind: 'category' }; + const normal = createTag('constructor', 'Constructor'); + const revision = { tags: [category, normal] } as unknown as Revision; + const before = JSON.stringify(document); + expect(getDocumentFilterableTags(document, revision).map((tag) => tag.slug)).toEqual([ + 'constructor', + ]); + expect(resolveTag('category', revision.tags)).toBeUndefined(); + expect(resolveTag('missing', revision.tags)).toBeUndefined(); + expect(resolveTag('constructor', revision.tags)).toEqual(normal); + expect(getRevisionTags(revision)).toEqual([category, normal]); + expect(JSON.stringify(document)).toBe(before); + }); + + it('rejects category assignments even when definitions are passed directly to the resolver', () => { + const normal = createTag('beta', 'Beta'); + const tags = [ + { ...createTag('products', 'Products'), kind: 'category' }, + normal, + ] satisfies (RevisionTag & { kind?: 'category' })[]; + expect( + resolveBlockTags( + [ + { kind: 'tag', tag: 'products' }, + { kind: 'tag', tag: 'beta' }, + { kind: 'tag', tag: 'missing' }, + ], + tags + ) + ).toEqual([normal]); + }); + it('returns unique update tags in document order', () => { const document = createUpdatesDocument([ ['improvements', 'fixes'], @@ -77,11 +117,11 @@ function createUpdatesDocument(entryTags: string[][]): JSONDocument { }; } -function createTag(slug: string, label: string) { +function createTag(slug: string, label: string): RevisionTag { return { slug, label, - color: 'default', + color: DocumentTextColor.Default, icon: undefined, }; }