mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-26 12:18:01 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fac5fcfbc2 | |||
| 52f2cace6f | |||
| 6a939b5685 | |||
| 0a89373f61 | |||
| 7599253c4b |
@@ -0,0 +1,36 @@
|
||||
# Content references
|
||||
|
||||
Content references are a concept in the GitBook API for linking between spaces, pages, anchors, files, and other entities. A definition of a content reference can be found in the OpenAPI spec under `api.ContentRef`.
|
||||
|
||||
Every link in GitBook - even to external URLs - is defined as a content reference.
|
||||
|
||||
Content references can be, by design, relative. We call "resolving a content ref" the process of taking a potentially relative ref along with the current context of the user and producing an absolute link:
|
||||
|
||||
```typescript
|
||||
async function resolveContentRef(contentRef: api.ContentRef, context: GitBookSpaceContext): Promise<ResolvedContentRef>;
|
||||
```
|
||||
|
||||
Relative content references are resolved against the context:
|
||||
|
||||
```typescript
|
||||
const ref = { kind: 'page', page: 'my-page-id' }; // a relative ref to a page in the current space
|
||||
const resolved = await resolveContentRef(ref, { space: 'my-space-id' });
|
||||
// resolved.url = ../../my-page-id
|
||||
|
||||
const resolved = await resolveContentRef(ref, { space: 'another-space-id' });
|
||||
// resolved == null, because the page does not exist here
|
||||
```
|
||||
|
||||
## Reusable Content
|
||||
|
||||
Reusable content presents an interesting challenge for content references:
|
||||
|
||||
- Reusable content belongs to a parent space.
|
||||
- All content refs defined in the reusable content will be relative to the parent space. For example, for a content ref inside `my-space`, `{ kind: page, page: 'my-page-id' }` refers to the page `my-page-id` inside `my-space`.
|
||||
- When reusable content is used in pages within the parent space, all links remain relative to the space.
|
||||
- When reusable content is used across other spaces, we must resolve the refs relative to the containing space (not the parent).
|
||||
|
||||
See the [ReusableContent](../components/DocumentView/ReusableContent.tsx) component for how we construct the `GitBookSpaceContext`, and [resolveContentRef](./references.tsx) for the `resolveContentRef` implementation.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { describe, expect, it, mock } from 'bun:test';
|
||||
import * as api from '@gitbook/api';
|
||||
import type { GitBookSpaceContext } from '@v2/lib/context';
|
||||
|
||||
import { resolveContentRef } from './references';
|
||||
|
||||
const NOW = new Date();
|
||||
|
||||
describe('resolveContentRef', () => {
|
||||
it('should resolve a relative page ref', async () => {
|
||||
const currentSpace = createMockSpace({ id: 'current-space' });
|
||||
const currentPages = [createMockPage({ id: 'page-1' })];
|
||||
const context = createMockContext({ space: currentSpace, pages: currentPages });
|
||||
|
||||
const result = await resolveContentRef(
|
||||
{
|
||||
kind: 'page',
|
||||
page: 'page-1',
|
||||
},
|
||||
context
|
||||
);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.text).toBe('Page 1');
|
||||
expect(result?.href).toBe('/page/page-1');
|
||||
});
|
||||
|
||||
it('should resolve a url', async () => {
|
||||
const currentSpace = createMockSpace({ id: 'current-space' });
|
||||
const currentPages = [createMockPage({ id: 'page-1' })];
|
||||
const context = createMockContext({ space: currentSpace, pages: currentPages });
|
||||
|
||||
const result = await resolveContentRef(
|
||||
{
|
||||
kind: 'url',
|
||||
url: 'https://example.com/some-page',
|
||||
},
|
||||
context
|
||||
);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.text).toBe('https://example.com/some-page');
|
||||
expect(result?.href).toBe('https://example.com/some-page');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveContentRef with reusable content', () => {
|
||||
it('should resolve a relative page ref with reusable content', async () => {
|
||||
const rcSpace = createMockSpace({ id: 'rc-parent-space' });
|
||||
const context = createMockContext({ space: rcSpace, pages: [] });
|
||||
|
||||
const result = await resolveContentRef(
|
||||
{
|
||||
kind: 'page',
|
||||
page: 'page-1',
|
||||
},
|
||||
context
|
||||
);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.text).toBe('Current Page');
|
||||
expect(result?.href).toBe('/page/page-1');
|
||||
});
|
||||
});
|
||||
|
||||
const createMockSpace = (space: MandateProps<Partial<api.Space>, 'id'>): api.Space => ({
|
||||
object: 'space',
|
||||
title: 'My Space',
|
||||
visibility: api.ContentVisibility.Public,
|
||||
urls: {
|
||||
location: `https://api.gitbook.com/s/${space.id}`,
|
||||
published: `https://example.com/space/${space.id}`,
|
||||
app: `https://app.gitbook.com/s/${space.id}`,
|
||||
},
|
||||
organization: 'org-1',
|
||||
revision: 'rev-1',
|
||||
emoji: '',
|
||||
createdAt: NOW.toISOString(),
|
||||
updatedAt: NOW.toISOString(),
|
||||
defaultLevel: 'inherit',
|
||||
comments: 0,
|
||||
changeRequests: 0,
|
||||
changeRequestsOpen: 0,
|
||||
changeRequestsDraft: 0,
|
||||
permissions: {
|
||||
view: true,
|
||||
access: true,
|
||||
admin: true,
|
||||
viewInviteLinks: true,
|
||||
edit: true,
|
||||
triggerGitSync: true,
|
||||
comment: true,
|
||||
merge: true,
|
||||
review: true,
|
||||
},
|
||||
...space,
|
||||
});
|
||||
|
||||
const createMockPage = (
|
||||
page: MandateProps<Partial<api.RevisionPageDocument>, 'id'>
|
||||
): api.RevisionPageDocument => ({
|
||||
title: 'Page 1',
|
||||
slug: 'page-1',
|
||||
kind: 'sheet',
|
||||
type: 'document',
|
||||
layout: {},
|
||||
urls: {
|
||||
app: `https://app.gitbook.com/s/${page.id}/page-1`,
|
||||
},
|
||||
path: '/page-1',
|
||||
pages: [],
|
||||
document: {
|
||||
object: 'document',
|
||||
data: {},
|
||||
nodes: [],
|
||||
},
|
||||
...page,
|
||||
});
|
||||
|
||||
const createMockContext = (
|
||||
context: MandateProps<Partial<GitBookSpaceContext>, 'space' | 'pages'>
|
||||
): GitBookSpaceContext => ({
|
||||
organizationId: 'org-1',
|
||||
changeRequest: null,
|
||||
revisionId: 'rev-1',
|
||||
shareKey: undefined,
|
||||
dataFetcher: {
|
||||
getSpace: mock().mockResolvedValue(null),
|
||||
getPublishedContentSite: mock().mockResolvedValue(null),
|
||||
} as any,
|
||||
linker: {
|
||||
toPathForPage: mock(({ page }: { page: api.RevisionPage }) => `/page/${page.id}`),
|
||||
toAbsoluteURL: mock((url: string) => `https://example.com${url}`),
|
||||
} as any,
|
||||
...context,
|
||||
});
|
||||
|
||||
/**
|
||||
* Type to make optional properties on a object mandatory.
|
||||
*
|
||||
* interface SomeObject {
|
||||
* uid: string;
|
||||
* price: number | null;
|
||||
* location?: string;
|
||||
* }
|
||||
*
|
||||
* type ValuableObject = MandateProps<SomeObject, 'price' | 'location'>;
|
||||
*/
|
||||
type MandateProps<T extends {}, K extends keyof T> = T & {
|
||||
[MK in K]-?: NonNullable<T[MK]>;
|
||||
};
|
||||
Reference in New Issue
Block a user