Skip to content

Introduce slim documents #3140

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/shaggy-pumas-rush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"gitbook-v2": patch
"gitbook": patch
---

Introduce the concept of slim documents
5 changes: 3 additions & 2 deletions packages/gitbook-v2/src/lib/data/api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getSlimJSONDocument } from '@/lib/slim-document';
import { trace } from '@/lib/tracing';
import {
type ComputedContentSource,
Expand Down Expand Up @@ -779,7 +780,7 @@ const getDocumentUncached = async (
cacheTag(...getCacheTagsFromResponse(res));
cacheLife('max');
}
return res.data;
return getSlimJSONDocument(res.data);
});
});
};
Expand Down Expand Up @@ -872,7 +873,7 @@ const getComputedDocumentUncached = async (
cacheTag(...getCacheTagsFromResponse(res));
cacheLife('max');
}
return res.data;
return getSlimJSONDocument(res.data);
});
}
);
Expand Down
5 changes: 3 additions & 2 deletions packages/gitbook-v2/src/lib/data/pages.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { JSONDocument, RevisionPageDocument, Space } from '@gitbook/api';
import type { SlimJSONDocument } from '@/lib/slim-document';
import type { RevisionPageDocument, Space } from '@gitbook/api';
import { getDataOrNull } from './errors';
import type { GitBookDataFetcher } from './types';

