From af98402655945a47473828cce1b8b33e4b858683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Mon, 16 Jun 2025 19:15:22 +0200 Subject: [PATCH 1/2] Add support for inline icons (#3329) --- .changeset/curly-rules-learn.md | 5 +++ bun.lock | 4 +-- package.json | 2 +- packages/gitbook/e2e/internal.spec.ts | 5 +++ .../DocumentView/CodeBlock/highlight.ts | 31 +++++++++++++------ .../src/components/DocumentView/Inline.tsx | 3 ++ .../components/DocumentView/InlineIcon.tsx | 10 ++++++ 7 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 .changeset/curly-rules-learn.md create mode 100644 packages/gitbook/src/components/DocumentView/InlineIcon.tsx diff --git a/.changeset/curly-rules-learn.md b/.changeset/curly-rules-learn.md new file mode 100644 index 000000000..47400e919 --- /dev/null +++ b/.changeset/curly-rules-learn.md @@ -0,0 +1,5 @@ +--- +"gitbook": minor +--- + +Add support for inline icons. diff --git a/bun.lock b/bun.lock index 1c192ca71..94b4fc3a5 100644 --- a/bun.lock +++ b/bun.lock @@ -285,7 +285,7 @@ "react-dom": "^19.0.0", }, "catalog": { - "@gitbook/api": "^0.120.0", + "@gitbook/api": "^0.121.0", }, "packages": { "@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="], @@ -650,7 +650,7 @@ "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="], - "@gitbook/api": ["@gitbook/api@0.120.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-FiRmPiSBwobMxmNjd14QkkOdM95BAPLDDRShgpS9Vsd8lHjNMyZfrJKVJTsJUuFcgYoi4cqNw9yu/TiUBUgv3g=="], + "@gitbook/api": ["@gitbook/api@0.121.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-o4/N24RM0Rg8S/3yPDjPmt6TbQF+1iZmg9q9QKxOxMqpQ2bZmMUqS7dSkeqEbEBMALx/m/x0xQlJbEJGbOwteg=="], "@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"], diff --git a/package.json b/package.json index 1faa750ba..1cf5326c1 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "workspaces": { "packages": ["packages/*"], "catalog": { - "@gitbook/api": "^0.120.0" + "@gitbook/api": "^0.121.0" } }, "patchedDependencies": { diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts index 3efe00bd2..3a884ae5e 100644 --- a/packages/gitbook/e2e/internal.spec.ts +++ b/packages/gitbook/e2e/internal.spec.ts @@ -603,6 +603,11 @@ const testCases: TestsCase[] = [ url: 'blocks/emojis', run: waitForCookiesDialog, }, + { + name: 'Icons', + url: 'blocks/icons', + run: waitForCookiesDialog, + }, { name: 'Links', url: 'blocks/links', diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts index eb02b5b92..89d00dafb 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/highlight.ts @@ -12,6 +12,7 @@ import { import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'; import { type BundledLanguage, bundledLanguages } from 'shiki/langs'; +import { nullIfNever } from '@/lib/typescript'; import { plainHighlight } from './plain-highlight'; export type HighlightLine = { @@ -263,16 +264,28 @@ function getPlainCodeBlockLine( if (node.object === 'text') { content += cleanupLine(node.leaves.map((leaf) => leaf.text).join('')); } else { - const start = index + content.length; - content += getPlainCodeBlockLine(node, index + content.length, inlines); - const end = index + content.length; + switch (node.type) { + case 'annotation': { + const start = index + content.length; + content += getPlainCodeBlockLine(node, index + content.length, inlines); + const end = index + content.length; - if (inlines) { - inlines.push({ - inline: node, - start, - end, - }); + if (inlines) { + inlines.push({ + inline: node, + start, + end, + }); + } + break; + } + case 'expression': { + break; + } + default: { + nullIfNever(node); + break; + } } } } diff --git a/packages/gitbook/src/components/DocumentView/Inline.tsx b/packages/gitbook/src/components/DocumentView/Inline.tsx index 1e1b091ce..84b56decf 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 { InlineIcon } from './InlineIcon'; import { InlineImage } from './InlineImage'; import { InlineLink } from './InlineLink'; import { InlineMath } from './Math'; @@ -47,6 +48,8 @@ export function Inline(props: InlineProps) { return ; case 'button': return ; + case 'icon': + return ; case 'expression': // The GitBook API should take care of evaluating expressions. // We should never need to render them. diff --git a/packages/gitbook/src/components/DocumentView/InlineIcon.tsx b/packages/gitbook/src/components/DocumentView/InlineIcon.tsx new file mode 100644 index 000000000..0eca373f6 --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/InlineIcon.tsx @@ -0,0 +1,10 @@ +import type { DocumentInlineIcon } from '@gitbook/api'; + +import { Icon, type IconName } from '@gitbook/icons'; +import type { InlineProps } from './Inline'; + +export async function InlineIcon(props: InlineProps) { + const { inline } = props; + + return ; +} From 11a6511b7aa7d1052c887417d97d4eede6104daa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Mon, 16 Jun 2025 20:06:33 +0200 Subject: [PATCH 2/2] Fix crash when integration script fails to render block (#3332) --- .changeset/famous-melons-compete.md | 5 +++ .changeset/orange-ears-drop.md | 5 +++ packages/gitbook-v2/src/lib/data/errors.ts | 34 ++++++++++++--- .../Integration/IntegrationBlock.tsx | 34 +++++++++------ .../DocumentView/Integration/render.ts | 31 ++++++++++++++ .../Integration/server-actions.tsx | 21 ++++++---- packages/react-contentkit/src/ContentKit.tsx | 41 ++++++++++++------- 7 files changed, 129 insertions(+), 42 deletions(-) create mode 100644 .changeset/famous-melons-compete.md create mode 100644 .changeset/orange-ears-drop.md create mode 100644 packages/gitbook/src/components/DocumentView/Integration/render.ts diff --git a/.changeset/famous-melons-compete.md b/.changeset/famous-melons-compete.md new file mode 100644 index 000000000..17512b6f4 --- /dev/null +++ b/.changeset/famous-melons-compete.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Fix crash when integration script fails to render block. diff --git a/.changeset/orange-ears-drop.md b/.changeset/orange-ears-drop.md new file mode 100644 index 000000000..12dd7fae5 --- /dev/null +++ b/.changeset/orange-ears-drop.md @@ -0,0 +1,5 @@ +--- +"@gitbook/react-contentkit": patch +--- + +Add basic error handling when transitioning between states. diff --git a/packages/gitbook-v2/src/lib/data/errors.ts b/packages/gitbook-v2/src/lib/data/errors.ts index 784eebdbb..4059fb5c9 100644 --- a/packages/gitbook-v2/src/lib/data/errors.ts +++ b/packages/gitbook-v2/src/lib/data/errors.ts @@ -47,11 +47,7 @@ export function getDataOrNull( return response.then((result) => getDataOrNull(result, ignoreErrors)); } - if (response.error) { - if (ignoreErrors.includes(response.error.code)) return null; - throw new DataFetcherError(response.error.message, response.error.code); - } - return response.data; + return ignoreDataFetcherErrors(response, ignoreErrors).data ?? null; } /** @@ -93,6 +89,34 @@ export async function wrapDataFetcherError( } } +/** + * Ignore some data fetcher errors. + */ +export function ignoreDataFetcherErrors( + response: DataFetcherResponse, + ignoreErrors?: number[] +): DataFetcherResponse; +export function ignoreDataFetcherErrors( + response: Promise>, + ignoreErrors?: number[] +): Promise>; +export function ignoreDataFetcherErrors( + response: DataFetcherResponse | Promise>, + ignoreErrors: number[] = [404] +): DataFetcherResponse | Promise> { + if (response instanceof Promise) { + return response.then((result) => ignoreDataFetcherErrors(result, ignoreErrors)); + } + + if (response.error) { + if (ignoreErrors.includes(response.error.code)) { + return response; + } + throw new DataFetcherError(response.error.message, response.error.code); + } + return response; +} + /** * Get a data fetcher exposable error from a JS error. */ diff --git a/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx b/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx index 6ebec6f03..a26aa3bfd 100644 --- a/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx +++ b/packages/gitbook/src/components/DocumentView/Integration/IntegrationBlock.tsx @@ -5,8 +5,8 @@ import { GITBOOK_INTEGRATIONS_HOST } from '@v2/lib/env'; import type { BlockProps } from '../Block'; import './contentkit.css'; -import { getDataOrNull } from '@v2/lib/data'; import { contentKitServerContext } from './contentkit'; +import { fetchSafeIntegrationUI } from './render'; import { renderIntegrationUi } from './server-actions'; export async function IntegrationBlock(props: BlockProps) { @@ -16,8 +16,6 @@ export async function IntegrationBlock(props: BlockProps +
+                    Unexpected error with integration {block.data.integration}:{' '}
+                    {initialResponse.error.message}
+                
+ + ); + } + const initialOutput = initialResponse.data; + if (initialOutput.type === 'complete') { return null; } diff --git a/packages/gitbook/src/components/DocumentView/Integration/render.ts b/packages/gitbook/src/components/DocumentView/Integration/render.ts new file mode 100644 index 000000000..a9a86c9e5 --- /dev/null +++ b/packages/gitbook/src/components/DocumentView/Integration/render.ts @@ -0,0 +1,31 @@ +import type { RenderIntegrationUI } from '@gitbook/api'; +import type { GitBookBaseContext } from '@v2/lib/context'; +import { ignoreDataFetcherErrors } from '@v2/lib/data'; + +/** + * Render an integration UI while ignoring some errors. + */ +export async function fetchSafeIntegrationUI( + context: GitBookBaseContext, + { + integrationName, + request, + }: { + integrationName: string; + request: RenderIntegrationUI; + } +) { + const output = await ignoreDataFetcherErrors( + context.dataFetcher.renderIntegrationUi({ + integrationName, + request, + }), + + // The API can respond with a 400 error if the integration is not installed + // and 404 if the integration is not found. + // The API can also respond with a 502 error if the integration is not generating a proper response. + [404, 400, 502] + ); + + return output; +} diff --git a/packages/gitbook/src/components/DocumentView/Integration/server-actions.tsx b/packages/gitbook/src/components/DocumentView/Integration/server-actions.tsx index 54180e5c8..91d1e0e73 100644 --- a/packages/gitbook/src/components/DocumentView/Integration/server-actions.tsx +++ b/packages/gitbook/src/components/DocumentView/Integration/server-actions.tsx @@ -4,9 +4,9 @@ import { getV1BaseContext } from '@/lib/v1'; import { isV2 } from '@/lib/v2'; import type { RenderIntegrationUI } from '@gitbook/api'; import { ContentKitOutput } from '@gitbook/react-contentkit'; -import { throwIfDataError } from '@v2/lib/data'; import { getServerActionBaseContext } from '@v2/lib/server-actions'; import { contentKitServerContext } from './contentkit'; +import { fetchSafeIntegrationUI } from './render'; /** * Server action to render an integration UI request from . @@ -22,16 +22,19 @@ export async function renderIntegrationUi({ request: RenderIntegrationUI; }) { const serverAction = isV2() ? await getServerActionBaseContext() : await getV1BaseContext(); + const output = await fetchSafeIntegrationUI(serverAction, { + integrationName: renderContext.integrationName, + request, + }); - const output = await throwIfDataError( - serverAction.dataFetcher.renderIntegrationUi({ - integrationName: renderContext.integrationName, - request, - }) - ); + if (output.error) { + return { + error: output.error.message, + }; + } return { - children: , - output: output, + children: , + output: output.data, }; } diff --git a/packages/react-contentkit/src/ContentKit.tsx b/packages/react-contentkit/src/ContentKit.tsx index 9d53c70de..4f413f367 100644 --- a/packages/react-contentkit/src/ContentKit.tsx +++ b/packages/react-contentkit/src/ContentKit.tsx @@ -38,10 +38,18 @@ export function ContentKit(props: { render: (input: { renderContext: RenderContext; request: RequestRenderIntegrationUI; - }) => Promise<{ - children: React.ReactNode; - output: ContentKitRenderOutput; - }>; + }) => Promise< + | { + error?: undefined; + children: React.ReactNode; + output: ContentKitRenderOutput; + } + | { + error: string; + children?: undefined; + output?: undefined; + } + >; /** Callback when an action is triggered */ onAction?: (action: ContentKitAction) => void; /** Callback when the flow is completed */ @@ -98,17 +106,18 @@ export function ContentKit(props: { request: newInput, }); const output = result.output; + if (output) { + if (output.type === 'complete') { + return onComplete?.(output.returnValue); + } - if (output.type === 'complete') { - return onComplete?.(output.returnValue); + setCurrent((prev) => ({ + input: newInput, + children: result.children, + output: output, + state: prev.state, + })); } - - setCurrent((prev) => ({ - input: newInput, - children: result.children, - output: output, - state: prev.state, - })); }, [setCurrent, current, render, onComplete] ); @@ -147,8 +156,10 @@ export function ContentKit(props: { renderContext, request: modalInput, }); - - if (result.output.type === 'element' || !result.output.type) { + if ( + result.output && + (result.output.type === 'element' || !result.output.type) + ) { setSubView({ mode: 'modal', initialInput: modalInput,