From 67ed8d69d61828b6bb12701f4b21776f9fa95304 Mon Sep 17 00:00:00 2001 From: Steeve Pastorelli Date: Wed, 17 Sep 2025 16:00:24 +0200 Subject: [PATCH 1/6] Support inline expressions rendering with visitor data in code block --- bun.lock | 1 + packages/gitbook/package.json | 1 + .../AdaptiveVisitorContextProvider.tsx | 13 +--- .../gitbook/src/components/Adaptive/index.ts | 2 + .../gitbook/src/components/Adaptive/types.ts | 9 +++ .../gitbook/src/components/Adaptive/utils.ts | 28 +++++++ .../CodeBlock/ClientCodeBlock.tsx | 36 +++++++-- .../DocumentView/CodeBlock/CodeBlock.tsx | 74 +++++++++++++------ .../DocumentView/CodeBlock/highlight.ts | 51 ++++++++++--- .../DocumentView/CodeBlock/plain-highlight.ts | 14 +++- .../InlineExpression/InlineExpression.tsx | 21 ++++++ .../InlineExpressionValue.tsx | 26 +++++++ .../DocumentView/InlineExpression/index.ts | 3 + .../DocumentView/InlineExpression/types.ts | 6 ++ .../useEvaluateInlineExpression.ts | 38 ++++++++++ .../src/components/PageBody/PageBody.tsx | 5 +- 16 files changed, 279 insertions(+), 49 deletions(-) create mode 100644 packages/gitbook/src/components/Adaptive/types.ts create mode 100644 packages/gitbook/src/components/Adaptive/utils.ts create mode 100644 packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpression.tsx create mode 100644 packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpressionValue.tsx create mode 100644 packages/gitbook/src/components/DocumentView/InlineExpression/index.ts create mode 100644 packages/gitbook/src/components/DocumentView/InlineExpression/types.ts create mode 100644 packages/gitbook/src/components/DocumentView/InlineExpression/useEvaluateInlineExpression.ts diff --git a/bun.lock b/bun.lock index a515522a67..cbe55eb984 100644 --- a/bun.lock +++ b/bun.lock @@ -102,6 +102,7 @@ "@gitbook/cache-tags": "workspace:*", "@gitbook/colors": "workspace:*", "@gitbook/emoji-codepoints": "workspace:*", + "@gitbook/expr": "workspace:*", "@gitbook/fonts": "workspace:*", "@gitbook/icons": "workspace:*", "@gitbook/openapi-parser": "workspace:*", diff --git a/packages/gitbook/package.json b/packages/gitbook/package.json index 56ccd22cfb..c9fb8b9b52 100644 --- a/packages/gitbook/package.json +++ b/packages/gitbook/package.json @@ -4,6 +4,7 @@ "private": true, "dependencies": { "@gitbook/api": "catalog:", + "@gitbook/expr": "workspace:*", "@gitbook/browser-types": "workspace:*", "@gitbook/cache-tags": "workspace:*", "@gitbook/colors": "workspace:*", diff --git a/packages/gitbook/src/components/Adaptive/AdaptiveVisitorContextProvider.tsx b/packages/gitbook/src/components/Adaptive/AdaptiveVisitorContextProvider.tsx index 917913f503..1edf650683 100644 --- a/packages/gitbook/src/components/Adaptive/AdaptiveVisitorContextProvider.tsx +++ b/packages/gitbook/src/components/Adaptive/AdaptiveVisitorContextProvider.tsx @@ -4,19 +4,14 @@ import type { GitBookSiteContext } from '@/lib/context'; import { OpenAPIPrefillContextProvider } from '@gitbook/react-openapi'; import * as React from 'react'; import { createContext, useContext } from 'react'; - -export type AdaptiveVisitorClaimsData = { - visitor: { - claims: Record & { unsigned: Record }; - }; -}; +import type { AdaptiveVisitorClaims } from './types'; /** * In-memory cache of visitor claim readers keyed by contextId. */ const adaptiveVisitorReaderCache = new Map< string, - ReturnType> + ReturnType> >(); function createResourceReader(promise: Promise) { @@ -52,7 +47,7 @@ function getAdaptiveVisitorClaimsReader(url: string, contextId: string) { if (!res.ok) { return null; } - return await res.json(); + return await res.json(); } catch { return null; } @@ -64,7 +59,7 @@ function getAdaptiveVisitorClaimsReader(url: string, contextId: string) { return reader; } -export type AdaptiveVisitorContextValue = () => AdaptiveVisitorClaimsData | null; +export type AdaptiveVisitorContextValue = () => AdaptiveVisitorClaims | null; const AdaptiveVisitorContext = createContext(() => null); diff --git a/packages/gitbook/src/components/Adaptive/index.ts b/packages/gitbook/src/components/Adaptive/index.ts index 0a6192d4c1..4b3df49b74 100644 --- a/packages/gitbook/src/components/Adaptive/index.ts +++ b/packages/gitbook/src/components/Adaptive/index.ts @@ -1 +1,3 @@ +export * from './types'; +export * from './utils'; export * from './AdaptiveVisitorContextProvider'; diff --git a/packages/gitbook/src/components/Adaptive/types.ts b/packages/gitbook/src/components/Adaptive/types.ts new file mode 100644 index 0000000000..f467643762 --- /dev/null +++ b/packages/gitbook/src/components/Adaptive/types.ts @@ -0,0 +1,9 @@ +export type AdaptiveVisitorClaimsData = Record & { + unsigned: Record; +}; + +export type AdaptiveVisitorClaims = { + visitor: { + claims: AdaptiveVisitorClaimsData; + }; +}; diff --git a/packages/gitbook/src/components/Adaptive/utils.ts b/packages/gitbook/src/components/Adaptive/utils.ts new file mode 100644 index 0000000000..304410a7a1 --- /dev/null +++ b/packages/gitbook/src/components/Adaptive/utils.ts @@ -0,0 +1,28 @@ +import type { Variables } from '@gitbook/api'; +import type { AdaptiveVisitorClaims } from './types'; + +/** + * Return an evaluation context to evaluate expressions. + */ +export function createExpressionEvaluationContext(args: { + visitorClaims: AdaptiveVisitorClaims | null; + variables: { + space?: Variables; + page?: Variables; + }; +}) { + const { visitorClaims, variables } = args; + return { + ...(visitorClaims ? visitorClaims : {}), + space: { + vars: variables.space ?? {}, + }, + ...(variables.page + ? { + page: { + vars: variables.page ?? {}, + }, + } + : {}), + }; +} diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx index ede23ee1d4..bf205df19b 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx @@ -1,30 +1,54 @@ 'use client'; import type { DocumentBlockCode } from '@gitbook/api'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { Suspense, useEffect, useMemo, useRef, useState } from 'react'; +import { type AdaptiveVisitorClaims, useAdaptiveVisitor } from '@/components/Adaptive'; import { useInViewportListener } from '@/components/hooks/useInViewportListener'; import { useScrollListener } from '@/components/hooks/useScrollListener'; import { useDebounceCallback } from 'usehooks-ts'; import type { BlockProps } from '../Block'; +import { type InlineExpressionVariables, useEvaluateInlineExpression } from '../InlineExpression'; import { CodeBlockRenderer } from './CodeBlockRenderer'; import type { HighlightLine, RenderedInline } from './highlight'; import { plainHighlight } from './plain-highlight'; type ClientBlockProps = Pick, 'block' | 'style'> & { inlines: RenderedInline[]; + inlineExprVariables: InlineExpressionVariables; }; +export function ClientCodeBlock(props: ClientBlockProps) { + const getAdaptiveVisitorClaims = useAdaptiveVisitor(); + const visitorClaims = getAdaptiveVisitorClaims(); + + return ( + + + + ); +} + /** * Render a code-block client-side by loading the highlighter asynchronously. * It allows us to defer some load to avoid blocking the rendering of the whole page with block highlighting. */ -export function ClientCodeBlock(props: ClientBlockProps) { - const { block, style, inlines } = props; +export function ClientCodeBlockWithVisitorClaims( + props: ClientBlockProps & { visitorClaims: AdaptiveVisitorClaims | null } +) { + const { block, style, inlines, inlineExprVariables, visitorClaims } = props; const blockRef = useRef(null); const isInViewportRef = useRef(false); const [isInViewport, setIsInViewport] = useState(false); - const plainLines = useMemo(() => plainHighlight(block, []), [block]); + + const evaluateInlineExpression = useEvaluateInlineExpression({ + visitorClaims, + variables: inlineExprVariables, + }); + const plainLines = useMemo( + () => plainHighlight(block, inlines, { evaluateInlineExpression }), + [block, inlines, evaluateInlineExpression] + ); const [lines, setLines] = useState(null); const [highlighting, setHighlighting] = useState(false); @@ -80,7 +104,7 @@ export function ClientCodeBlock(props: ClientBlockProps) { if (typeof window !== 'undefined') { setHighlighting(true); import('./highlight').then(({ highlight }) => { - highlight(block, inlines).then((lines) => { + highlight(block, inlines, { evaluateInlineExpression }).then((lines) => { if (cancelled) { return; } @@ -98,7 +122,7 @@ export function ClientCodeBlock(props: ClientBlockProps) { // Otherwise if the block is not in viewport, we reset to the plain lines setLines(null); - }, [isInViewport, block, inlines]); + }, [isInViewport, block, inlines, evaluateInlineExpression]); return ( ) { const { block, document, style, isEstimatedOffscreen, context } = props; const inlines = getInlines(block); - const richInlines: RenderedInline[] = inlines.map((inline, index) => { - const body = (() => { - const fragment = getNodeFragmentByType(inline.inline, 'annotation-body'); - if (!fragment) { - return null; + + let hasInlineExpression = false; + + const richInlines: RenderedInline[] = inlines + // Exclude inline expressions from rendered inline as they are rendered as code text once evaluated + // and so need to be treated as plain code tokens. + .filter((inline) => { + if (inline.inline.type === 'expression') { + hasInlineExpression = true; + return false; } - return ( - - ); - })(); - - return { inline, body }; - }); - - if (!isEstimatedOffscreen) { + return true; + }) + .map((inline, index) => { + const body = (() => { + const fragment = getNodeFragmentByType(inline.inline, 'annotation-body'); + if (!fragment) { + return null; + } + return ( + + ); + })(); + + return { inline, body }; + }); + + if (!isEstimatedOffscreen && !hasInlineExpression) { // In v2, we render the code block server-side const lines = await highlight(block, richInlines); return ; } - return ; + const variables = context.contentContext + ? { + space: context.contentContext?.revision.variables, + page: + 'page' in context.contentContext + ? context.contentContext.page.variables + : undefined, + } + : {}; + + return ( + + ); } diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts index 315d072e60..f59a5631e6 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts @@ -64,7 +64,10 @@ export async function preloadHighlight(block: DocumentBlockCode) { */ export async function highlight( block: DocumentBlockCode, - inlines: RenderedInline[] + inlines: RenderedInline[], + options?: { + evaluateInlineExpression?: (expr: string) => string; + } ): Promise { const langName = getBlockLang(block); @@ -74,10 +77,10 @@ export async function highlight( // - TEMP : language is PowerShell or C++ and browser is Safari: // RegExp#[Symbol.search] throws TypeError when `lastIndex` isn’t writable // Fixed in upcoming Safari 18.6, remove when it'll be released - RND-7772 - return plainHighlight(block, inlines); + return plainHighlight(block, inlines, options); } - const code = getPlainCodeBlock(block); + const code = getPlainCodeBlock(block, undefined, options); const highlighter = await getSingletonHighlighter({ langs: [langName], @@ -255,11 +258,17 @@ function matchTokenAndInlines( return result; } -function getPlainCodeBlock(code: DocumentBlockCode, inlines?: InlineIndexed[]): string { +function getPlainCodeBlock( + code: DocumentBlockCode, + inlines?: InlineIndexed[], + options?: { + evaluateInlineExpression?: (expr: string) => string; + } +): string { let content = ''; code.nodes.forEach((node, index) => { - const lineContent = getPlainCodeBlockLine(node, content.length, inlines); + const lineContent = getPlainCodeBlockLine(node, content.length, inlines, options); content += lineContent; if (index < code.nodes.length - 1) { @@ -273,7 +282,10 @@ function getPlainCodeBlock(code: DocumentBlockCode, inlines?: InlineIndexed[]): function getPlainCodeBlockLine( parent: DocumentBlockCodeLine | DocumentInlineAnnotation, index: number, - inlines?: InlineIndexed[] + inlines?: InlineIndexed[], + options?: { + evaluateInlineExpression?: (expr: string) => string; + } ): string { let content = ''; @@ -282,10 +294,13 @@ function getPlainCodeBlockLine( content += cleanupLine(node.leaves.map((leaf) => leaf.text).join('')); } else { switch (node.type) { - case 'annotation': { + case 'expression': { const start = index + content.length; - content += getPlainCodeBlockLine(node, index + content.length, inlines); - const end = index + content.length; + const exprValue = String( + options?.evaluateInlineExpression?.(node.data.expression) ?? '' + ); + content += exprValue; + const end = start + exprValue.length; if (inlines) { inlines.push({ @@ -296,7 +311,23 @@ function getPlainCodeBlockLine( } break; } - case 'expression': { + case 'annotation': { + const start = index + content.length; + content += getPlainCodeBlockLine( + node, + index + content.length, + inlines, + options + ); + const end = index + content.length; + + if (inlines) { + inlines.push({ + inline: node, + start, + end, + }); + } break; } default: { diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/plain-highlight.ts b/packages/gitbook/src/components/DocumentView/CodeBlock/plain-highlight.ts index 5247c14ddc..433742d5dc 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/plain-highlight.ts +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/plain-highlight.ts @@ -9,9 +9,13 @@ import type { HighlightLine, HighlightToken, RenderedInline } from './highlight' */ export function plainHighlight( block: DocumentBlockCode, - inlines: RenderedInline[] + inlines: RenderedInline[], + options?: { + evaluateInlineExpression?: (expr: string) => string; + } ): HighlightLine[] { const inlinesCopy = Array.from(inlines); + return block.nodes.map((lineBlock) => { const tokens: HighlightToken[] = lineBlock.nodes.map((node) => { if (node.object === 'text') { @@ -20,6 +24,14 @@ export function plainHighlight( content: getNodeText(node), }; } + + if (node.type === 'expression') { + return { + type: 'plain', + content: options?.evaluateInlineExpression?.(node.data.expression) ?? '', + }; + } + const inline = inlinesCopy.shift(); return { type: 'annotation', diff --git a/packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpression.tsx b/packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpression.tsx new file mode 100644 index 0000000000..77b55f2eda --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpression.tsx @@ -0,0 +1,21 @@ +import type { DocumentInlineExpression } from '@gitbook/api'; +import type { InlineProps } from '../Inline'; +import { InlineExpressionValue } from './InlineExpressionValue'; + +export function InlineExpression(props: InlineProps) { + const { context, inline } = props; + + const { data } = inline; + + const variables = context.contentContext + ? { + space: context.contentContext?.revision.variables, + page: + 'page' in context.contentContext + ? context.contentContext.page.variables + : undefined, + } + : {}; + + return ; +} diff --git a/packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpressionValue.tsx b/packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpressionValue.tsx new file mode 100644 index 0000000000..9684e667cd --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpressionValue.tsx @@ -0,0 +1,26 @@ +'use client'; +import { useAdaptiveVisitor } from '@/components/Adaptive'; +import { useMemo } from 'react'; +import type { InlineExpressionVariables } from './types'; +import { useEvaluateInlineExpression } from './useEvaluateInlineExpression'; + +export function InlineExpressionValue(props: { + expression: string; + variables: InlineExpressionVariables; +}) { + const { expression, variables } = props; + + const getAdaptiveVisitorClaims = useAdaptiveVisitor(); + const visitorClaims = getAdaptiveVisitorClaims(); + const evaluateInlineExpression = useEvaluateInlineExpression({ + visitorClaims, + variables, + }); + + const result = useMemo( + () => evaluateInlineExpression(expression), + [expression, evaluateInlineExpression] + ); + + return <>{result}; +} diff --git a/packages/gitbook/src/components/DocumentView/InlineExpression/index.ts b/packages/gitbook/src/components/DocumentView/InlineExpression/index.ts new file mode 100644 index 0000000000..59dec96a78 --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/InlineExpression/index.ts @@ -0,0 +1,3 @@ +export * from './InlineExpression'; +export * from './types'; +export * from './useEvaluateInlineExpression'; diff --git a/packages/gitbook/src/components/DocumentView/InlineExpression/types.ts b/packages/gitbook/src/components/DocumentView/InlineExpression/types.ts new file mode 100644 index 0000000000..831831b8b5 --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/InlineExpression/types.ts @@ -0,0 +1,6 @@ +import type { Variables } from '@gitbook/api'; + +export interface InlineExpressionVariables { + space?: Variables; + page?: Variables; +} diff --git a/packages/gitbook/src/components/DocumentView/InlineExpression/useEvaluateInlineExpression.ts b/packages/gitbook/src/components/DocumentView/InlineExpression/useEvaluateInlineExpression.ts new file mode 100644 index 0000000000..25e2b4b335 --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/InlineExpression/useEvaluateInlineExpression.ts @@ -0,0 +1,38 @@ +import * as React from 'react'; + +import { + type AdaptiveVisitorClaims, + createExpressionEvaluationContext, +} from '@/components/Adaptive'; +import type { Variables } from '@gitbook/api'; +import { ExpressionRuntime, formatExpressionResult } from '@gitbook/expr'; + +export function useEvaluateInlineExpression(args: { + visitorClaims: AdaptiveVisitorClaims | null; + variables: { + space?: Variables; + page?: Variables; + }; +}) { + const { visitorClaims, variables } = args; + const evaluateInlineExpression = React.useMemo(() => { + const runtime = new ExpressionRuntime(); + const evaluationContext = createExpressionEvaluationContext({ + visitorClaims, + variables, + }); + + return (expression: string) => { + try { + return formatExpressionResult( + runtime.evaluate(expression, evaluationContext) ?? '' + ); + } catch (err) { + console.error('Failed to evaluate expression:', expression, err); + return `{{${expression}}}`; + } + }; + }, [variables, visitorClaims]); + + return evaluateInlineExpression; +} diff --git a/packages/gitbook/src/components/PageBody/PageBody.tsx b/packages/gitbook/src/components/PageBody/PageBody.tsx index 0f9f4d4f95..4b674789a7 100644 --- a/packages/gitbook/src/components/PageBody/PageBody.tsx +++ b/packages/gitbook/src/components/PageBody/PageBody.tsx @@ -81,7 +81,10 @@ export function PageBody(props: { blockStyle="page-api-block:ml-0" context={{ mode: 'default', - contentContext: context, + contentContext: { + ...context, + page, + }, withLinkPreviews, }} /> From 25ae0783f2b1a079ec7db3761301f3b2bc5d1532 Mon Sep 17 00:00:00 2001 From: Steeve Pastorelli Date: Wed, 17 Sep 2025 16:01:34 +0200 Subject: [PATCH 2/6] Add support for inline expression rendering --- packages/gitbook/src/components/DocumentView/Inline.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/Inline.tsx b/packages/gitbook/src/components/DocumentView/Inline.tsx index 84b56decfc..01a722a72f 100644 --- a/packages/gitbook/src/components/DocumentView/Inline.tsx +++ b/packages/gitbook/src/components/DocumentView/Inline.tsx @@ -5,6 +5,7 @@ import { Annotation } from './Annotation/Annotation'; import type { DocumentContextProps } from './DocumentView'; import { Emoji } from './Emoji'; import { InlineButton } from './InlineButton'; +import { InlineExpression } from './InlineExpression'; import { InlineIcon } from './InlineIcon'; import { InlineImage } from './InlineImage'; import { InlineLink } from './InlineLink'; @@ -51,9 +52,7 @@ export function Inline(props: InlineProps) { case 'icon': return ; case 'expression': - // The GitBook API should take care of evaluating expressions. - // We should never need to render them. - return null; + return ; default: return nullIfNever(inline); } From 4a1d41a0354aedef6cb788120b5e4c970fe9f61a Mon Sep 17 00:00:00 2001 From: Steeve Pastorelli Date: Wed, 17 Sep 2025 16:06:06 +0200 Subject: [PATCH 3/6] Cleaner diff in getPlainCodeBlockLine --- .../DocumentView/CodeBlock/highlight.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts index f59a5631e6..e2c9d0a4d5 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts @@ -294,13 +294,15 @@ function getPlainCodeBlockLine( content += cleanupLine(node.leaves.map((leaf) => leaf.text).join('')); } else { switch (node.type) { - case 'expression': { + case 'annotation': { const start = index + content.length; - const exprValue = String( - options?.evaluateInlineExpression?.(node.data.expression) ?? '' + content += getPlainCodeBlockLine( + node, + index + content.length, + inlines, + options ); - content += exprValue; - const end = start + exprValue.length; + const end = index + content.length; if (inlines) { inlines.push({ @@ -311,15 +313,13 @@ function getPlainCodeBlockLine( } break; } - case 'annotation': { + case 'expression': { const start = index + content.length; - content += getPlainCodeBlockLine( - node, - index + content.length, - inlines, - options + const exprValue = String( + options?.evaluateInlineExpression?.(node.data.expression) ?? '' ); - const end = index + content.length; + content += exprValue; + const end = start + exprValue.length; if (inlines) { inlines.push({ From 9e8ebb3bf5de3ac54130b04a50853cd88cc6036a Mon Sep 17 00:00:00 2001 From: Steeve Pastorelli Date: Wed, 17 Sep 2025 16:08:43 +0200 Subject: [PATCH 4/6] Remove unecessary cast --- .../CodeBlock/ClientCodeBlock.tsx | 23 +++++-------------- .../DocumentView/CodeBlock/CodeBlock.tsx | 16 ++++++++----- .../DocumentView/CodeBlock/highlight.ts | 5 ++-- .../InlineExpression/InlineExpression.tsx | 8 ++++++- 4 files changed, 25 insertions(+), 27 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx index bf205df19b..9d8cfbb399 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/ClientCodeBlock.tsx @@ -1,9 +1,9 @@ 'use client'; import type { DocumentBlockCode } from '@gitbook/api'; -import { Suspense, useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; -import { type AdaptiveVisitorClaims, useAdaptiveVisitor } from '@/components/Adaptive'; +import { useAdaptiveVisitor } from '@/components/Adaptive'; import { useInViewportListener } from '@/components/hooks/useInViewportListener'; import { useScrollListener } from '@/components/hooks/useScrollListener'; import { useDebounceCallback } from 'usehooks-ts'; @@ -18,29 +18,18 @@ type ClientBlockProps = Pick, 'block' | 'style'> & inlineExprVariables: InlineExpressionVariables; }; -export function ClientCodeBlock(props: ClientBlockProps) { - const getAdaptiveVisitorClaims = useAdaptiveVisitor(); - const visitorClaims = getAdaptiveVisitorClaims(); - - return ( - - - - ); -} - /** * Render a code-block client-side by loading the highlighter asynchronously. * It allows us to defer some load to avoid blocking the rendering of the whole page with block highlighting. */ -export function ClientCodeBlockWithVisitorClaims( - props: ClientBlockProps & { visitorClaims: AdaptiveVisitorClaims | null } -) { - const { block, style, inlines, inlineExprVariables, visitorClaims } = props; +export function ClientCodeBlock(props: ClientBlockProps) { + const { block, style, inlines, inlineExprVariables } = props; const blockRef = useRef(null); const isInViewportRef = useRef(false); const [isInViewport, setIsInViewport] = useState(false); + const getAdaptiveVisitorClaims = useAdaptiveVisitor(); + const visitorClaims = getAdaptiveVisitorClaims(); const evaluateInlineExpression = useEvaluateInlineExpression({ visitorClaims, variables: inlineExprVariables, diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlock.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlock.tsx index 5596fa60e6..206cb82adb 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlock.tsx +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/CodeBlock.tsx @@ -1,3 +1,5 @@ +import * as React from 'react'; + import type { DocumentBlockCode } from '@gitbook/api'; import { getNodeFragmentByType } from '@/lib/document'; @@ -65,11 +67,13 @@ export async function CodeBlock(props: BlockProps) { : {}; return ( - + + + ); } diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts index e2c9d0a4d5..d20578fade 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts @@ -315,9 +315,8 @@ function getPlainCodeBlockLine( } case 'expression': { const start = index + content.length; - const exprValue = String( - options?.evaluateInlineExpression?.(node.data.expression) ?? '' - ); + const exprValue = + options?.evaluateInlineExpression?.(node.data.expression) ?? ''; content += exprValue; const end = start + exprValue.length; diff --git a/packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpression.tsx b/packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpression.tsx index 77b55f2eda..dba25beb6f 100644 --- a/packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpression.tsx +++ b/packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpression.tsx @@ -1,3 +1,5 @@ +import * as React from 'react'; + import type { DocumentInlineExpression } from '@gitbook/api'; import type { InlineProps } from '../Inline'; import { InlineExpressionValue } from './InlineExpressionValue'; @@ -17,5 +19,9 @@ export function InlineExpression(props: InlineProps) { } : {}; - return ; + return ( + + + + ); } From 19db00307b0b71fb2edacaa648e765883a732643 Mon Sep 17 00:00:00 2001 From: Steeve Pastorelli Date: Thu, 18 Sep 2025 09:48:10 +0200 Subject: [PATCH 5/6] Add comments --- .../DocumentView/InlineExpression/InlineExpression.tsx | 3 +++ .../InlineExpression/useEvaluateInlineExpression.ts | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpression.tsx b/packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpression.tsx index dba25beb6f..2c38f684b1 100644 --- a/packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpression.tsx +++ b/packages/gitbook/src/components/DocumentView/InlineExpression/InlineExpression.tsx @@ -4,6 +4,9 @@ import type { DocumentInlineExpression } from '@gitbook/api'; import type { InlineProps } from '../Inline'; import { InlineExpressionValue } from './InlineExpressionValue'; +/** + * Render an inline expression. + */ export function InlineExpression(props: InlineProps) { const { context, inline } = props; diff --git a/packages/gitbook/src/components/DocumentView/InlineExpression/useEvaluateInlineExpression.ts b/packages/gitbook/src/components/DocumentView/InlineExpression/useEvaluateInlineExpression.ts index 25e2b4b335..758c9b252a 100644 --- a/packages/gitbook/src/components/DocumentView/InlineExpression/useEvaluateInlineExpression.ts +++ b/packages/gitbook/src/components/DocumentView/InlineExpression/useEvaluateInlineExpression.ts @@ -7,6 +7,10 @@ import { import type { Variables } from '@gitbook/api'; import { ExpressionRuntime, formatExpressionResult } from '@gitbook/expr'; +/** + * Hook that returns a callback to evaluate an inline expression with visitor data + * and space/page variables as context. + */ export function useEvaluateInlineExpression(args: { visitorClaims: AdaptiveVisitorClaims | null; variables: { From c1ec32f14c2221a80b8b7d9764b9ffdd9f75b106 Mon Sep 17 00:00:00 2001 From: Steeve Pastorelli Date: Thu, 18 Sep 2025 10:31:59 +0200 Subject: [PATCH 6/6] Add changeset --- .changeset/little-months-exercise.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/little-months-exercise.md diff --git a/.changeset/little-months-exercise.md b/.changeset/little-months-exercise.md new file mode 100644 index 0000000000..a294ea7888 --- /dev/null +++ b/.changeset/little-months-exercise.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Add support for inline expressions rendering with visitor data on GBO side