Compare commits

..

13 Commits

Author SHA1 Message Date
Samy Pessé 7375d3c597 Version Packages (#3645)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-12 11:37:22 +02:00
spastorelli 3548fa6dff Fix eval estree expr cjs named import when gitbook/expr is imported in playwright tests (#3643) 2025-09-12 11:20:33 +02:00
conico974 cb73040e0f Bump Next.js to version 15.4.0 (#3644)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-09-12 11:17:43 +02:00
Samy Pessé 872d36b64f Version Packages (#3629)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-11 18:06:31 +02:00
spastorelli e1ff17e655 Fix bundling of @gitbook/expr package (#3639) 2025-09-11 17:24:06 +02:00
Utku Ufuk f3010bd28a Stop using deprecated search API param (#3638) 2025-09-11 15:26:35 +03:00
spastorelli 8ff1e3b619 Port latest changes from gbx util-expr (#3636) 2025-09-09 18:55:41 +02:00
Viktor Renkema d7596bf454 Don't show toolbar when rendering within GitBook app preview (#3635) 2025-09-09 15:09:52 +02:00
Zeno Kapitein aea5eb10ae Persist language choice across sections (#3633) 2025-09-09 15:06:28 +02:00
Zeno Kapitein 1165a81cf5 Language selector fixes (#3632) 2025-09-08 17:22:42 +02:00
conico974 61d1a0192e Fix null contentRef handling in RecordColumnValue (#3566)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
2025-09-08 13:54:58 +02:00
Zeno Kapitein f9a2977621 Better handling for external link "mailto:" in Hovered Card in GBO (#3630) 2025-09-08 10:31:30 +00:00
spastorelli 24f601d594 Small optim in resolveTryItPrefillForOperation and remove uneeded dep (#3628) 2025-09-08 10:29:59 +02:00
38 changed files with 581 additions and 453 deletions
+230 -362
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -34,7 +34,7 @@
"workspaces": { "workspaces": {
"packages": ["packages/*"], "packages": ["packages/*"],
"catalog": { "catalog": {
"@gitbook/api": "^0.139.0", "@gitbook/api": "^0.140.0",
"bidc": "^0.0.2" "bidc": "^0.0.2"
} }
}, },
+16
View File
@@ -1,5 +1,21 @@
# @gitbook/expr # @gitbook/expr
## 1.1.1
### Patch Changes
- 3548fa6: Fix eval-estree-expr named import
## 1.1.0
### Minor Changes
- e1ff17e: Fix bundling of gitbook/expr package
### Patch Changes
- 8ff1e3b: Add support for every/some array methods
## 1.0.0 ## 1.0.0
### Major Changes ### Major Changes
+8 -5
View File
@@ -1,7 +1,7 @@
{ {
"name": "@gitbook/expr", "name": "@gitbook/expr",
"description": "Safely evaluate & parse user-defined GitBook expressions.", "description": "Safely evaluate & parse user-defined GitBook expressions.",
"version": "1.0.0", "version": "1.1.1",
"type": "module", "type": "module",
"exports": { "exports": {
".": { ".": {
@@ -12,23 +12,26 @@
}, },
"sideEffects": false, "sideEffects": false,
"dependencies": { "dependencies": {
"eval-estree-expression": "^2.0.3", "eval-estree-expression": "github:jonschlinkert/eval-estree-expression#9cf28d2",
"acorn": "^8.14.0", "acorn": "^8.14.0",
"acorn-loose": "8.4.0", "acorn-loose": "8.4.0",
"acorn-walk": "^8.3.4", "acorn-walk": "^8.3.4",
"escodegen": "^2.1.0",
"assert-never": "^1.2.1" "assert-never": "^1.2.1"
}, },
"devDependencies": { "devDependencies": {
"bun-types": "^1.1.20", "bun-types": "^1.1.20",
"tsdown": "^0.15.0",
"@types/estree": "^1.0.6", "@types/estree": "^1.0.6",
"@babel/types": "^7.26.0", "@babel/types": "^7.26.0",
"@types/json-schema": "^7.0.15" "@types/json-schema": "^7.0.15",
"@types/escodegen": "^0.0.10"
}, },
"scripts": { "scripts": {
"build": "tsc --project tsconfig.build.json", "build": "tsdown --project tsconfig.build.json",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"unit": "bun test", "unit": "bun test",
"clean": "rm -rf ./dist" "clean": "rm -rf ./dist"
}, },
"files": ["dist", "src", "README.md", "CHANGELOG.md"] "files": ["dist", "README.md", "CHANGELOG.md"]
} }
@@ -210,6 +210,8 @@ describe('autocomplete', () => {
'visitor.claims.hello.length', 'visitor.claims.hello.length',
'visitor.claims.hello.at', 'visitor.claims.hello.at',
'visitor.claims.hello.includes', 'visitor.claims.hello.includes',
'visitor.claims.hello.some',
'visitor.claims.hello.every',
], ],
}, },
}, },
@@ -268,6 +270,8 @@ describe('autocomplete', () => {
'visitor.claims.hello.length', 'visitor.claims.hello.length',
'visitor.claims.hello.at', 'visitor.claims.hello.at',
'visitor.claims.hello.includes', 'visitor.claims.hello.includes',
'visitor.claims.hello.some',
'visitor.claims.hello.every',
], ],
}, },
}, },
+20 -2
View File
@@ -42,11 +42,29 @@ describe('ExpressionRuntime', () => {
}, },
expectedResult: true, expectedResult: true,
}, },
{
scenario: 'array method',
condition: 'reviews.every(review => !!review.status)',
inputs: { reviews: [{ status: 'approved' }, { status: 'approved' }] },
expectedResult: true,
},
{
scenario: 'array every',
condition: 'reviews.every(review => review.status === "approved")',
inputs: { reviews: [{ status: 'approved' }, { status: 'approved' }] },
expectedResult: true,
},
{
scenario: 'array map',
condition: '[1, 2, 3].map(n => n * x)',
inputs: { x: 2 },
expectedResult: [2, 4, 6],
},
])( ])(
'should properly evaluate/safeEvaluate a valid conditional expression: $scenario', 'should properly evaluate/safeEvaluate a valid conditional expression: $scenario',
({ condition, inputs, expectedResult }) => { ({ condition, inputs, expectedResult }) => {
expect(runtime.evaluate(condition, inputs)).toBe(expectedResult); expect(runtime.evaluate(condition, inputs)).toEqual(expectedResult);
expect(runtime.safeEvaluate(condition, inputs).value).toBe(expectedResult); expect(runtime.safeEvaluate(condition, inputs).value).toEqual(expectedResult);
} }
); );
-1
View File
@@ -1,6 +1,5 @@
export * from './errors'; export * from './errors';
export * from './input-values'; export * from './input-values';
export * from './input-values';
export * from './runtime'; export * from './runtime';
export * from './symbols'; export * from './symbols';
export * from './template'; export * from './template';
+4 -1
View File
@@ -9,7 +9,9 @@ import {
tokenizer, tokenizer,
} from 'acorn'; } from 'acorn';
import { parse as parseLoose } from 'acorn-loose'; import { parse as parseLoose } from 'acorn-loose';
import { evaluate } from 'eval-estree-expression'; import escodegen from 'escodegen';
import evalESTreeExpr from 'eval-estree-expression';
const { evaluate } = evalESTreeExpr;
import { AutoComplete } from './autocomplete'; import { AutoComplete } from './autocomplete';
import { ExpressionError } from './errors'; import { ExpressionError } from './errors';
@@ -49,6 +51,7 @@ export class ExpressionRuntime {
return evaluate.sync<Expression>(parsed.result, inputs, { return evaluate.sync<Expression>(parsed.result, inputs, {
functions: true, functions: true,
withMembers: true, withMembers: true,
generate: escodegen.generate,
}); });
} catch (error) { } catch (error) {
throw error instanceof Error throw error instanceof Error
@@ -338,6 +338,8 @@ describe('ExpressionRuntime', () => {
'visitor.claims.hello.length', 'visitor.claims.hello.length',
'visitor.claims.hello.at', 'visitor.claims.hello.at',
'visitor.claims.hello.includes', 'visitor.claims.hello.includes',
'visitor.claims.hello.some',
'visitor.claims.hello.every',
], ],
}); });
}); });
+54
View File
@@ -259,6 +259,60 @@ const StandardLibrary: Partial<
'true if the value searchElement is found within the array (or the part of the array indicated by the index fromIndex, if specified).', 'true if the value searchElement is found within the array (or the part of the array indicated by the index fromIndex, if specified).',
}), }),
}), }),
SymbolFunction({
name: 'some',
description:
'Tests whether at least one element in the array passes the test implemented by the provided function.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some',
args: [
SymbolFunction({
name: 'callback',
description: 'A function that tests each element of the array.',
args: [
{
...arraySymbolDef.items,
name: 'element',
description: 'The current element being processed in the array.',
},
],
returns: SymbolBoolean({
description:
'true if the callback function returns a truthy value for at least one element in the array.',
}),
}),
],
returns: SymbolBoolean({
description:
'true if the callback function returns a truthy value for at least one element in the array.',
}),
}),
SymbolFunction({
name: 'every',
description:
'Tests whether all elements in the array pass the test implemented by the provided function.',
link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every',
args: [
SymbolFunction({
name: 'callback',
description: 'A function that tests each element of the array.',
args: [
{
...arraySymbolDef.items,
name: 'element',
description: 'The current element being processed in the array.',
},
],
returns: SymbolBoolean({
description:
'true if the callback function returns a truthy value for all elements in the array.',
}),
}),
],
returns: SymbolBoolean({
description:
'true if the callback function returns a truthy value for all elements in the array.',
}),
}),
], ],
}), }),
}; };
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from 'tsdown';
export default defineConfig([
{
entry: ['src/index.ts'],
dts: true,
format: ['esm'],
},
]);
+1 -1
View File
@@ -14,7 +14,7 @@ declare module 'eval-estree-expression' {
/** /**
* Enable support for function statements and expressions by enabling the functions option AND by passing the .generate() function from the escodegen library. Default: undefined * Enable support for function statements and expressions by enabling the functions option AND by passing the .generate() function from the escodegen library. Default: undefined
*/ */
generate?: boolean; generate?: boolean | ((node: any) => string);
/** /**
* Enable the =~ regex operator to support testing values without using functions (example name =~ /^a.*c$/). Default: true * Enable the =~ regex operator to support testing values without using functions (example name =~ /^a.*c$/). Default: true
*/ */
+17
View File
@@ -1,5 +1,22 @@
# gitbook # gitbook
## 0.17.2
### Patch Changes
- @gitbook/react-openapi@1.4.2
## 0.17.1
### Patch Changes
- 24f601d: Small optim in resolveTryItPrefillForOperation
- aea5eb1: Persist language choice across sections if possible
- 1165a81: Language selector edge cases
- f9a2977: Better handling for external link "mailto:" in Hovered Card in GBO
- Updated dependencies [24f601d]
- @gitbook/react-openapi@1.4.1
## 0.17.0 ## 0.17.0
### Minor Changes ### Minor Changes
@@ -17,7 +17,8 @@
"dev": { "dev": {
"vars": { "vars": {
"STAGE": "dev", "STAGE": "dev",
"OPEN_NEXT_REQUEST_ID_HEADER": "true" "OPEN_NEXT_REQUEST_ID_HEADER": "true",
"GITBOOK_URL": "http://localhost:8771"
}, },
"r2_buckets": [ "r2_buckets": [
{ {
+2 -3
View File
@@ -1,13 +1,12 @@
{ {
"name": "gitbook", "name": "gitbook",
"version": "0.17.0", "version": "0.17.2",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@gitbook/api": "catalog:", "@gitbook/api": "catalog:",
"@gitbook/browser-types": "workspace:*", "@gitbook/browser-types": "workspace:*",
"@gitbook/cache-tags": "workspace:*", "@gitbook/cache-tags": "workspace:*",
"@gitbook/colors": "workspace:*", "@gitbook/colors": "workspace:*",
"@gitbook/expr": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*", "@gitbook/emoji-codepoints": "workspace:*",
"@gitbook/fonts": "workspace:*", "@gitbook/fonts": "workspace:*",
"@gitbook/icons": "workspace:*", "@gitbook/icons": "workspace:*",
@@ -47,7 +46,7 @@
"memoizee": "^0.4.17", "memoizee": "^0.4.17",
"micromark-extension-frontmatter": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0",
"micromark-extension-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0",
"next": "15.3.5", "next": "15.4.0",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"nuqs": "^2.2.3", "nuqs": "^2.2.3",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
@@ -1,11 +1,10 @@
import type { GitBookSiteContext } from '@/lib/context'; import type { GitBookSiteContext } from '@/lib/context';
import { Icon } from '@gitbook/icons'; import { Icon } from '@gitbook/icons';
import { headers } from 'next/headers';
import React from 'react'; import React from 'react';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
import { DateRelative } from '../primitives'; import { DateRelative } from '../primitives';
import { IframeWrapper } from './IframeWrapper';
import { RefreshChangeRequestButton } from './RefreshChangeRequestButton'; import { RefreshChangeRequestButton } from './RefreshChangeRequestButton';
import { Toolbar, ToolbarBody, ToolbarButton, ToolbarButtonGroups } from './Toolbar'; import { Toolbar, ToolbarBody, ToolbarButton, ToolbarButtonGroups } from './Toolbar';
@@ -47,19 +46,21 @@ function ToolbarLayout(props: { children: React.ReactNode }) {
*/ */
export async function AdminToolbar(props: AdminToolbarProps) { export async function AdminToolbar(props: AdminToolbarProps) {
const { context } = props; const { context } = props;
const mode = (await headers()).get('x-gitbook-mode');
if (mode === 'multi-id') {
// We don't show the admin toolbar in multi-id mode, as it's used for previewing in the dashboard.
return null;
}
if (context.changeRequest) { if (context.changeRequest) {
return <ChangeRequestToolbar context={context} />; return (
<IframeWrapper>
<ChangeRequestToolbar context={context} />
</IframeWrapper>
);
} }
if (context.revisionId !== context.space.revision) { if (context.revisionId !== context.space.revision) {
return <RevisionToolbar context={context} />; return (
<IframeWrapper>
<RevisionToolbar context={context} />
</IframeWrapper>
);
} }
return null; return null;
@@ -0,0 +1,27 @@
'use client';
import React from 'react';
interface IframeWrapperProps {
children: React.ReactNode;
}
/**
* Client component that detects if we're in an iframe and conditionally renders children
*/
export function IframeWrapper({ children }: IframeWrapperProps) {
const [isInIframe, setIsInIframe] = React.useState(false);
React.useEffect(() => {
// Check if we're running inside an iframe
const inIframe = window !== window.parent;
setIsInIframe(inIframe);
}, []);
// Don't render children if we're in an iframe (GitBook app preview)
if (isInIframe) {
return null;
}
return children;
}
@@ -1 +1,2 @@
export * from './AdminToolbar'; export * from './AdminToolbar';
export * from './IframeWrapper';
@@ -46,6 +46,7 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
); );
} }
const isExternal = inline.data.ref.kind === 'url'; const isExternal = inline.data.ref.kind === 'url';
const isMailto = resolved.href.startsWith('mailto:');
const content = ( const content = (
<StyledLink <StyledLink
href={resolved.href} href={resolved.href}
@@ -63,7 +64,12 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
nodes={inline.nodes} nodes={inline.nodes}
ancestorInlines={[...ancestorInlines, inline]} ancestorInlines={[...ancestorInlines, inline]}
/> />
{isExternal ? ( {isMailto ? (
<Icon
icon="envelope"
className="ml-1 inline size-3 links-accent:text-tint-subtle"
/>
) : isExternal ? (
<Icon <Icon
icon="arrow-up-right" icon="arrow-up-right"
className="ml-0.5 inline size-3 links-accent:text-tint-subtle" className="ml-0.5 inline size-3 links-accent:text-tint-subtle"
@@ -96,16 +102,24 @@ function InlineLinkTooltipWrapper(props: {
const { inline, language, resolved, children } = props; const { inline, language, resolved, children } = props;
let breadcrumbs = resolved.ancestors ?? []; let breadcrumbs = resolved.ancestors ?? [];
const isMailto = resolved.href.startsWith('mailto:');
const isExternal = inline.data.ref.kind === 'url'; const isExternal = inline.data.ref.kind === 'url';
const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined; const isSamePage = inline.data.ref.kind === 'anchor' && inline.data.ref.page === undefined;
if (isExternal) {
if (isMailto) {
resolved.text = resolved.text.split('mailto:')[1] ?? resolved.text;
breadcrumbs = [
{
label: tString(language, 'link_tooltip_email'),
},
];
} else if (isExternal) {
breadcrumbs = [ breadcrumbs = [
{ {
label: tString(language, 'link_tooltip_external_link'), label: tString(language, 'link_tooltip_external_link'),
}, },
]; ];
} } else if (isSamePage) {
if (isSamePage) {
breadcrumbs = [ breadcrumbs = [
{ {
label: tString(language, 'link_tooltip_page_anchor'), label: tString(language, 'link_tooltip_page_anchor'),
@@ -15,12 +15,17 @@ import { getSimplifiedContentType } from '@/lib/files';
import { resolveContentRef } from '@/lib/references'; import { resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
import { filterOutNullable } from '@/lib/typescript'; import { filterOutNullable } from '@/lib/typescript';
import type { BlockProps } from '../Block'; import type { BlockProps } from '../Block';
import { Blocks } from '../Blocks'; import { Blocks } from '../Blocks';
import { FileIcon } from '../FileIcon'; import { FileIcon } from '../FileIcon';
import type { TableRecordKV } from './Table'; import type { TableRecordKV } from './Table';
import { type VerticalAlignment, getColumnAlignment } from './utils'; import {
type VerticalAlignment,
getColumnAlignment,
isContentRef,
isDocumentTableImageRecord,
isStringArray,
} from './utils';
const alignmentMap: Record<'text-left' | 'text-center' | 'text-right', string> = { const alignmentMap: Record<'text-left' | 'text-center' | 'text-right', string> = {
'text-left': '**:text-left text-left', 'text-left': '**:text-left text-left',
@@ -58,18 +63,28 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
return null; return null;
} }
// Because definition and value depends on column, we have to check typing in each case at runtime.
// Validation should have been done at the API level, but we can't know typing based on `definition.type`.
// OpenAPI types cannot really handle discriminated unions based on a dynamic key.
switch (definition.type) { switch (definition.type) {
case 'checkbox': case 'checkbox': {
if (value === null || typeof value !== 'boolean') {
return null;
}
return ( return (
<Checkbox <Checkbox
className={tcls('w-5', 'h-5')} className={tcls('w-5', 'h-5')}
checked={value as boolean} checked={value}
disabled={true} disabled={true}
aria-labelledby={ariaLabelledBy} aria-labelledby={ariaLabelledBy}
/> />
); );
}
case 'rating': { case 'rating': {
const rating = value as number; if (typeof value !== 'number') {
return null;
}
const rating = value;
const max = definition.max; const max = definition.max;
return ( return (
@@ -108,15 +123,21 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
</Tag> </Tag>
); );
} }
case 'number': case 'number': {
if (typeof value !== 'number') {
return null;
}
return ( return (
<Tag <Tag
className={tcls('text-base', 'tabular-nums', 'tracking-tighter')} className={tcls('text-base', 'tabular-nums', 'tracking-tighter')}
aria-labelledby={ariaLabelledBy} aria-labelledby={ariaLabelledBy}
>{`${value}`}</Tag> >{`${value}`}</Tag>
); );
}
case 'text': { case 'text': {
// @ts-ignore if (typeof value !== 'string') {
return null;
}
const fragment = getNodeFragmentByName(block, value); const fragment = getNodeFragmentByName(block, value);
if (!fragment) { if (!fragment) {
return <Tag className={tcls(['w-full', verticalAlignment])}>{''}</Tag>; return <Tag className={tcls(['w-full', verticalAlignment])}>{''}</Tag>;
@@ -149,8 +170,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
); );
} }
case 'files': { case 'files': {
if (!isStringArray(value)) {
return null;
}
const files = await Promise.all( const files = await Promise.all(
(value as string[]).map((fileId) => value.map((fileId) =>
context.contentContext context.contentContext
? resolveContentRef( ? resolveContentRef(
{ {
@@ -221,10 +245,12 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
); );
} }
case 'content-ref': { case 'content-ref': {
const contentRef = value ? (value as ContentRef) : null; if (value === null || !isContentRef(value)) {
return null;
}
const resolved = const resolved =
contentRef && context.contentContext value && context.contentContext
? await resolveContentRef(contentRef, context.contentContext, { ? await resolveContentRef(value, context.contentContext, {
resolveAnchorText: true, resolveAnchorText: true,
iconStyle: ['mr-2', 'text-tint-subtle'], iconStyle: ['mr-2', 'text-tint-subtle'],
}) })
@@ -239,11 +265,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
<StyledLink <StyledLink
href={resolved.href} href={resolved.href}
insights={ insights={
contentRef value
? { ? {
type: 'link_click', type: 'link_click',
link: { link: {
target: contentRef, target: value,
position: SiteInsightsLinkPosition.Content, position: SiteInsightsLinkPosition.Content,
}, },
} }
@@ -257,8 +283,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
); );
} }
case 'users': { case 'users': {
if (!isStringArray(value)) {
return null;
}
const resolved = await Promise.all( const resolved = await Promise.all(
(value as string[]).map(async (userId) => { value.map(async (userId) => {
const contentRef: ContentRefUser = { const contentRef: ContentRefUser = {
kind: 'user', kind: 'user',
user: userId, user: userId,
@@ -295,10 +324,13 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
); );
} }
case 'select': { case 'select': {
if (!isStringArray(value)) {
return null;
}
return ( return (
<Tag aria-labelledby={ariaLabelledBy}> <Tag aria-labelledby={ariaLabelledBy}>
<span className={tcls('inline-flex', 'gap-2', 'flex-wrap')}> <span className={tcls('inline-flex', 'gap-2', 'flex-wrap')}>
{(value as string[]).map((selectId) => { {value.map((selectId) => {
const option = definition.options.find( const option = definition.options.find(
(option) => option.value === selectId (option) => option.value === selectId
); );
@@ -329,8 +361,15 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
); );
} }
case 'image': { case 'image': {
if (!isDocumentTableImageRecord(value)) {
return null;
}
const image = context.contentContext const image = context.contentContext
? await resolveContentRef(value as ContentRef, context.contentContext) ? await resolveContentRef(
'ref' in value ? value.ref : value,
context.contentContext
)
: null; : null;
if (!image) { if (!image) {
@@ -123,3 +123,35 @@ export function getColumnVerticalAlignment(column: DocumentTableDefinition): Ver
return 'self-center'; return 'self-center';
} }
/**
* Check if a value is a ContentRef.
* @param ref The value to check.
* @returns True if the value is a ContentRef, false otherwise.
*/
export function isContentRef(ref?: DocumentTableRecord['values'][string]): ref is ContentRef {
return Boolean(ref && typeof ref === 'object' && 'kind' in ref);
}
/**
* Check if a value is an array of strings.
* @param value The value to check.
* @returns True if the value is an array of strings, false otherwise.
*/
export function isStringArray(value?: DocumentTableRecord['values'][string]): value is string[] {
return Array.isArray(value) && value.every((v) => typeof v === 'string');
}
/**
* Check if a value is a DocumentTableImageRecord.
* @param value The value to check.
* @returns True if the value is a DocumentTableImageRecord, false otherwise.
*/
export function isDocumentTableImageRecord(
value?: DocumentTableRecord['values'][string]
): value is DocumentTableImageRecord {
if (isContentRef(value) && (value.kind === 'file' || value.kind === 'url')) {
return true;
}
return Boolean(value && typeof value === 'object' && 'ref' in value && isContentRef(value.ref));
}
@@ -80,13 +80,14 @@ export function Header(props: {
> >
<HeaderMobileMenu <HeaderMobileMenu
className={tcls( className={tcls(
'lg:hidden',
'-ml-2', '-ml-2',
'text-tint-strong', 'text-tint-strong',
'theme-bold:text-header-link', 'theme-bold:text-header-link',
'hover:bg-tint-hover', 'hover:bg-tint-hover',
'hover:theme-bold:bg-header-link/3', 'hover:theme-bold:bg-header-link/3',
withVariants === 'generic' ? '' : 'page-no-toc:hidden' withVariants === 'generic'
? 'xl:hidden'
: 'page-no-toc:hidden lg:hidden'
)} )}
/> />
<HeaderLogo context={context} /> <HeaderLogo context={context} />
@@ -123,8 +123,7 @@ export async function streamAskQuestion({
}, },
scope: { scope: {
mode: 'default', mode: 'default',
// Include the current site space regardless. currentSiteSpace: context.siteSpace.id,
includedSiteSpaces: [context.siteSpace.id],
}, },
}, },
{ format: 'document' } { format: 'document' }
@@ -30,9 +30,7 @@ export function encodeClientSiteSections(context: GitBookSiteContext, sections:
for (const item of list) { for (const item of list) {
switch (item.object) { switch (item.object) {
case 'site-section-group': { case 'site-section-group': {
const sections = item.sections const sections = item.sections.map((section) => encodeSection(context, section));
.filter((section) => shouldIncludeSection(context, section))
.map((section) => encodeSection(context, section));
// Skip empty groups // Skip empty groups
if (sections.length === 0) { if (sections.length === 0) {
@@ -74,38 +72,13 @@ function encodeSection(context: GitBookSiteContext, section: SiteSection) {
}; };
} }
/**
* Test if a section should be included in the list of sections.
*/
function shouldIncludeSection(context: GitBookSiteContext, section: SiteSection) {
if (context.site.id !== 'site_JOVzv') {
return true;
}
// Testing for a new mode of navigation where the multi-variants section are hidden
// if they do not include an equivalent of the current site space.
// TODO: replace with a proper flag on the section
const withNavigateOnlyIfEquivalent = section.id === 'sitesc_4jvEm';
if (!withNavigateOnlyIfEquivalent) {
return true;
}
const { siteSpace: currentSiteSpace } = context;
if (section.siteSpaces.length === 1) {
return true;
}
return section.siteSpaces.some((siteSpace) =>
areSiteSpacesEquivalent(siteSpace, currentSiteSpace)
);
}
/** /**
* Find the best default site space to navigate to for a givent section: * Find the best default site space to navigate to for a givent section:
* 1. If we are on the default, continue on the default. * 1. If we are on the default, continue on the default.
* 2. If a site space has the same path as the current one, return it. * 2. If there are site spaces with the same language as the current, filter by language.
* 3. Otherwise, return the default one. * 3. If a site space has the same path as the current one, return it.
* 4. Otherwise, return the default first language match.
* 5. Otherwise, return the default one.
*/ */
function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) { function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) {
const { siteSpace: currentSiteSpace } = context; const { siteSpace: currentSiteSpace } = context;
@@ -114,9 +87,15 @@ function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) {
return getSectionURL(context, section); return getSectionURL(context, section);
} }
const bestMatch = section.siteSpaces.find((siteSpace) => const possibleMatches =
areSiteSpacesEquivalent(siteSpace, currentSiteSpace) section.siteSpaces.filter((siteSpace) =>
); areSiteSpacesSameLanguage(siteSpace, currentSiteSpace)
) ?? section.siteSpaces;
const bestMatch =
possibleMatches.find((siteSpace) => areSiteSpacesEquivalent(siteSpace, currentSiteSpace)) ??
possibleMatches[0];
if (bestMatch) { if (bestMatch) {
return getSiteSpaceURL(context, bestMatch); return getSiteSpaceURL(context, bestMatch);
} }
@@ -130,3 +109,7 @@ function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) {
function areSiteSpacesEquivalent(siteSpace1: SiteSpace, siteSpace2: SiteSpace) { function areSiteSpacesEquivalent(siteSpace1: SiteSpace, siteSpace2: SiteSpace) {
return siteSpace1.path === siteSpace2.path; return siteSpace1.path === siteSpace2.path;
} }
function areSiteSpacesSameLanguage(siteSpace1: SiteSpace, siteSpace2: SiteSpace) {
return siteSpace1.space.language === siteSpace2.space.language;
}
@@ -20,7 +20,7 @@ import type { RenderAIMessageOptions } from '../AI';
import { AIChat } from '../AIChat'; import { AIChat } from '../AIChat';
import { AdaptiveVisitorContextProvider } from '../Adaptive'; import { AdaptiveVisitorContextProvider } from '../Adaptive';
import { Announcement } from '../Announcement'; import { Announcement } from '../Announcement';
import { SpacesDropdown } from '../Header/SpacesDropdown'; import { SpacesDropdown, TranslationsDropdown } from '../Header/SpacesDropdown';
import { InsightsProvider } from '../Insights'; import { InsightsProvider } from '../Insights';
import { SearchContainer } from '../Search'; import { SearchContainer } from '../Search';
import { SiteSectionList, encodeClientSiteSections } from '../SiteSections'; import { SiteSectionList, encodeClientSiteSections } from '../SiteSections';
@@ -156,12 +156,20 @@ export function SpaceLayout(props: SpaceLayoutProps) {
'pr-4', 'pr-4',
'lg:flex', 'lg:flex',
'grow-0', 'grow-0',
'flex-wrap',
'dark:shadow-light/1', 'dark:shadow-light/1',
'text-base/tight' 'text-base/tight',
'items-center'
)} )}
> >
<HeaderLogo context={context} /> <HeaderLogo context={context} />
{withVariants === 'translations' ? (
<TranslationsDropdown
context={context}
siteSpace={siteSpace}
siteSpaces={siteSpaces}
className="[&_.button-leading-icon]:block! ml-auto py-2 [&_.button-content]:hidden"
/>
) : null}
</div> </div>
) )
} }
@@ -68,6 +68,7 @@ export const de = {
pdf_limit_reached_continue: 'Mit ${1} weiteren Seiten erweitern.', pdf_limit_reached_continue: 'Mit ${1} weiteren Seiten erweitern.',
more: 'Mehr', more: 'Mehr',
link_tooltip_external_link: 'Externe Verlinkung zu', link_tooltip_external_link: 'Externe Verlinkung zu',
link_tooltip_email: 'E-Mail senden an',
link_tooltip_page_anchor: 'Zum Abschnitt springen', link_tooltip_page_anchor: 'Zum Abschnitt springen',
open_in_new_tab: 'In neuem Tab öffnen', open_in_new_tab: 'In neuem Tab öffnen',
ai_chat_assistant_name: 'GitBook-Assistent', ai_chat_assistant_name: 'GitBook-Assistent',
@@ -65,6 +65,7 @@ export const en = {
pdf_limit_reached_continue: 'Extend with ${1} more pages.', pdf_limit_reached_continue: 'Extend with ${1} more pages.',
more: 'More', more: 'More',
link_tooltip_external_link: 'External link to', link_tooltip_external_link: 'External link to',
link_tooltip_email: 'Send an email to',
link_tooltip_page_anchor: 'Jump to section', link_tooltip_page_anchor: 'Jump to section',
open_in_new_tab: 'Open in new tab', open_in_new_tab: 'Open in new tab',
ai_chat_assistant_name: 'GitBook Assistant', ai_chat_assistant_name: 'GitBook Assistant',
@@ -69,6 +69,7 @@ export const es: TranslationLanguage = {
pdf_limit_reached_continue: 'Extender con ${1} páginas más.', pdf_limit_reached_continue: 'Extender con ${1} páginas más.',
more: 'Más', more: 'Más',
link_tooltip_external_link: 'Enlace externo a', link_tooltip_external_link: 'Enlace externo a',
link_tooltip_email: 'Enviar un correo electrónico a',
link_tooltip_page_anchor: 'Saltar a la sección', link_tooltip_page_anchor: 'Saltar a la sección',
open_in_new_tab: 'Abrir en una nueva pestaña', open_in_new_tab: 'Abrir en una nueva pestaña',
ai_chat_assistant_name: 'Asistente de GitBook', ai_chat_assistant_name: 'Asistente de GitBook',
@@ -64,6 +64,7 @@ export const fr = {
pdf_limit_reached_continue: 'Ajouter ${1} pages supplémentaires', pdf_limit_reached_continue: 'Ajouter ${1} pages supplémentaires',
more: 'Plus', more: 'Plus',
link_tooltip_external_link: 'Lien externe vers', link_tooltip_external_link: 'Lien externe vers',
link_tooltip_email: 'Envoyer un e-mail à',
link_tooltip_page_anchor: 'Aller à la section', link_tooltip_page_anchor: 'Aller à la section',
open_in_new_tab: 'Ouvrir dans un nouvel onglet', open_in_new_tab: 'Ouvrir dans un nouvel onglet',
ai_chat_assistant_name: 'Assistant GitBook', ai_chat_assistant_name: 'Assistant GitBook',
@@ -67,6 +67,7 @@ export const ja: TranslationLanguage = {
pdf_limit_reached_continue: 'さらに${1}ページで拡張', pdf_limit_reached_continue: 'さらに${1}ページで拡張',
more: '詳細', more: '詳細',
link_tooltip_external_link: '外部リンク先', link_tooltip_external_link: '外部リンク先',
link_tooltip_email: 'メールを送信',
link_tooltip_page_anchor: 'ページ内リンク先', link_tooltip_page_anchor: 'ページ内リンク先',
open_in_new_tab: '新しいタブで開く', open_in_new_tab: '新しいタブで開く',
ai_chat_assistant_name: 'GitBookアシスタント', ai_chat_assistant_name: 'GitBookアシスタント',
@@ -67,6 +67,7 @@ export const nl: TranslationLanguage = {
pdf_limit_reached_continue: "Verleng met ${1} extra pagina's.", pdf_limit_reached_continue: "Verleng met ${1} extra pagina's.",
more: 'Meer', more: 'Meer',
link_tooltip_external_link: 'Externe link naar', link_tooltip_external_link: 'Externe link naar',
link_tooltip_email: 'E-mail versturen naar',
link_tooltip_page_anchor: 'Spring naar sectie', link_tooltip_page_anchor: 'Spring naar sectie',
open_in_new_tab: 'Open in nieuw tabblad', open_in_new_tab: 'Open in nieuw tabblad',
ai_chat_assistant_name: 'GitBook Assistent', ai_chat_assistant_name: 'GitBook Assistent',
@@ -68,6 +68,7 @@ export const no: TranslationLanguage = {
pdf_limit_reached_continue: 'Utvid med ${1} flere sider.', pdf_limit_reached_continue: 'Utvid med ${1} flere sider.',
more: 'Mer', more: 'Mer',
link_tooltip_external_link: 'Ekstern lenke til', link_tooltip_external_link: 'Ekstern lenke til',
link_tooltip_email: 'Send e-post til',
link_tooltip_page_anchor: 'Hopp til seksjon', link_tooltip_page_anchor: 'Hopp til seksjon',
open_in_new_tab: 'Åpne i ny fane', open_in_new_tab: 'Åpne i ny fane',
ai_chat_assistant_name: 'GitBook-assistent', ai_chat_assistant_name: 'GitBook-assistent',
@@ -67,6 +67,7 @@ export const pt_br = {
pdf_limit_reached_continue: 'Extender com mais ${1} páginas.', pdf_limit_reached_continue: 'Extender com mais ${1} páginas.',
more: 'Mais', more: 'Mais',
link_tooltip_external_link: 'Link externo para', link_tooltip_external_link: 'Link externo para',
link_tooltip_email: 'Enviar e-mail para',
link_tooltip_page_anchor: 'Pular para a seção', link_tooltip_page_anchor: 'Pular para a seção',
open_in_new_tab: 'Abrir em uma nova guia', open_in_new_tab: 'Abrir em uma nova guia',
ai_chat_assistant_name: 'Assistente do GitBook', ai_chat_assistant_name: 'Assistente do GitBook',
@@ -66,6 +66,7 @@ export const ru = {
pdf_limit_reached_continue: 'Расширьте ещё на ${1} страниц.', pdf_limit_reached_continue: 'Расширьте ещё на ${1} страниц.',
more: 'Ещё', more: 'Ещё',
link_tooltip_external_link: 'Внешняя ссылка на', link_tooltip_external_link: 'Внешняя ссылка на',
link_tooltip_email: 'Отправить письмо на',
link_tooltip_page_anchor: 'Перейти к разделу', link_tooltip_page_anchor: 'Перейти к разделу',
open_in_new_tab: 'Открыть в новой вкладке', open_in_new_tab: 'Открыть в новой вкладке',
ai_chat_assistant_name: 'GitBook-помощник', ai_chat_assistant_name: 'GitBook-помощник',
@@ -65,6 +65,7 @@ export const zh: TranslationLanguage = {
pdf_limit_reached_continue: '使用${1}页进行扩展。', pdf_limit_reached_continue: '使用${1}页进行扩展。',
more: '更多', more: '更多',
link_tooltip_external_link: '外部链接到', link_tooltip_external_link: '外部链接到',
link_tooltip_email: '发送邮件到',
link_tooltip_page_anchor: '跳转到页面', link_tooltip_page_anchor: '跳转到页面',
open_in_new_tab: '在新标签页中打开', open_in_new_tab: '在新标签页中打开',
ai_chat_assistant_name: 'GitBook 助手', ai_chat_assistant_name: 'GitBook 助手',
+16
View File
@@ -1,5 +1,21 @@
# @gitbook/react-openapi # @gitbook/react-openapi
## 1.4.2
### Patch Changes
- Updated dependencies [3548fa6]
- @gitbook/expr@1.1.1
## 1.4.1
### Patch Changes
- 24f601d: Small optim in resolveTryItPrefillForOperation
- Updated dependencies [e1ff17e]
- Updated dependencies [8ff1e3b]
- @gitbook/expr@1.1.0
## 1.4.0 ## 1.4.0
### Minor Changes ### Minor Changes
+1 -1
View File
@@ -8,7 +8,7 @@
"default": "./dist/index.js" "default": "./dist/index.js"
} }
}, },
"version": "1.4.0", "version": "1.4.2",
"sideEffects": false, "sideEffects": false,
"dependencies": { "dependencies": {
"@gitbook/expr": "workspace:*", "@gitbook/expr": "workspace:*",
@@ -27,12 +27,16 @@ export function resolveTryItPrefillForOperation(args: {
prefillInputContext, prefillInputContext,
} = args; } = args;
// Fixed ExpressionRuntime and resolveTryItPrefillExpression function if (!prefillInputContext) {
return {};
}
const runtime = new ExpressionRuntime(); const runtime = new ExpressionRuntime();
const resolveTryItPrefillExpression = (expr: string) => { const resolveTryItPrefillExpression = (expr: string) => {
if (!prefillInputContext) return undefined;
const parts = parseTemplate(expr); const parts = parseTemplate(expr);
if (!parts.length) return undefined; if (!parts.length) {
return undefined;
}
return runtime.evaluateTemplate(expr, prefillInputContext); return runtime.evaluateTemplate(expr, prefillInputContext);
}; };