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:
Samy Pessé
2024-03-18 17:16:53 +00:00
committed by GitHub
parent 718452728f
commit d0b92dd408
7 changed files with 138 additions and 9 deletions
@@ -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;
}
+5 -2
View File
@@ -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.
+3 -1
View File
@@ -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 ? (
+2 -1
View File
@@ -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),
}}
/>
) : (
+81
View File
@@ -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
View File
@@ -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,
};
}