mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 14:00:28 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7375d3c597 | |||
| 3548fa6dff | |||
| cb73040e0f | |||
| 872d36b64f | |||
| e1ff17e655 | |||
| f3010bd28a | |||
| 8ff1e3b619 | |||
| d7596bf454 | |||
| aea5eb10ae | |||
| 1165a81cf5 | |||
| 61d1a0192e | |||
| f9a2977621 | |||
| 24f601d594 |
+1
-1
@@ -34,7 +34,7 @@
|
||||
"workspaces": {
|
||||
"packages": ["packages/*"],
|
||||
"catalog": {
|
||||
"@gitbook/api": "^0.139.0",
|
||||
"@gitbook/api": "^0.140.0",
|
||||
"bidc": "^0.0.2"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# @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
|
||||
|
||||
### Major Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@gitbook/expr",
|
||||
"description": "Safely evaluate & parse user-defined GitBook expressions.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.1",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
@@ -12,23 +12,26 @@
|
||||
},
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"eval-estree-expression": "^2.0.3",
|
||||
"eval-estree-expression": "github:jonschlinkert/eval-estree-expression#9cf28d2",
|
||||
"acorn": "^8.14.0",
|
||||
"acorn-loose": "8.4.0",
|
||||
"acorn-walk": "^8.3.4",
|
||||
"escodegen": "^2.1.0",
|
||||
"assert-never": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bun-types": "^1.1.20",
|
||||
"tsdown": "^0.15.0",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@babel/types": "^7.26.0",
|
||||
"@types/json-schema": "^7.0.15"
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@types/escodegen": "^0.0.10"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"build": "tsdown --project tsconfig.build.json",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"unit": "bun test",
|
||||
"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.at',
|
||||
'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.at',
|
||||
'visitor.claims.hello.includes',
|
||||
'visitor.claims.hello.some',
|
||||
'visitor.claims.hello.every',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -42,11 +42,29 @@ describe('ExpressionRuntime', () => {
|
||||
},
|
||||
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',
|
||||
({ condition, inputs, expectedResult }) => {
|
||||
expect(runtime.evaluate(condition, inputs)).toBe(expectedResult);
|
||||
expect(runtime.safeEvaluate(condition, inputs).value).toBe(expectedResult);
|
||||
expect(runtime.evaluate(condition, inputs)).toEqual(expectedResult);
|
||||
expect(runtime.safeEvaluate(condition, inputs).value).toEqual(expectedResult);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export * from './errors';
|
||||
export * from './input-values';
|
||||
export * from './input-values';
|
||||
export * from './runtime';
|
||||
export * from './symbols';
|
||||
export * from './template';
|
||||
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
tokenizer,
|
||||
} from 'acorn';
|
||||
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 { ExpressionError } from './errors';
|
||||
@@ -49,6 +51,7 @@ export class ExpressionRuntime {
|
||||
return evaluate.sync<Expression>(parsed.result, inputs, {
|
||||
functions: true,
|
||||
withMembers: true,
|
||||
generate: escodegen.generate,
|
||||
});
|
||||
} catch (error) {
|
||||
throw error instanceof Error
|
||||
|
||||
@@ -338,6 +338,8 @@ describe('ExpressionRuntime', () => {
|
||||
'visitor.claims.hello.length',
|
||||
'visitor.claims.hello.at',
|
||||
'visitor.claims.hello.includes',
|
||||
'visitor.claims.hello.some',
|
||||
'visitor.claims.hello.every',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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).',
|
||||
}),
|
||||
}),
|
||||
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.',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'tsdown';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
entry: ['src/index.ts'],
|
||||
dts: true,
|
||||
format: ['esm'],
|
||||
},
|
||||
]);
|
||||
+1
-1
@@ -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
|
||||
*/
|
||||
generate?: boolean;
|
||||
generate?: boolean | ((node: any) => string);
|
||||
/**
|
||||
* Enable the =~ regex operator to support testing values without using functions (example name =~ /^a.*c$/). Default: true
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# 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
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"dev": {
|
||||
"vars": {
|
||||
"STAGE": "dev",
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true"
|
||||
"OPEN_NEXT_REQUEST_ID_HEADER": "true",
|
||||
"GITBOOK_URL": "http://localhost:8771"
|
||||
},
|
||||
"r2_buckets": [
|
||||
{
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
{
|
||||
"name": "gitbook",
|
||||
"version": "0.17.0",
|
||||
"version": "0.17.2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@gitbook/api": "catalog:",
|
||||
"@gitbook/browser-types": "workspace:*",
|
||||
"@gitbook/cache-tags": "workspace:*",
|
||||
"@gitbook/colors": "workspace:*",
|
||||
"@gitbook/expr": "workspace:*",
|
||||
"@gitbook/emoji-codepoints": "workspace:*",
|
||||
"@gitbook/fonts": "workspace:*",
|
||||
"@gitbook/icons": "workspace:*",
|
||||
@@ -47,7 +46,7 @@
|
||||
"memoizee": "^0.4.17",
|
||||
"micromark-extension-frontmatter": "^2.0.0",
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"next": "15.3.5",
|
||||
"next": "15.4.0",
|
||||
"next-themes": "^0.2.1",
|
||||
"nuqs": "^2.2.3",
|
||||
"object-hash": "^3.0.0",
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { headers } from 'next/headers';
|
||||
import React from 'react';
|
||||
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { DateRelative } from '../primitives';
|
||||
import { IframeWrapper } from './IframeWrapper';
|
||||
import { RefreshChangeRequestButton } from './RefreshChangeRequestButton';
|
||||
import { Toolbar, ToolbarBody, ToolbarButton, ToolbarButtonGroups } from './Toolbar';
|
||||
|
||||
@@ -47,19 +46,21 @@ function ToolbarLayout(props: { children: React.ReactNode }) {
|
||||
*/
|
||||
export async function AdminToolbar(props: AdminToolbarProps) {
|
||||
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) {
|
||||
return <ChangeRequestToolbar context={context} />;
|
||||
return (
|
||||
<IframeWrapper>
|
||||
<ChangeRequestToolbar context={context} />
|
||||
</IframeWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (context.revisionId !== context.space.revision) {
|
||||
return <RevisionToolbar context={context} />;
|
||||
return (
|
||||
<IframeWrapper>
|
||||
<RevisionToolbar context={context} />
|
||||
</IframeWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
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 './IframeWrapper';
|
||||
|
||||
@@ -46,6 +46,7 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
||||
);
|
||||
}
|
||||
const isExternal = inline.data.ref.kind === 'url';
|
||||
const isMailto = resolved.href.startsWith('mailto:');
|
||||
const content = (
|
||||
<StyledLink
|
||||
href={resolved.href}
|
||||
@@ -63,7 +64,12 @@ export async function InlineLink(props: InlineProps<DocumentInlineLink>) {
|
||||
nodes={inline.nodes}
|
||||
ancestorInlines={[...ancestorInlines, inline]}
|
||||
/>
|
||||
{isExternal ? (
|
||||
{isMailto ? (
|
||||
<Icon
|
||||
icon="envelope"
|
||||
className="ml-1 inline size-3 links-accent:text-tint-subtle"
|
||||
/>
|
||||
) : isExternal ? (
|
||||
<Icon
|
||||
icon="arrow-up-right"
|
||||
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;
|
||||
|
||||
let breadcrumbs = resolved.ancestors ?? [];
|
||||
const isMailto = resolved.href.startsWith('mailto:');
|
||||
const isExternal = inline.data.ref.kind === 'url';
|
||||
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 = [
|
||||
{
|
||||
label: tString(language, 'link_tooltip_external_link'),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (isSamePage) {
|
||||
} else if (isSamePage) {
|
||||
breadcrumbs = [
|
||||
{
|
||||
label: tString(language, 'link_tooltip_page_anchor'),
|
||||
|
||||
@@ -15,12 +15,17 @@ import { getSimplifiedContentType } from '@/lib/files';
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { filterOutNullable } from '@/lib/typescript';
|
||||
|
||||
import type { BlockProps } from '../Block';
|
||||
import { Blocks } from '../Blocks';
|
||||
import { FileIcon } from '../FileIcon';
|
||||
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> = {
|
||||
'text-left': '**:text-left text-left',
|
||||
@@ -58,18 +63,28 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
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) {
|
||||
case 'checkbox':
|
||||
case 'checkbox': {
|
||||
if (value === null || typeof value !== 'boolean') {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Checkbox
|
||||
className={tcls('w-5', 'h-5')}
|
||||
checked={value as boolean}
|
||||
checked={value}
|
||||
disabled={true}
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'rating': {
|
||||
const rating = value as number;
|
||||
if (typeof value !== 'number') {
|
||||
return null;
|
||||
}
|
||||
const rating = value;
|
||||
const max = definition.max;
|
||||
|
||||
return (
|
||||
@@ -108,15 +123,21 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
case 'number':
|
||||
case 'number': {
|
||||
if (typeof value !== 'number') {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Tag
|
||||
className={tcls('text-base', 'tabular-nums', 'tracking-tighter')}
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
>{`${value}`}</Tag>
|
||||
);
|
||||
}
|
||||
case 'text': {
|
||||
// @ts-ignore
|
||||
if (typeof value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
const fragment = getNodeFragmentByName(block, value);
|
||||
if (!fragment) {
|
||||
return <Tag className={tcls(['w-full', verticalAlignment])}>{''}</Tag>;
|
||||
@@ -149,8 +170,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
);
|
||||
}
|
||||
case 'files': {
|
||||
if (!isStringArray(value)) {
|
||||
return null;
|
||||
}
|
||||
const files = await Promise.all(
|
||||
(value as string[]).map((fileId) =>
|
||||
value.map((fileId) =>
|
||||
context.contentContext
|
||||
? resolveContentRef(
|
||||
{
|
||||
@@ -221,10 +245,12 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
);
|
||||
}
|
||||
case 'content-ref': {
|
||||
const contentRef = value ? (value as ContentRef) : null;
|
||||
if (value === null || !isContentRef(value)) {
|
||||
return null;
|
||||
}
|
||||
const resolved =
|
||||
contentRef && context.contentContext
|
||||
? await resolveContentRef(contentRef, context.contentContext, {
|
||||
value && context.contentContext
|
||||
? await resolveContentRef(value, context.contentContext, {
|
||||
resolveAnchorText: true,
|
||||
iconStyle: ['mr-2', 'text-tint-subtle'],
|
||||
})
|
||||
@@ -239,11 +265,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
<StyledLink
|
||||
href={resolved.href}
|
||||
insights={
|
||||
contentRef
|
||||
value
|
||||
? {
|
||||
type: 'link_click',
|
||||
link: {
|
||||
target: contentRef,
|
||||
target: value,
|
||||
position: SiteInsightsLinkPosition.Content,
|
||||
},
|
||||
}
|
||||
@@ -257,8 +283,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
);
|
||||
}
|
||||
case 'users': {
|
||||
if (!isStringArray(value)) {
|
||||
return null;
|
||||
}
|
||||
const resolved = await Promise.all(
|
||||
(value as string[]).map(async (userId) => {
|
||||
value.map(async (userId) => {
|
||||
const contentRef: ContentRefUser = {
|
||||
kind: 'user',
|
||||
user: userId,
|
||||
@@ -295,10 +324,13 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
);
|
||||
}
|
||||
case 'select': {
|
||||
if (!isStringArray(value)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Tag aria-labelledby={ariaLabelledBy}>
|
||||
<span className={tcls('inline-flex', 'gap-2', 'flex-wrap')}>
|
||||
{(value as string[]).map((selectId) => {
|
||||
{value.map((selectId) => {
|
||||
const option = definition.options.find(
|
||||
(option) => option.value === selectId
|
||||
);
|
||||
@@ -329,8 +361,15 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
);
|
||||
}
|
||||
case 'image': {
|
||||
if (!isDocumentTableImageRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const image = context.contentContext
|
||||
? await resolveContentRef(value as ContentRef, context.contentContext)
|
||||
? await resolveContentRef(
|
||||
'ref' in value ? value.ref : value,
|
||||
context.contentContext
|
||||
)
|
||||
: null;
|
||||
|
||||
if (!image) {
|
||||
|
||||
@@ -123,3 +123,35 @@ export function getColumnVerticalAlignment(column: DocumentTableDefinition): Ver
|
||||
|
||||
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
|
||||
className={tcls(
|
||||
'lg:hidden',
|
||||
'-ml-2',
|
||||
'text-tint-strong',
|
||||
'theme-bold:text-header-link',
|
||||
'hover:bg-tint-hover',
|
||||
'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} />
|
||||
|
||||
@@ -123,8 +123,7 @@ export async function streamAskQuestion({
|
||||
},
|
||||
scope: {
|
||||
mode: 'default',
|
||||
// Include the current site space regardless.
|
||||
includedSiteSpaces: [context.siteSpace.id],
|
||||
currentSiteSpace: context.siteSpace.id,
|
||||
},
|
||||
},
|
||||
{ format: 'document' }
|
||||
|
||||
@@ -30,9 +30,7 @@ export function encodeClientSiteSections(context: GitBookSiteContext, sections:
|
||||
for (const item of list) {
|
||||
switch (item.object) {
|
||||
case 'site-section-group': {
|
||||
const sections = item.sections
|
||||
.filter((section) => shouldIncludeSection(context, section))
|
||||
.map((section) => encodeSection(context, section));
|
||||
const sections = item.sections.map((section) => encodeSection(context, section));
|
||||
|
||||
// Skip empty groups
|
||||
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:
|
||||
* 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.
|
||||
* 3. Otherwise, return the default one.
|
||||
* 2. If there are site spaces with the same language as the current, filter by language.
|
||||
* 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) {
|
||||
const { siteSpace: currentSiteSpace } = context;
|
||||
@@ -114,9 +87,15 @@ function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) {
|
||||
return getSectionURL(context, section);
|
||||
}
|
||||
|
||||
const bestMatch = section.siteSpaces.find((siteSpace) =>
|
||||
areSiteSpacesEquivalent(siteSpace, currentSiteSpace)
|
||||
);
|
||||
const possibleMatches =
|
||||
section.siteSpaces.filter((siteSpace) =>
|
||||
areSiteSpacesSameLanguage(siteSpace, currentSiteSpace)
|
||||
) ?? section.siteSpaces;
|
||||
|
||||
const bestMatch =
|
||||
possibleMatches.find((siteSpace) => areSiteSpacesEquivalent(siteSpace, currentSiteSpace)) ??
|
||||
possibleMatches[0];
|
||||
|
||||
if (bestMatch) {
|
||||
return getSiteSpaceURL(context, bestMatch);
|
||||
}
|
||||
@@ -130,3 +109,7 @@ function findBestTargetURL(context: GitBookSiteContext, section: SiteSection) {
|
||||
function areSiteSpacesEquivalent(siteSpace1: SiteSpace, siteSpace2: SiteSpace) {
|
||||
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 { AdaptiveVisitorContextProvider } from '../Adaptive';
|
||||
import { Announcement } from '../Announcement';
|
||||
import { SpacesDropdown } from '../Header/SpacesDropdown';
|
||||
import { SpacesDropdown, TranslationsDropdown } from '../Header/SpacesDropdown';
|
||||
import { InsightsProvider } from '../Insights';
|
||||
import { SearchContainer } from '../Search';
|
||||
import { SiteSectionList, encodeClientSiteSections } from '../SiteSections';
|
||||
@@ -156,12 +156,20 @@ export function SpaceLayout(props: SpaceLayoutProps) {
|
||||
'pr-4',
|
||||
'lg:flex',
|
||||
'grow-0',
|
||||
'flex-wrap',
|
||||
'dark:shadow-light/1',
|
||||
'text-base/tight'
|
||||
'text-base/tight',
|
||||
'items-center'
|
||||
)}
|
||||
>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ export const de = {
|
||||
pdf_limit_reached_continue: 'Mit ${1} weiteren Seiten erweitern.',
|
||||
more: 'Mehr',
|
||||
link_tooltip_external_link: 'Externe Verlinkung zu',
|
||||
link_tooltip_email: 'E-Mail senden an',
|
||||
link_tooltip_page_anchor: 'Zum Abschnitt springen',
|
||||
open_in_new_tab: 'In neuem Tab öffnen',
|
||||
ai_chat_assistant_name: 'GitBook-Assistent',
|
||||
|
||||
@@ -65,6 +65,7 @@ export const en = {
|
||||
pdf_limit_reached_continue: 'Extend with ${1} more pages.',
|
||||
more: 'More',
|
||||
link_tooltip_external_link: 'External link to',
|
||||
link_tooltip_email: 'Send an email to',
|
||||
link_tooltip_page_anchor: 'Jump to section',
|
||||
open_in_new_tab: 'Open in new tab',
|
||||
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.',
|
||||
more: 'Más',
|
||||
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',
|
||||
open_in_new_tab: 'Abrir en una nueva pestaña',
|
||||
ai_chat_assistant_name: 'Asistente de GitBook',
|
||||
|
||||
@@ -64,6 +64,7 @@ export const fr = {
|
||||
pdf_limit_reached_continue: 'Ajouter ${1} pages supplémentaires',
|
||||
more: 'Plus',
|
||||
link_tooltip_external_link: 'Lien externe vers',
|
||||
link_tooltip_email: 'Envoyer un e-mail à',
|
||||
link_tooltip_page_anchor: 'Aller à la section',
|
||||
open_in_new_tab: 'Ouvrir dans un nouvel onglet',
|
||||
ai_chat_assistant_name: 'Assistant GitBook',
|
||||
|
||||
@@ -67,6 +67,7 @@ export const ja: TranslationLanguage = {
|
||||
pdf_limit_reached_continue: 'さらに${1}ページで拡張',
|
||||
more: '詳細',
|
||||
link_tooltip_external_link: '外部リンク先',
|
||||
link_tooltip_email: 'メールを送信',
|
||||
link_tooltip_page_anchor: 'ページ内リンク先',
|
||||
open_in_new_tab: '新しいタブで開く',
|
||||
ai_chat_assistant_name: 'GitBookアシスタント',
|
||||
|
||||
@@ -67,6 +67,7 @@ export const nl: TranslationLanguage = {
|
||||
pdf_limit_reached_continue: "Verleng met ${1} extra pagina's.",
|
||||
more: 'Meer',
|
||||
link_tooltip_external_link: 'Externe link naar',
|
||||
link_tooltip_email: 'E-mail versturen naar',
|
||||
link_tooltip_page_anchor: 'Spring naar sectie',
|
||||
open_in_new_tab: 'Open in nieuw tabblad',
|
||||
ai_chat_assistant_name: 'GitBook Assistent',
|
||||
|
||||
@@ -68,6 +68,7 @@ export const no: TranslationLanguage = {
|
||||
pdf_limit_reached_continue: 'Utvid med ${1} flere sider.',
|
||||
more: 'Mer',
|
||||
link_tooltip_external_link: 'Ekstern lenke til',
|
||||
link_tooltip_email: 'Send e-post til',
|
||||
link_tooltip_page_anchor: 'Hopp til seksjon',
|
||||
open_in_new_tab: 'Åpne i ny fane',
|
||||
ai_chat_assistant_name: 'GitBook-assistent',
|
||||
|
||||
@@ -67,6 +67,7 @@ export const pt_br = {
|
||||
pdf_limit_reached_continue: 'Extender com mais ${1} páginas.',
|
||||
more: 'Mais',
|
||||
link_tooltip_external_link: 'Link externo para',
|
||||
link_tooltip_email: 'Enviar e-mail para',
|
||||
link_tooltip_page_anchor: 'Pular para a seção',
|
||||
open_in_new_tab: 'Abrir em uma nova guia',
|
||||
ai_chat_assistant_name: 'Assistente do GitBook',
|
||||
|
||||
@@ -66,6 +66,7 @@ export const ru = {
|
||||
pdf_limit_reached_continue: 'Расширьте ещё на ${1} страниц.',
|
||||
more: 'Ещё',
|
||||
link_tooltip_external_link: 'Внешняя ссылка на',
|
||||
link_tooltip_email: 'Отправить письмо на',
|
||||
link_tooltip_page_anchor: 'Перейти к разделу',
|
||||
open_in_new_tab: 'Открыть в новой вкладке',
|
||||
ai_chat_assistant_name: 'GitBook-помощник',
|
||||
|
||||
@@ -65,6 +65,7 @@ export const zh: TranslationLanguage = {
|
||||
pdf_limit_reached_continue: '使用${1}页进行扩展。',
|
||||
more: '更多',
|
||||
link_tooltip_external_link: '外部链接到',
|
||||
link_tooltip_email: '发送邮件到',
|
||||
link_tooltip_page_anchor: '跳转到页面',
|
||||
open_in_new_tab: '在新标签页中打开',
|
||||
ai_chat_assistant_name: 'GitBook 助手',
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# @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
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"version": "1.4.0",
|
||||
"version": "1.4.2",
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@gitbook/expr": "workspace:*",
|
||||
|
||||
@@ -27,12 +27,16 @@ export function resolveTryItPrefillForOperation(args: {
|
||||
prefillInputContext,
|
||||
} = args;
|
||||
|
||||
// Fixed ExpressionRuntime and resolveTryItPrefillExpression function
|
||||
if (!prefillInputContext) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const runtime = new ExpressionRuntime();
|
||||
const resolveTryItPrefillExpression = (expr: string) => {
|
||||
if (!prefillInputContext) return undefined;
|
||||
const parts = parseTemplate(expr);
|
||||
if (!parts.length) return undefined;
|
||||
if (!parts.length) {
|
||||
return undefined;
|
||||
}
|
||||
return runtime.evaluateTemplate(expr, prefillInputContext);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user