mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-21 01:53:26 +00:00
Improve text displayed in mention for anchor content refs (#287)
* Resolve anchor content reference to the block text for mentions * Only apply it for mentions/etc, where text is needed * Handle tabs/expandable * Rename to resolveAnchorText
This commit is contained in:
@@ -8,7 +8,9 @@ export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>
|
||||
const { block, context, style } = props;
|
||||
const kind = block?.data?.ref?.kind;
|
||||
|
||||
const resolved = await context.resolveContentRef(block.data.ref);
|
||||
const resolved = await context.resolveContentRef(block.data.ref, {
|
||||
resolveAnchorText: true,
|
||||
});
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ContentRef, JSONDocument } from '@gitbook/api';
|
||||
|
||||
import { ContentTarget } from '@/lib/api';
|
||||
import { ResolvedContentRef } from '@/lib/references';
|
||||
import { ResolveContentRefOptions, ResolvedContentRef } from '@/lib/references';
|
||||
import { ClassValue } from '@/lib/tailwind';
|
||||
|
||||
import { Blocks } from './Blocks';
|
||||
@@ -21,7 +21,10 @@ export interface DocumentContext {
|
||||
/**
|
||||
* Resolve a content reference.
|
||||
*/
|
||||
resolveContentRef: (ref: ContentRef) => Promise<ResolvedContentRef | null>;
|
||||
resolveContentRef: (
|
||||
ref: ContentRef,
|
||||
options?: ResolveContentRefOptions,
|
||||
) => Promise<ResolvedContentRef | null>;
|
||||
|
||||
/**
|
||||
* Transform an ID to be added to the DOM.
|
||||
|
||||
@@ -7,7 +7,9 @@ import { InlineProps } from './Inline';
|
||||
export async function Mention(props: InlineProps<DocumentInlineMention>) {
|
||||
const { inline, context } = props;
|
||||
|
||||
const resolved = await context.resolveContentRef(inline.data.ref);
|
||||
const resolved = await context.resolveContentRef(inline.data.ref, {
|
||||
resolveAnchorText: true,
|
||||
});
|
||||
|
||||
if (!resolved) {
|
||||
return null;
|
||||
|
||||
@@ -133,7 +133,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
</Tag>
|
||||
);
|
||||
case 'content-ref': {
|
||||
const resolved = value ? await context.resolveContentRef(value as ContentRef) : null;
|
||||
const resolved = value
|
||||
? await context.resolveContentRef(value as ContentRef, {
|
||||
resolveAnchorText: true,
|
||||
})
|
||||
: null;
|
||||
return (
|
||||
<Tag className={tcls('text-base', 'text-balance')}>
|
||||
{resolved && resolved.emoji ? (
|
||||
|
||||
@@ -70,7 +70,8 @@ export function PageBody(props: {
|
||||
context={{
|
||||
mode: 'default',
|
||||
content: contentTarget,
|
||||
resolveContentRef: (ref) => resolveContentRef(ref, context),
|
||||
resolveContentRef: (ref, options) =>
|
||||
resolveContentRef(ref, context, options),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -147,3 +147,84 @@ export function isNodeEmpty(
|
||||
const text = getNodeText(node);
|
||||
return text.trim().length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the title for a node.
|
||||
*/
|
||||
export function getBlockTitle(block: DocumentBlock): string {
|
||||
switch (block.type) {
|
||||
case 'expandable': {
|
||||
const titleFragment = getNodeFragmentByType(block, 'title');
|
||||
if (titleFragment) {
|
||||
return getNodeText(titleFragment);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
case 'tabs-item': {
|
||||
return block.data.title ?? '';
|
||||
}
|
||||
|
||||
case 'swagger': {
|
||||
return `${block.data.method?.toUpperCase()} ${block.data.path}`;
|
||||
}
|
||||
|
||||
case 'heading-1':
|
||||
case 'heading-2':
|
||||
case 'heading-3':
|
||||
default:
|
||||
return getNodeText(block);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a block by its ID in the document.
|
||||
*/
|
||||
export function getBlockById(document: JSONDocument, id: string): DocumentBlock | null {
|
||||
return findBlock(document, (block) => {
|
||||
if ('meta' in block && block.meta && 'id' in block.meta) {
|
||||
return block.meta.id === id;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a block by a predicate in the document.
|
||||
*/
|
||||
function findBlock(
|
||||
container: JSONDocument | DocumentBlock | DocumentFragment,
|
||||
test: (block: DocumentBlock) => boolean,
|
||||
): DocumentBlock | null {
|
||||
if (!('nodes' in container)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const block of container.nodes) {
|
||||
if (block.object !== 'block') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (test(block)) {
|
||||
return block;
|
||||
}
|
||||
|
||||
if (block.object === 'block' && 'nodes' in block) {
|
||||
const result = findBlock(block, test);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (block.object === 'block' && 'fragments' in block) {
|
||||
for (const fragment of block.fragments) {
|
||||
const result = findBlock(fragment, test);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
+39
-3
@@ -4,12 +4,14 @@ import assertNever from 'assert-never';
|
||||
import {
|
||||
ContentPointer,
|
||||
getCollection,
|
||||
getDocument,
|
||||
getRevisionFile,
|
||||
getSpace,
|
||||
getSpaceContentData,
|
||||
getUserById,
|
||||
ignoreAPIError,
|
||||
} from './api';
|
||||
import { getBlockById, getBlockTitle } from './document';
|
||||
import { gitbookAppHref, pageHref, PageHrefContext } from './links';
|
||||
import { getPagePath, resolvePageId } from './pages';
|
||||
|
||||
@@ -53,13 +55,23 @@ export interface ContentRefContext extends PageHrefContext {
|
||||
page?: RevisionPageDocument;
|
||||
}
|
||||
|
||||
export interface ResolveContentRefOptions {
|
||||
/**
|
||||
* Should the content ref be rendered as text.
|
||||
* @default false
|
||||
*/
|
||||
resolveAnchorText?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a content reference to be rendered.
|
||||
*/
|
||||
export async function resolveContentRef(
|
||||
contentRef: ContentRef,
|
||||
context: ContentRefContext,
|
||||
options: ResolveContentRefOptions = {},
|
||||
): Promise<ResolvedContentRef | null> {
|
||||
const { resolveAnchorText = false } = options;
|
||||
const { space, revisionId, pages, page: activePage, ...linksContext } = context;
|
||||
|
||||
switch (contentRef.kind) {
|
||||
@@ -100,9 +112,33 @@ export async function resolveContentRef(
|
||||
}
|
||||
|
||||
let anchor = contentRef.kind === 'page' ? undefined : contentRef.anchor;
|
||||
|
||||
const isCurrentPage = page.id === activePage?.id;
|
||||
|
||||
let href = '';
|
||||
let text = '';
|
||||
let emoji: string | undefined = undefined;
|
||||
|
||||
// Compute the text to display for the link
|
||||
if (anchor) {
|
||||
text = '#' + anchor;
|
||||
|
||||
if (resolveAnchorText) {
|
||||
const document = page.documentId
|
||||
? await getDocument(space.id, page.documentId)
|
||||
: null;
|
||||
if (document) {
|
||||
const block = getBlockById(document, anchor);
|
||||
if (block) {
|
||||
text = getBlockTitle(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
text = page.title;
|
||||
emoji = isCurrentPage ? undefined : page.emoji;
|
||||
}
|
||||
|
||||
// Compute the href for the link
|
||||
if (context.baseUrl) {
|
||||
// Page in another content
|
||||
href = new URL(getPagePath(pages, page), context.baseUrl).toString();
|
||||
@@ -117,8 +153,8 @@ export async function resolveContentRef(
|
||||
|
||||
return {
|
||||
href,
|
||||
text: anchor ? (isCurrentPage ? '' : page.title) + '#' + anchor : page.title,
|
||||
emoji: isCurrentPage ? undefined : page.emoji,
|
||||
text,
|
||||
emoji,
|
||||
active: !anchor && page.id === activePage?.id,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user