mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-25 11:52:10 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27441c0605 | |||
| dd1ca93837 | |||
| 08e61c89f0 | |||
| 587398ddd4 | |||
| 321b06d873 | |||
| 50eee8fec9 | |||
| ec709c3d99 |
@@ -354,7 +354,7 @@
|
||||
},
|
||||
"catalog": {
|
||||
"@base-ui/react": "^1.7.0",
|
||||
"@gitbook/api": "0.199.0",
|
||||
"@gitbook/api": "0.200.0",
|
||||
"@scalar/api-client-react": "^1.3.46",
|
||||
"@tsconfig/node20": "^20.1.6",
|
||||
"@tsconfig/strictest": "^2.0.6",
|
||||
@@ -726,7 +726,7 @@
|
||||
|
||||
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.2.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.2.0" } }, "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q=="],
|
||||
|
||||
"@gitbook/api": ["@gitbook/api@0.199.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-yLxkSTXlGk7jbtThV2vpnTfqZTq6yLgMJt6jhDGqOmP0LsYAVYYtg8NPf2tXp37aF46m3Z0D908wfSQyu374cg=="],
|
||||
"@gitbook/api": ["@gitbook/api@0.200.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-JgPosRwabDqw8FzTpSZdEWCUdwUxO2vIHJH9A8CXYXBmubjjj/Ft01qnzbL2T5q1CSLSQKe9PvW9t5V+R/8iJA=="],
|
||||
|
||||
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
|
||||
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@
|
||||
"@tsconfig/strictest": "^2.0.6",
|
||||
"@tsconfig/node20": "^20.1.6",
|
||||
"@base-ui/react": "^1.7.0",
|
||||
"@gitbook/api": "0.199.0",
|
||||
"@gitbook/api": "0.200.0",
|
||||
"@scalar/api-client-react": "^1.3.46",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
import { permanentRedirect, redirect } from 'next/navigation';
|
||||
|
||||
import {
|
||||
SITE_REDIRECT_SOURCE_PATH_MAX_LENGTH,
|
||||
@@ -89,7 +89,21 @@ async function resolvePage(context: GitBookSiteContext, params: PagePathParams |
|
||||
})
|
||||
));
|
||||
if (resolvedSiteRedirect) {
|
||||
return redirect(linker.toLinkForContent(resolvedSiteRedirect.target));
|
||||
const destination = linker.toLinkForContent(resolvedSiteRedirect.target);
|
||||
const isPublicLiveContext =
|
||||
!shareKey &&
|
||||
!context.changeRequest &&
|
||||
!context.preview &&
|
||||
context.revisionId === context.space.revision &&
|
||||
!context.isLoggedInVisitor;
|
||||
if (
|
||||
resolvedSiteRedirect.redirect?.permanent &&
|
||||
!resolvedSiteRedirect.redirect.draft &&
|
||||
isPublicLiveContext
|
||||
) {
|
||||
return permanentRedirect(destination);
|
||||
}
|
||||
return redirect(destination);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -201,6 +201,9 @@ export type GitBookSiteContext = GitBookSpaceContext & {
|
||||
/** Whether the request included a visitor token. */
|
||||
isLoggedInVisitor: boolean;
|
||||
|
||||
/** Whether the site is rendered from a preview URL. */
|
||||
preview: boolean;
|
||||
|
||||
/** Whether to display agent instructions in the markdown output. Defaults to true when undefined. */
|
||||
displayAgentInstructions?: boolean;
|
||||
|
||||
@@ -287,6 +290,7 @@ export async function fetchSiteContextByURLLookup(
|
||||
isFallback: data.isFallback ?? false,
|
||||
noIndexSearch: data.noIndexSearch ?? false,
|
||||
isLoggedInVisitor: data.isLoggedInVisitor ?? false,
|
||||
preview: data.preview ?? false,
|
||||
displayAgentInstructions: data.displayAgentInstructions,
|
||||
isAiAgent: data.isAiAgent,
|
||||
});
|
||||
@@ -310,6 +314,7 @@ export async function fetchSiteContextByIds(
|
||||
isFallback: boolean;
|
||||
noIndexSearch: boolean;
|
||||
isLoggedInVisitor: boolean;
|
||||
preview: boolean;
|
||||
displayAgentInstructions?: boolean;
|
||||
isAiAgent?: boolean;
|
||||
}
|
||||
@@ -439,6 +444,7 @@ export async function fetchSiteContextByIds(
|
||||
isFallback: ids.isFallback,
|
||||
noIndexSearch: ids.noIndexSearch,
|
||||
isLoggedInVisitor: ids.isLoggedInVisitor,
|
||||
preview: ids.preview,
|
||||
displayAgentInstructions: ids.displayAgentInstructions,
|
||||
isAiAgent: ids.isAiAgent,
|
||||
};
|
||||
|
||||
@@ -219,7 +219,8 @@ async function renderGroupPageMarkdown(args: {
|
||||
|
||||
/**
|
||||
* Re-writes URLs in a markdown content:
|
||||
* -
|
||||
* - stable content refs (`/pages/:id`, `/spaces/:id/pages/:id`, `/files/:id`...) in links,
|
||||
* images, definitions and in the `href`/`src` of raw HTML blocks are resolved to site URLs.
|
||||
* - the URL of every relative <a> link so it is expressed from the site-root.
|
||||
*/
|
||||
async function rewriteMarkdownLinks(
|
||||
@@ -246,30 +247,13 @@ async function rewriteMarkdownLinks(
|
||||
pending.push(
|
||||
(async () => {
|
||||
const resolved = await resolveContentRef(contentRef, context);
|
||||
if (resolved?.href) {
|
||||
node.url = resolved.href;
|
||||
} else {
|
||||
// We use an absolute URL so that crawler don't follow it.
|
||||
node.url = `broken://${original.startsWith('/') ? original.slice(1) : original}`;
|
||||
}
|
||||
node.url = resolved?.href ?? toBrokenURL(original);
|
||||
|
||||
if (isMention) {
|
||||
// Replace the text for mentions as otherwise it contains the raw ref
|
||||
if (resolved) {
|
||||
node.children = [
|
||||
{
|
||||
type: 'text',
|
||||
value: resolved.text,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
node.children = [
|
||||
{
|
||||
type: 'text',
|
||||
value: 'Broken mention',
|
||||
},
|
||||
];
|
||||
}
|
||||
node.children = [
|
||||
{ type: 'text', value: resolved?.text ?? 'Broken mention' },
|
||||
];
|
||||
node.title = undefined;
|
||||
}
|
||||
})()
|
||||
@@ -288,6 +272,20 @@ async function rewriteMarkdownLinks(
|
||||
}
|
||||
});
|
||||
|
||||
visit(tree, 'image', (node: Image) => {
|
||||
pending.push(rewriteNodeURL(context, node));
|
||||
});
|
||||
|
||||
visit(tree, 'definition', (node: Definition) => {
|
||||
pending.push(rewriteNodeURL(context, node));
|
||||
});
|
||||
|
||||
// Blocks markdown cannot express (tables, cards, figures...) are emitted as raw HTML,
|
||||
// with the same stable refs in their anchors and images.
|
||||
visit(tree, 'html', (node: Html) => {
|
||||
pending.push(rewriteHTMLRefs(context, node));
|
||||
});
|
||||
|
||||
if (pending.length > 0) {
|
||||
await Promise.all(pending);
|
||||
}
|
||||
@@ -295,6 +293,76 @@ async function rewriteMarkdownLinks(
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a URL if it is a stable content ref. Returns null for anything else
|
||||
* (external URLs, anchors, plain paths) so the caller leaves it untouched.
|
||||
*/
|
||||
async function resolveRefURL(
|
||||
context: GitBookAnyContext,
|
||||
url: string
|
||||
): Promise<{ url: string; text: string | null } | null> {
|
||||
if (checkIsExternalURL(url) || checkIsAnchor(url)) {
|
||||
return null;
|
||||
}
|
||||
const contentRef = resolveStringContentRef(url);
|
||||
if (!contentRef) {
|
||||
return null;
|
||||
}
|
||||
const resolved = await resolveContentRef(contentRef, context);
|
||||
return { url: resolved?.href ?? toBrokenURL(url), text: resolved?.text ?? null };
|
||||
}
|
||||
|
||||
async function rewriteNodeURL(context: GitBookAnyContext, node: Image | Definition) {
|
||||
const resolved = await resolveRefURL(context, node.url);
|
||||
if (resolved) {
|
||||
node.url = resolved.url;
|
||||
}
|
||||
}
|
||||
|
||||
async function rewriteHTMLRefs(context: GitBookAnyContext, node: Html): Promise<void> {
|
||||
node.value = await replaceAsync(node.value, HTML_ANCHOR_RE, async (match) => {
|
||||
const [full, before = '', href = '', after = '', text = ''] = match;
|
||||
const resolved = await resolveRefURL(context, href);
|
||||
if (!resolved) {
|
||||
return full;
|
||||
}
|
||||
// The API emits the raw ref as the text; swap it for the resolved title.
|
||||
const content = text === href ? escapeHTML(resolved.text ?? 'Broken link') : text;
|
||||
return `<a${before}href="${escapeHTML(resolved.url)}"${after}>${content}</a>`;
|
||||
});
|
||||
|
||||
node.value = await replaceAsync(node.value, HTML_SRC_RE, async (match) => {
|
||||
const [full, src = ''] = match;
|
||||
const resolved = await resolveRefURL(context, src);
|
||||
return resolved ? `src="${escapeHTML(resolved.url)}"` : full;
|
||||
});
|
||||
}
|
||||
|
||||
async function replaceAsync(
|
||||
value: string,
|
||||
re: RegExp,
|
||||
replacer: (match: RegExpMatchArray) => Promise<string>
|
||||
): Promise<string> {
|
||||
const replacements = await Promise.all(Array.from(value.matchAll(re), replacer));
|
||||
let index = 0;
|
||||
return value.replace(re, () => replacements[index++]!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use an absolute URL so that crawlers don't follow it.
|
||||
*/
|
||||
function toBrokenURL(original: string): string {
|
||||
return `broken://${original.startsWith('/') ? original.slice(1) : original}`;
|
||||
}
|
||||
|
||||
function escapeHTML(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function isMentionLike(node: Link) {
|
||||
if (node.title === 'mention') {
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user