Expand All @@ -9,7 +10,7 @@ export async function getPageDocument(
dataFetcher: GitBookDataFetcher,
space: Space,
page: RevisionPageDocument
): Promise<JSONDocument | null> {
): Promise<SlimJSONDocument | null> {
if (page.documentId) {
return getDataOrNull(
dataFetcher.getDocument({ spaceId: space.id, documentId: page.documentId })
Expand Down
5 changes: 3 additions & 2 deletions packages/gitbook-v2/src/lib/data/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { SlimJSONDocument } from '@/lib/slim-document';
import type * as api from '@gitbook/api';

export type DataFetcherErrorData = {
Expand Down Expand Up @@ -110,7 +111,7 @@ export interface GitBookDataFetcher {
* Get a document by its space ID and document ID.
*/
getDocument(params: { spaceId: string; documentId: string }): Promise<
DataFetcherResponse<api.JSONDocument>
DataFetcherResponse<SlimJSONDocument>
>;

/**
Expand All @@ -121,7 +122,7 @@ export interface GitBookDataFetcher {
spaceId: string;
source: api.ComputedContentSource;
seed: string;
}): Promise<DataFetcherResponse<api.JSONDocument>>;
}): Promise<DataFetcherResponse<SlimJSONDocument>>;

/**
* Get a reusable content by its space ID, revision ID and reusable content ID.
Expand Down
12 changes: 6 additions & 6 deletions packages/gitbook/src/components/DocumentView/Block.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { DocumentBlock, JSONDocument } from '@gitbook/api';
import React from 'react';

import {
Expand All @@ -10,6 +9,7 @@ import {
} from '@/components/primitives';
import type { ClassValue } from '@/lib/tailwind';

import type { SlimDocumentBlock, SlimJSONDocument } from '@/lib/slim-document';
import { BlockContentRef } from './BlockContentRef';
import { CodeBlock } from './CodeBlock';
import { Divider } from './Divider';
Expand All @@ -34,10 +34,10 @@ import { StepperStep } from './StepperStep';
import { Table } from './Table';
import { Tabs } from './Tabs';

export interface BlockProps<Block extends DocumentBlock> extends DocumentContextProps {
export interface BlockProps<Block extends SlimDocumentBlock> extends DocumentContextProps {
block: Block;
document: JSONDocument;
ancestorBlocks: DocumentBlock[];
document: SlimJSONDocument;
ancestorBlocks: SlimDocumentBlock[];
/** If true, we estimate that the block will be outside the initial viewport */
isEstimatedOffscreen: boolean;
/** Class names to be passed to the underlying DOM element */
Expand All @@ -51,7 +51,7 @@ function nullIfNever(_value: never): null {
return null;
}

export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
export function Block<T extends SlimDocumentBlock>(props: BlockProps<T>) {
const { block, style, isEstimatedOffscreen, context } = props;

const content = (() => {
Expand Down Expand Up @@ -132,7 +132,7 @@ export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
/**
* Skeleton for a block while it is being loaded.
*/
export function BlockSkeleton(props: { block: DocumentBlock; style: ClassValue }) {
export function BlockSkeleton(props: { block: SlimDocumentBlock; style: ClassValue }) {
const { block, style } = props;
const id = 'meta' in block && block.meta && 'id' in block.meta ? block.meta.id : undefined;

Expand Down
19 changes: 10 additions & 9 deletions packages/gitbook/src/components/DocumentView/Blocks.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import type { DocumentBlock, JSONDocument } from '@gitbook/api';

import { type ClassValue, tcls } from '@/lib/tailwind';

import type { SlimDocumentBlock, SlimJSONDocument } from '@/lib/slim-document';
import { Block } from './Block';
import type { DocumentContextProps } from './DocumentView';
import { isBlockOffscreen } from './utils';

/**
* Renders a list of blocks with a wrapper element.
*/
export function Blocks<TBlock extends DocumentBlock, Tag extends React.ElementType = 'div'>(
export function Blocks<TBlock extends SlimDocumentBlock, Tag extends React.ElementType = 'div'>(
props: UnwrappedBlocksProps<TBlock> & {
/** HTML tag to use for the wrapper */
tag?: Tag;
Expand All @@ -30,15 +29,15 @@ export function Blocks<TBlock extends DocumentBlock, Tag extends React.ElementTy
);
}

type UnwrappedBlocksProps<TBlock extends DocumentBlock> = DocumentContextProps & {
type UnwrappedBlocksProps<TBlock extends SlimDocumentBlock> = DocumentContextProps & {
/** Blocks to render */
nodes: TBlock[];

/** Document being rendered */
document: JSONDocument;
document: SlimJSONDocument;

/** Ancestors of the blocks */
ancestorBlocks: DocumentBlock[];
ancestorBlocks: SlimDocumentBlock[];

/** Style passed to all blocks */
blockStyle?: ClassValue;
Expand All @@ -50,7 +49,9 @@ type UnwrappedBlocksProps<TBlock extends DocumentBlock> = DocumentContextProps &
/**
* Renders a list of blocks without a wrapper element.
*/
export function UnwrappedBlocks<TBlock extends DocumentBlock>(props: UnwrappedBlocksProps<TBlock>) {
export function UnwrappedBlocks<TBlock extends SlimDocumentBlock>(
props: UnwrappedBlocksProps<TBlock>
) {
const { nodes, blockStyle, isOffscreen: defaultIsOffscreen = false, ...contextProps } = props;

let isOffscreen = defaultIsOffscreen;
Expand All @@ -65,11 +66,11 @@ export function UnwrappedBlocks<TBlock extends DocumentBlock>(props: UnwrappedBl

return (
<Block
key={node.key || `${node.type}-${index}`}
key={'key' in node && node.key ? node.key : `${node.type}-${index}`}
block={node}
style={[
'mx-auto w-full decoration-primary/6',
node.data && 'fullWidth' in node.data && node.data.fullWidth
'data' in node && node.data && 'fullWidth' in node.data && node.data.fullWidth
? 'max-w-screen-xl'
: 'max-w-3xl',
blockStyle,
Expand Down
4 changes: 2 additions & 2 deletions packages/gitbook/src/components/DocumentView/Caption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import type {
DocumentBlockEmbed,
DocumentBlockFile,
DocumentBlockImage,
JSONDocument,
} from '@gitbook/api';

import { getNodeFragmentByName, isNodeEmpty } from '@/lib/document';
import { type ClassValue, tcls } from '@/lib/tailwind';

import type { SlimJSONDocument } from '@/lib/slim-document';
import type { DocumentContextProps } from './DocumentView';
import { Inlines } from './Inlines';

Expand All @@ -18,7 +18,7 @@ import { Inlines } from './Inlines';
export function Caption(
props: {
children: React.ReactNode;
document: JSONDocument;
document: SlimJSONDocument;
style?: ClassValue;
fit?: boolean;
wrapperStyle?: ClassValue;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
'use client';

import type { DocumentBlockCode } from '@gitbook/api';
import { useEffect, useMemo, useRef, useState } from 'react';

import { useInViewportListener } from '@/components/hooks/useInViewportListener';
import { useScrollListener } from '@/components/hooks/useScrollListener';
import type { SlimDocumentBlockCode } from '@/lib/slim-document';
import { useDebounceCallback } from 'usehooks-ts';
import type { BlockProps } from '../Block';
import { CodeBlockRenderer } from './CodeBlockRenderer';
import { CodeBlockRenderer, type CodeBlockRendererProps } from './CodeBlockRenderer';
import type { HighlightLine, RenderedInline } from './highlight';
import { plainHighlight } from './plain-highlight';

type ClientBlockProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> & {
interface ClientBlockProps extends Omit<CodeBlockRendererProps, 'lines'> {
block: SlimDocumentBlockCode;
inlines: RenderedInline[];
};
}

/**
* Render a code-block client-side by loading the highlighter asynchronously.
Expand All @@ -24,14 +24,14 @@ export function ClientCodeBlock(props: ClientBlockProps) {
const blockRef = useRef<HTMLDivElement>(null);
const isInViewportRef = useRef(false);
const [isInViewport, setIsInViewport] = useState(false);
const plainLines = useMemo(() => plainHighlight(block, []), [block]);
const plainLines = useMemo(() => plainHighlight({ block, inlines: [] }), [block]);
const [lines, setLines] = useState<null | HighlightLine[]>(null);
const [highlighting, setHighlighting] = useState(false);

// Preload the highlighter when the block is mounted.
useEffect(() => {
import('./highlight').then(({ preloadHighlight }) => preloadHighlight(block));
}, [block]);
import('./highlight').then(({ preloadHighlight }) => preloadHighlight(block.data.syntax));
}, [block.data.syntax]);

// When user scrolls, we need to wait for the scroll to finish before running the highlight
const isScrollingRef = useRef(false);
Expand Down Expand Up @@ -80,7 +80,7 @@ export function ClientCodeBlock(props: ClientBlockProps) {
if (typeof window !== 'undefined') {
setHighlighting(true);
import('./highlight').then(({ highlight }) => {
highlight(block, inlines).then((lines) => {
highlight({ block, inlines }).then((lines) => {
if (cancelled) {
return;
}
Expand All @@ -104,7 +104,9 @@ export function ClientCodeBlock(props: ClientBlockProps) {
<CodeBlockRenderer
ref={blockRef}
aria-busy={highlighting}
block={block}
title={props.title}
withLineNumbers={props.withLineNumbers}
withWrap={props.withWrap}
style={style}
lines={lines ?? plainLines}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { DocumentBlockCode } from '@gitbook/api';

import { getNodeFragmentByType } from '@/lib/document';
import { isV2 } from '@/lib/v2';

import type { SlimDocumentBlockCode } from '@/lib/slim-document';
import type { BlockProps } from '../Block';
import { Blocks } from '../Blocks';
import { ClientCodeBlock } from './ClientCodeBlock';
Expand All @@ -12,8 +11,11 @@ import { type RenderedInline, getInlines, highlight } from './highlight';
/**
* Render a code block, can be client-side or server-side.
*/
export async function CodeBlock(props: BlockProps<DocumentBlockCode>) {
export async function CodeBlock(props: BlockProps<SlimDocumentBlockCode>) {
const { block, document, style, isEstimatedOffscreen, context } = props;
const withLineNumbers = Boolean(block.data.lineNumbers) && block.nodes.length > 1;
const withWrap = block.data.overflow === 'wrap';
const title = block.data.title ?? '';
const inlines = getInlines(block);
const richInlines: RenderedInline[] = inlines.map((inline, index) => {
const body = (() => {
Expand All @@ -38,9 +40,29 @@ export async function CodeBlock(props: BlockProps<DocumentBlockCode>) {

if (isV2() && !isEstimatedOffscreen) {
// In v2, we render the code block server-side
const lines = await highlight(block, richInlines);
return <CodeBlockRenderer block={block} style={style} lines={lines} />;
const lines = await highlight({
inlines: richInlines,
block,
});
return (
<CodeBlockRenderer
withLineNumbers={withLineNumbers}
withWrap={withWrap}
title={title}
style={style}
lines={lines}
/>
);
}

return <ClientCodeBlock block={block} style={style} inlines={richInlines} />;
return (
<ClientCodeBlock
block={block}
withLineNumbers={withLineNumbers}
withWrap={withWrap}
title={title}
style={style}
inlines={richInlines}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ import type { HighlightLine, HighlightToken } from './highlight';
import './theme.css';
import './CodeBlockRenderer.css';

type CodeBlockRendererProps = Pick<BlockProps<DocumentBlockCode>, 'block' | 'style'> & {
export interface CodeBlockRendererProps extends Pick<BlockProps<DocumentBlockCode>, 'style'> {
lines: HighlightLine[];
'aria-busy'?: boolean;
};
withLineNumbers: boolean;
withWrap: boolean;
title: string;
}

/**
* The logic of rendering a code block from lines.
Expand All @@ -24,12 +27,8 @@ export const CodeBlockRenderer = forwardRef(function CodeBlockRenderer(
props: CodeBlockRendererProps,
ref: React.ForwardedRef<HTMLDivElement>
) {
const { block, style, lines, 'aria-busy': ariaBusy } = props;

const { style, lines, withLineNumbers, withWrap, title, 'aria-busy': ariaBusy } = props;
const id = useId();
const withLineNumbers = Boolean(block.data.lineNumbers) && block.nodes.length > 1;
const withWrap = block.data.overflow === 'wrap';
const title = block.data.title;

return (
<div
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { DocumentBlockCode, JSONDocument } from '@gitbook/api';
import { useId } from 'react';

import type { SlimDocumentBlockCode, SlimJSONDocument } from '@/lib/slim-document';
import { CodeBlock } from './CodeBlock';

/**
Expand All @@ -11,7 +11,7 @@ export function PlainCodeBlock(props: { code: string; syntax: string }) {
const { code, syntax } = props;
const id = useId();

const block: DocumentBlockCode = {
const block: SlimDocumentBlockCode = {
key: id,
object: 'block',
type: 'code',
Expand All @@ -37,7 +37,7 @@ export function PlainCodeBlock(props: { code: string; syntax: string }) {
})),
};

const document: JSONDocument = {
const document: SlimJSONDocument = {
object: 'document',
data: {},
nodes: [block],
Expand Down
Loading