mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-26 04:07:07 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f720f1dbd6 | |||
| 59bc231ccd | |||
| 3c177f04a2 | |||
| 1d648a9391 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Automatically resolve GitHub and GitLab page links to matching pages in the same published site, including cross-space links imported before their target page was available.
|
||||
@@ -52,8 +52,8 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
||||
const anchorElement = (
|
||||
<InlineLinkAnchor
|
||||
href={resolved.href}
|
||||
contentRef={inline.data.ref}
|
||||
isExternal={inline.data.ref.kind === 'url'}
|
||||
contentRef={resolved.resolvedRef ?? inline.data.ref}
|
||||
isExternal={(resolved.resolvedRef ?? inline.data.ref).kind === 'url'}
|
||||
>
|
||||
{inlinesElement}
|
||||
</InlineLinkAnchor>
|
||||
@@ -121,7 +121,7 @@ function InlineLinkTooltipWrapper(props: {
|
||||
|
||||
let breadcrumbs = resolved.ancestors ?? [];
|
||||
const isMailto = resolved.href.startsWith('mailto:');
|
||||
const isExternal = inline.data.ref.kind === 'url';
|
||||
const isExternal = (resolved.resolvedRef ?? inline.data.ref).kind === 'url';
|
||||
const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined;
|
||||
|
||||
if (isMailto) {
|
||||
|
||||
@@ -519,6 +519,7 @@ export async function fetchSpaceContextByIds(
|
||||
shareKey: string | undefined;
|
||||
changeRequest: string | undefined;
|
||||
revision: string | undefined;
|
||||
revisionMetadata?: boolean;
|
||||
}
|
||||
): Promise<GitBookSpaceContext> {
|
||||
const { dataFetcher } = baseContext;
|
||||
@@ -552,6 +553,7 @@ export async function fetchSpaceContextByIds(
|
||||
dataFetcher.getRevision({
|
||||
spaceId: ids.space,
|
||||
revisionId,
|
||||
...(ids.revisionMetadata ? { metadata: true } : {}),
|
||||
}),
|
||||
|
||||
// When trying to render a revision with an invalid / non-existing ID,
|
||||
|
||||
@@ -80,6 +80,7 @@ export function createDataFetcher(
|
||||
return getRevision(input, {
|
||||
spaceId: params.spaceId,
|
||||
revisionId: params.revisionId,
|
||||
metadata: params.metadata ?? false,
|
||||
});
|
||||
},
|
||||
getRevisionPageByPath(params) {
|
||||
@@ -319,8 +320,15 @@ const getChangeRequest = cache(
|
||||
|
||||
// We don't use remote cache on vercel because of the 2Mb limit on cache size that makes some route crash
|
||||
const getRevision = cache(
|
||||
async (input: DataFetcherInput, params: { spaceId: string; revisionId: string }) => {
|
||||
async (
|
||||
input: DataFetcherInput,
|
||||
params: { spaceId: string; revisionId: string; metadata: boolean }
|
||||
) => {
|
||||
'use cache';
|
||||
if (params.metadata) {
|
||||
// Git paths can change without changing the content revision.
|
||||
cacheTag(getCacheTag({ tag: 'space', space: params.spaceId }));
|
||||
}
|
||||
return wrapDataFetcherError(async () => {
|
||||
return trace(`getRevision(${params.spaceId}, ${params.revisionId})`, async () => {
|
||||
const api = apiClient(input);
|
||||
@@ -328,7 +336,7 @@ const getRevision = cache(
|
||||
params.spaceId,
|
||||
params.revisionId,
|
||||
{
|
||||
metadata: false,
|
||||
metadata: params.metadata,
|
||||
},
|
||||
{
|
||||
...noCacheFetchOptions,
|
||||
|
||||
@@ -79,6 +79,7 @@ export interface GitBookDataFetcher {
|
||||
getRevision(params: {
|
||||
spaceId: string;
|
||||
revisionId: string;
|
||||
metadata?: boolean;
|
||||
}): Promise<DataFetcherResponse<api.Revision>>;
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import { findGitPageURLTarget, findPageByGitPath } from './gitPageURL';
|
||||
|
||||
const SPACES = [
|
||||
{
|
||||
id: 'a',
|
||||
gitSync: {
|
||||
url: 'https://github.com/acme/docs/tree/main',
|
||||
installationProjectDirectory: 'guides',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
gitSync: {
|
||||
url: 'https://github.com/acme/docs/tree/main',
|
||||
installationProjectDirectory: '/api/',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('findGitPageURLTarget', () => {
|
||||
it('matches the repository, ref and directory and preserves anchors', () => {
|
||||
expect(
|
||||
findGitPageURLTarget(
|
||||
'https://github.com/acme/docs/tree/main/api/auth.md#tokens',
|
||||
SPACES
|
||||
)
|
||||
).toEqual({ space: 'b', path: 'api/auth.md', anchor: 'tokens' });
|
||||
});
|
||||
|
||||
it.each([
|
||||
'https://github.com/other/docs/tree/main/api/auth.md',
|
||||
'https://github.com/acme/docs/tree/preview/api/auth.md',
|
||||
'https://github.com.evil.test/acme/docs/tree/main/api/auth.md',
|
||||
'https://github.com/acme/docs/tree/main/api-other/auth.md',
|
||||
'https://github.com/acme/docs/tree/main/api/auth.md?raw=1',
|
||||
'https://github.com/acme/docs/tree/main/api/%ZZ.md',
|
||||
'https://github.com/acme/docs/tree/main/api/%2Fsecret.md',
|
||||
])('does not reinterpret %s', (url) => {
|
||||
expect(findGitPageURLTarget(url, SPACES)).toBeNull();
|
||||
});
|
||||
|
||||
it('supports blob URLs and encoded file names', () => {
|
||||
expect(
|
||||
findGitPageURLTarget(
|
||||
'https://github.com/acme/docs/blob/main/api/hello%20world.md',
|
||||
SPACES
|
||||
)
|
||||
).toEqual({ space: 'b', path: 'api/hello world.md', anchor: undefined });
|
||||
});
|
||||
|
||||
it('matches self-hosted GitLab with nested groups and a slash in the branch', () => {
|
||||
const spaces = [
|
||||
{
|
||||
id: 'b',
|
||||
gitSync: {
|
||||
url: 'https://git.example.com/group/sub/docs/-/tree/release/v2',
|
||||
installationProjectDirectory: 'api',
|
||||
},
|
||||
},
|
||||
];
|
||||
expect(
|
||||
findGitPageURLTarget(
|
||||
'https://git.example.com/group/sub/docs/-/blob/release/v2/api/auth.md',
|
||||
spaces
|
||||
)?.space
|
||||
).toBe('b');
|
||||
});
|
||||
|
||||
it('rejects ambiguous owners and ambiguous branch prefixes', () => {
|
||||
expect(
|
||||
findGitPageURLTarget('https://github.com/acme/docs/tree/main/api/auth.md', [
|
||||
...SPACES,
|
||||
{ ...SPACES[1]!, id: 'duplicate' },
|
||||
])
|
||||
).toBeNull();
|
||||
expect(
|
||||
findGitPageURLTarget('https://github.com/acme/docs/tree/main/api/auth.md', [
|
||||
...SPACES,
|
||||
{
|
||||
id: 'other-ref',
|
||||
gitSync: {
|
||||
url: 'https://github.com/acme/docs/tree/main/api',
|
||||
installationProjectDirectory: '',
|
||||
},
|
||||
},
|
||||
])
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('prefers the most specific directory and deduplicates site placements', () => {
|
||||
expect(
|
||||
findGitPageURLTarget('https://github.com/acme/docs/tree/main/api/auth.md', [
|
||||
...SPACES,
|
||||
SPACES[1]!,
|
||||
{
|
||||
id: 'root',
|
||||
gitSync: { url: SPACES[0]!.gitSync.url, installationProjectDirectory: '' },
|
||||
},
|
||||
])?.space
|
||||
).toBe('b');
|
||||
});
|
||||
|
||||
it('requires directory metadata, including an explicit empty root directory', () => {
|
||||
expect(
|
||||
findGitPageURLTarget('https://github.com/acme/docs/tree/main/api/auth.md', [
|
||||
{ id: 'old', gitSync: { url: SPACES[0]!.gitSync.url } },
|
||||
])
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findPageByGitPath', () => {
|
||||
const pages = [
|
||||
{ id: 'auth', git: { path: 'api/auth.md' }, pages: [] },
|
||||
{ id: 'group', pages: [{ id: 'readme', git: { path: 'api/11.8/README.md' }, pages: [] }] },
|
||||
];
|
||||
it('finds nested pages and directory README links', () => {
|
||||
expect(findPageByGitPath(pages, 'api/auth.md')?.id).toBe('auth');
|
||||
expect(findPageByGitPath(pages, 'api/11.8/')?.id).toBe('readme');
|
||||
expect(findPageByGitPath(pages, 'api/missing.md')).toBeNull();
|
||||
});
|
||||
it('does not select between duplicate paths', () => {
|
||||
expect(
|
||||
findPageByGitPath(
|
||||
[...pages, { id: 'copy', git: { path: 'api/auth.md' }, pages: [] }],
|
||||
'api/auth.md'
|
||||
)
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
export interface GitPageURLSpace {
|
||||
id: string;
|
||||
gitSync?: {
|
||||
url?: string;
|
||||
installationProjectDirectory?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GitPageURLTarget {
|
||||
space: string;
|
||||
path: string;
|
||||
anchor?: string;
|
||||
}
|
||||
|
||||
/** Locate a unique owning space without fetching any revisions. */
|
||||
export function findGitPageURLTarget(
|
||||
href: string,
|
||||
spaces: readonly GitPageURLSpace[]
|
||||
): GitPageURLTarget | null {
|
||||
const url = parseURL(href);
|
||||
if (!url || url.search) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const matches = new Map<string, GitPageURLTarget & { root: string; tree: string }>();
|
||||
for (const space of spaces) {
|
||||
const { url: treeURL, installationProjectDirectory } = space.gitSync ?? {};
|
||||
if (!treeURL || installationProjectDirectory === undefined) {
|
||||
continue;
|
||||
}
|
||||
const tree = parseURL(treeURL);
|
||||
if (!tree || tree.host !== url.host) {
|
||||
continue;
|
||||
}
|
||||
const prefix = tree.pathname.replace(/\/$/, '');
|
||||
const blobPrefix = prefix
|
||||
.replace('/-/tree/', '/-/blob/')
|
||||
.replace(/^(\/[^/]+\/[^/]+)\/tree\//, '$1/blob/');
|
||||
const matchedPrefix = [prefix, blobPrefix].find((candidate) =>
|
||||
url.pathname.startsWith(`${candidate}/`)
|
||||
);
|
||||
if (!matchedPrefix) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let filePath: string;
|
||||
try {
|
||||
const encoded = url.pathname.slice(matchedPrefix.length + 1);
|
||||
// Encoded separators make repository/ref boundaries ambiguous.
|
||||
if (/%2f|%5c/i.test(encoded)) {
|
||||
continue;
|
||||
}
|
||||
filePath = decodeURIComponent(encoded);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const root = installationProjectDirectory.replace(/^\.\//, '').replace(/^\/+|\/+$/g, '');
|
||||
if (
|
||||
filePath.split('/').some((part) => part === '.' || part === '..') ||
|
||||
filePath.includes('\\')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (root && filePath !== root && !filePath.startsWith(`${root}/`)) {
|
||||
continue;
|
||||
}
|
||||
matches.set(space.id, {
|
||||
space: space.id,
|
||||
path: filePath,
|
||||
anchor: url.hash.slice(1) || undefined,
|
||||
root,
|
||||
tree: `${tree.host}${prefix}`,
|
||||
});
|
||||
}
|
||||
|
||||
const candidates = [...matches.values()];
|
||||
if (new Set(candidates.map((candidate) => candidate.tree)).size !== 1) {
|
||||
return null;
|
||||
}
|
||||
const longestRoot = Math.max(...candidates.map((candidate) => candidate.root.length));
|
||||
const owners = candidates.filter((candidate) => candidate.root.length === longestRoot);
|
||||
if (owners.length !== 1) {
|
||||
return null;
|
||||
}
|
||||
const owner = owners[0]!;
|
||||
return { space: owner.space, path: owner.path, anchor: owner.anchor };
|
||||
}
|
||||
|
||||
/** Match an API revision's nested page tree, including directory README links. */
|
||||
export function findPageByGitPath<T extends { id: string; git?: { path: string }; pages?: T[] }>(
|
||||
pages: readonly T[],
|
||||
filePath: string
|
||||
): T | null {
|
||||
const paths = [filePath, `${filePath.replace(/\/$/, '')}/README.md`];
|
||||
const matches: T[] = [];
|
||||
const visit = (children: readonly T[]) => {
|
||||
for (const page of children) {
|
||||
if (page.git && paths.includes(page.git.path)) {
|
||||
matches.push(page);
|
||||
}
|
||||
visit(page.pages ?? []);
|
||||
}
|
||||
};
|
||||
visit(pages);
|
||||
return matches.length === 1 ? matches[0]! : null;
|
||||
}
|
||||
|
||||
function parseURL(href: string): URL | null {
|
||||
try {
|
||||
const url = new URL(href);
|
||||
return ['https:', 'http:'].includes(url.protocol) && !url.username && !url.password
|
||||
? url
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { describe, expect, it, mock } from 'bun:test';
|
||||
|
||||
import type { Revision, RevisionPageDocument, SiteSpace, Space } from '@gitbook/api';
|
||||
|
||||
@@ -738,3 +738,155 @@ describe('resolveContentRef for direct space links', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('repository page links', () => {
|
||||
function fixture(options: { denied?: boolean; missing?: boolean; draft?: boolean } = {}) {
|
||||
const page = {
|
||||
id: 'target-page',
|
||||
type: 'document',
|
||||
title: 'Authentication',
|
||||
path: 'authentication',
|
||||
slug: 'authentication',
|
||||
pages: [],
|
||||
git: { path: 'api/auth.md', oid: 'blob' },
|
||||
} as unknown as RevisionPageDocument;
|
||||
const targetSpace = {
|
||||
id: 'target',
|
||||
title: 'API',
|
||||
organization: 'org',
|
||||
revision: 'target-main',
|
||||
gitSync: {
|
||||
url: 'https://github.com/acme/docs/tree/main',
|
||||
installationProjectDirectory: 'api',
|
||||
},
|
||||
urls: {
|
||||
app: 'https://app.gitbook.com/s/target',
|
||||
published: 'https://docs.example.com/api/',
|
||||
},
|
||||
} as unknown as Space;
|
||||
const targetSiteSpace = {
|
||||
id: 'site-target',
|
||||
title: 'API',
|
||||
space: targetSpace,
|
||||
path: 'api',
|
||||
draft: options.draft ?? false,
|
||||
urls: { published: 'https://docs.example.com/api/' },
|
||||
} as unknown as SiteSpace;
|
||||
const getSpace = mock(async () =>
|
||||
options.denied ? { error: { code: 403, message: 'Forbidden' } } : { data: targetSpace }
|
||||
);
|
||||
const getRevision = mock(async () => ({
|
||||
data: {
|
||||
id: 'target-main',
|
||||
pages: options.missing
|
||||
? []
|
||||
: [{ ...page, id: 'home', path: '', slug: '', git: undefined }, page],
|
||||
files: [],
|
||||
reusableContents: [],
|
||||
},
|
||||
}));
|
||||
const context = {
|
||||
organizationId: 'org',
|
||||
site: { id: 'site' },
|
||||
space: { id: 'source', revision: 'source-main' },
|
||||
revision: { pages: [] },
|
||||
revisionId: 'source-main',
|
||||
changeRequest: null,
|
||||
structure: { type: 'siteSpaces', structure: [targetSiteSpace] },
|
||||
linker: createLinker({
|
||||
host: 'docs.example.com',
|
||||
siteBasePath: '/',
|
||||
spaceBasePath: '/source/',
|
||||
}),
|
||||
dataFetcher: { getSpace, getRevision },
|
||||
} as unknown as GitBookAnyContext;
|
||||
return { context, getSpace, getRevision };
|
||||
}
|
||||
|
||||
const ref = {
|
||||
kind: 'url' as const,
|
||||
url: 'https://github.com/acme/docs/tree/main/api/auth.md#tokens',
|
||||
};
|
||||
|
||||
it('renders a matching repository URL as a site page link with its anchor', async () => {
|
||||
const { context, getRevision } = fixture();
|
||||
const result = await resolveContentRef(ref, context);
|
||||
expect(result?.href).toBe('/api/authentication#tokens');
|
||||
expect(result?.text).toBe('Authentication');
|
||||
expect(result?.ancestors?.[0]?.label).toBe('API');
|
||||
expect(result?.resolvedRef).toEqual({
|
||||
kind: 'anchor',
|
||||
space: 'target',
|
||||
page: 'target-page',
|
||||
anchor: 'tokens',
|
||||
});
|
||||
expect(getRevision).toHaveBeenCalledWith({
|
||||
spaceId: 'target',
|
||||
revisionId: 'target-main',
|
||||
metadata: true,
|
||||
});
|
||||
expect(ref.kind).toBe('url');
|
||||
});
|
||||
|
||||
it('preserves asset URLs that do not match a page', async () => {
|
||||
const { context } = fixture();
|
||||
const assetRef = {
|
||||
kind: 'url' as const,
|
||||
url: ref.url.replace('auth.md#tokens', 'diagram.png'),
|
||||
};
|
||||
expect((await resolveContentRef(assetRef, context))?.href).toBe(assetRef.url);
|
||||
});
|
||||
|
||||
it('reads only the matching space in a 500-space site', async () => {
|
||||
const { context, getSpace, getRevision } = fixture();
|
||||
if (!('site' in context) || context.structure.type !== 'siteSpaces') {
|
||||
throw new Error('Expected a site fixture');
|
||||
}
|
||||
const target = context.structure.structure[0]!;
|
||||
context.structure.structure.push(
|
||||
...Array.from({ length: 499 }, (_, index) => ({
|
||||
...target,
|
||||
id: `site-${index}`,
|
||||
space: {
|
||||
...target.space,
|
||||
id: `space-${index}`,
|
||||
gitSync: {
|
||||
...target.space.gitSync!,
|
||||
installationProjectDirectory: `other-${index}`,
|
||||
},
|
||||
},
|
||||
}))
|
||||
);
|
||||
expect((await resolveContentRef(ref, context))?.href).toBe('/api/authentication#tokens');
|
||||
expect(getSpace).toHaveBeenCalledTimes(1);
|
||||
expect(getRevision).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each([{ denied: true }, { missing: true }, { draft: true }])(
|
||||
'preserves the fallback for unavailable content: %j',
|
||||
async (state) => {
|
||||
const { context } = fixture(state);
|
||||
const result = await resolveContentRef(ref, context);
|
||||
expect(result).toEqual({ href: ref.url, text: ref.url, active: false });
|
||||
}
|
||||
);
|
||||
|
||||
it('does not fetch revisions for a different repository or branch', async () => {
|
||||
const { context, getRevision } = fixture();
|
||||
for (const url of [
|
||||
ref.url.replace('/main/', '/preview/'),
|
||||
ref.url.replace('/acme/', '/other/'),
|
||||
]) {
|
||||
expect((await resolveContentRef({ kind: 'url', url }, context))?.href).toBe(url);
|
||||
}
|
||||
expect(getRevision).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('resolves the unchanged stored URL when the target becomes available', async () => {
|
||||
const state = { missing: true };
|
||||
const { context } = fixture(state);
|
||||
expect((await resolveContentRef(ref, context))?.href).toBe(ref.url);
|
||||
state.missing = false;
|
||||
expect((await resolveContentRef(ref, context))?.href).toBe('/api/authentication#tokens');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,12 +16,14 @@ import type { Filesystem } from '@gitbook/openapi-parser';
|
||||
|
||||
import { getGitBookAppHref } from './app';
|
||||
import { getBlockById, getBlockTitle } from './document';
|
||||
import { findGitPageURLTarget, findPageByGitPath } from './gitPageURL';
|
||||
import { resolvePageId } from './pages';
|
||||
import {
|
||||
findSiteSpaceBy,
|
||||
getFallbackSiteSpacePath,
|
||||
getLinkerForSiteSpace,
|
||||
getLocalizedTitle,
|
||||
listAllSiteSpaces,
|
||||
} from './sites';
|
||||
import { getRevisionTags, resolveTag } from './tags';
|
||||
import type { ClassValue } from './tailwind';
|
||||
@@ -43,6 +45,8 @@ import {
|
||||
import { type GitBookLinker, createLinker, linkerWithAbsoluteURLs } from '@/lib/links';
|
||||
|
||||
export interface ResolvedContentRef {
|
||||
/** Effective destination when a repository URL resolves to a site page. */
|
||||
resolvedRef?: ContentRef;
|
||||
/** Text to render in the content ref */
|
||||
text: string;
|
||||
/** Additional sub text to render in the content ref */
|
||||
@@ -143,6 +147,66 @@ export async function resolveContentRef(
|
||||
|
||||
switch (contentRef.kind) {
|
||||
case 'url': {
|
||||
if ('site' in context) {
|
||||
const target = findGitPageURLTarget(
|
||||
contentRef.url,
|
||||
listAllSiteSpaces(context.structure)
|
||||
.filter((entry) => !entry.draft)
|
||||
.map((entry) => entry.space)
|
||||
);
|
||||
if (target) {
|
||||
try {
|
||||
// Site CRs must select the target member's revision here instead of main.
|
||||
const targetContext = await createContextForSpace(
|
||||
target.space,
|
||||
context,
|
||||
true
|
||||
);
|
||||
const page =
|
||||
targetContext &&
|
||||
findPageByGitPath(
|
||||
targetContext.spaceContext.revision.pages,
|
||||
target.path
|
||||
);
|
||||
if (page?.type === 'document' && targetContext) {
|
||||
const resolvedRef: ContentRef = target.anchor
|
||||
? {
|
||||
kind: 'anchor',
|
||||
space: target.space,
|
||||
page: page.id,
|
||||
anchor: target.anchor,
|
||||
}
|
||||
: { kind: 'page', space: target.space, page: page.id };
|
||||
const resolved = await resolveContentRef(
|
||||
resolvedRef,
|
||||
targetContext.spaceContext,
|
||||
options
|
||||
);
|
||||
if (resolved) {
|
||||
const foundSiteSpace = findSiteSpaceBy(
|
||||
context.structure,
|
||||
(entry) => entry.space.id === target.space
|
||||
);
|
||||
return {
|
||||
...resolved,
|
||||
resolvedRef,
|
||||
ancestors: [
|
||||
...resolvePageAncestors(
|
||||
context,
|
||||
resolvedRef,
|
||||
foundSiteSpace,
|
||||
targetContext
|
||||
),
|
||||
...(resolved.ancestors ?? []),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// An unavailable or forbidden target must not prevent rendering the source page.
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
href: contentRef.url,
|
||||
text: contentRef.url,
|
||||
@@ -627,7 +691,8 @@ async function resolveContentRefInSpace(
|
||||
*/
|
||||
async function createContextForSpace(
|
||||
spaceId: string,
|
||||
context: GitBookAnyContext
|
||||
context: GitBookAnyContext,
|
||||
revisionMetadata = false
|
||||
): Promise<{
|
||||
spaceContext: GitBookSpaceContext;
|
||||
baseURL: URL;
|
||||
@@ -639,6 +704,7 @@ async function createContextForSpace(
|
||||
shareKey: context?.shareKey,
|
||||
changeRequest: undefined,
|
||||
revision: undefined,
|
||||
revisionMetadata,
|
||||
})
|
||||
),
|
||||
getBestTargetSpace(context, spaceId),
|
||||
|
||||
Reference in New Issue
Block a user