Compare commits

...

6 Commits

Author SHA1 Message Date
Samy Pessé 304042017c Version Packages (#2847)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-02-20 08:36:09 +01:00
Greg Bergé 82cd9f2979 Add support for anchor link in OpenAPI blocks (#2858) 2025-02-19 23:38:20 +01:00
Greg Bergé a3f1fea27b Display OpenAPI header description (#2857) 2025-02-19 23:34:52 +01:00
Greg Bergé bb5c6a42e7 Support multiple examples and multiple responses example (#2856) 2025-02-19 20:39:15 +01:00
Valentino Hudhra 445baaaa61 Add a new packages @gitbook/colors (#2854)
Co-authored-by: Zeno Kapitein <zenomilan@me.com>
2025-02-19 17:17:39 +01:00
Nolann B. 7419ee7dba Show additional fields in OpenAPI block (#2851) 2025-02-19 17:01:15 +01:00
36 changed files with 670 additions and 227 deletions
-5
View File
@@ -1,5 +0,0 @@
---
'gitbook': patch
---
Fix opening external links when the GitBook page is embedded in an iframe
-7
View File
@@ -1,7 +0,0 @@
---
'@gitbook/openapi-parser': patch
'@gitbook/react-openapi': patch
'gitbook': patch
---
Improve Markdown parsing
+13
View File
@@ -22,6 +22,13 @@
"wrangler": "3.82.0",
},
},
"packages/colors": {
"name": "@gitbook/colors",
"version": "0.1.0",
"devDependencies": {
"typescript": "^5.5.3",
},
},
"packages/emoji-codepoints": {
"name": "@gitbook/emoji-codepoints",
"version": "0.2.0",
@@ -35,6 +42,7 @@
"dependencies": {
"@gitbook/api": "^0.93.0",
"@gitbook/cache-do": "workspace:*",
"@gitbook/colors": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
"@gitbook/icons": "workspace:*",
"@gitbook/openapi-parser": "workspace:*",
@@ -210,6 +218,7 @@
"@scalar/oas-utils": "^0.2.101",
"clsx": "^2.1.1",
"flatted": "^3.2.9",
"json-xml-parse": "^1.3.0",
"react-aria": "^3.37.0",
"react-aria-components": "^1.6.0",
"usehooks-ts": "^3.1.0",
@@ -594,6 +603,8 @@
"@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
"@gitbook/colors": ["@gitbook/colors@workspace:packages/colors"],
"@gitbook/emoji-codepoints": ["@gitbook/emoji-codepoints@workspace:packages/emoji-codepoints"],
"@gitbook/fontawesome-pro": ["@gitbook/fontawesome-pro@1.0.8", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "^6.6.0" } }, "sha512-i4PgiuGyUb52Muhc52kK3aMJIMfMkA2RbPW30tre8a6M8T6mWTfYo6gafSgjNvF1vH29zcuB8oBYnF0gO4XcHA=="],
@@ -2400,6 +2411,8 @@
"json-stringify-deterministic": ["json-stringify-deterministic@1.0.12", "", {}, "sha512-q3PN0lbUdv0pmurkBNdJH3pfFvOTL/Zp0lquqpvcjfKzt6Y0j49EPHAmVHCAS4Ceq/Y+PejWTzyiVpoY71+D6g=="],
"json-xml-parse": ["json-xml-parse@1.3.0", "", {}, "sha512-MVosauc/3W2wL4dd4yaJzH5oXw+HOUfptn0+d4+bFghMiJFop7MaqIwFXJNLiRnNYJNQ6L4o7B+53n5wcvoLFw=="],
"json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
"jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
+1
View File
@@ -0,0 +1 @@
dist/
+7
View File
@@ -0,0 +1,7 @@
# @gitbook/colors
## 0.2.0
### Minor Changes
- 445baaa: Initial release
+3
View File
@@ -0,0 +1,3 @@
# `@gitbook/colors`
A set of default colors and transformation functions used throughout the GitBook Open and app.
+26
View File
@@ -0,0 +1,26 @@
{
"name": "@gitbook/colors",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"development": "./src/index.ts",
"default": "./dist/index.js"
}
},
"version": "0.2.0",
"devDependencies": {
"typescript": "^5.5.3"
},
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit",
"dev": "tsc -w"
},
"files": [
"dist",
"src",
"README.md",
"CHANGELOG.md"
]
}
+39
View File
@@ -0,0 +1,39 @@
/**
* Default primary color throughout the GitBook ecosystem.
*/
export const DEFAULT_PRIMARY_COLOR = '#346DDB';
/**
* The darkest color that exists in GitBook, used as the relative minimum of every generated color scale.
*/
export const DARK_BASE = '#1D1D1D';
/**
* The lightest color that exists in GitBook, used as the relative maximum of every generated color scale.
*/
export const LIGHT_BASE = '#FFFFFF';
/**
* Used as the basis of all UI elements that are not colored by the primary color. Neutral gray by default, overridden by site customization.
*/
export const DEFAULT_TINT_COLOR = '#787878';
/**
* Used for informational messages and neutral alerts.
*/
export const DEFAULT_HINT_INFO_COLOR = '#787878';
/**
* Used for showing important information or non-critical warnings.
*/
export const DEFAULT_HINT_WARNING_COLOR = '#FE9A00';
/**
* Used for destructive actions or raising attention to critical information.
*/
export const DEFAULT_HINT_DANGER_COLOR = '#FB2C36';
/**
* Used for showing positive actions or achievements.
*/
export const DEFAULT_HINT_SUCCESS_COLOR = '#00C950';
+2
View File
@@ -0,0 +1,2 @@
export * from './colors';
export * from './transformations';
@@ -1,3 +1,5 @@
import { DARK_BASE, LIGHT_BASE, DEFAULT_TINT_COLOR } from './colors';
type ColorShades = {
[key: string]: string;
};
@@ -6,9 +8,6 @@ type RGBColor = [number, number, number];
type OKLABColor = { L: number; A: number; B: number };
type OKLCHColor = { L: number; C: number; H: number };
export const DARK_BASE = '#1d1d1d';
export const LIGHT_BASE = '#ffffff';
export const DEFAULT_TINT_COLOR = '#787878';
const D65 = [95.047, 100.0, 108.883]; // Reference white (D65)
export enum ColorCategory {
@@ -226,7 +225,7 @@ export function colorScale(
/**
* Convert a hex color to an RGB color set.
*/
function hexToRgbArray(hex: string): RGBColor {
export function hexToRgbArray(hex: string): RGBColor {
const originalHex = hex;
let value = hex.replace('#', '');
@@ -252,7 +251,7 @@ function hexToRgbArray(hex: string): RGBColor {
/**
* Convert a RGB color set to a hex color.
*/
function rgbArrayToHex(rgb: RGBColor): string {
export function rgbArrayToHex(rgb: RGBColor): string {
return `#${rgb
.map((channel) => {
const component = channel.toString(16);
@@ -262,7 +261,7 @@ function rgbArrayToHex(rgb: RGBColor): string {
.join('')}`;
}
function getColor(percentage: number, start: RGBColor, end: RGBColor) {
export function getColor(percentage: number, start: RGBColor, end: RGBColor) {
const rgb = end.map((channel, index) => {
return Math.round(channel + percentage * (start[index] - channel));
});
@@ -271,21 +270,21 @@ function getColor(percentage: number, start: RGBColor, end: RGBColor) {
}
// Utility constants and helper functions
function rgbToLinear(rgb: RGBColor): [number, number, number] {
export function rgbToLinear(rgb: RGBColor): [number, number, number] {
return rgb.map((v) => {
const scaled = v / 255;
return scaled <= 0.04045 ? scaled / 12.92 : ((scaled + 0.055) / 1.055) ** 2.4;
}) as [number, number, number];
}
function linearToRgb(linear: [number, number, number]): RGBColor {
export function linearToRgb(linear: [number, number, number]): RGBColor {
return linear.map((v) => {
const scaled = v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055;
return Math.round(Math.max(0, Math.min(1, scaled)) * 255);
}) as RGBColor;
}
function rgbToOklab(rgb: RGBColor): OKLABColor {
export function rgbToOklab(rgb: RGBColor): OKLABColor {
const [r, g, b] = rgbToLinear(rgb);
const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
@@ -303,7 +302,7 @@ function rgbToOklab(rgb: RGBColor): OKLABColor {
};
}
function oklabToRgb(oklab: OKLABColor): RGBColor {
export function oklabToRgb(oklab: OKLABColor): RGBColor {
const { L, A, B } = oklab;
const lRoot = L + 0.3963377774 * A + 0.2158037573 * B;
@@ -321,14 +320,14 @@ function oklabToRgb(oklab: OKLABColor): RGBColor {
return linearToRgb([r, g, b]);
}
function oklabToOklch(oklab: OKLABColor): OKLCHColor {
export function oklabToOklch(oklab: OKLABColor): OKLCHColor {
const { L, A, B } = oklab;
const C = Math.sqrt(A ** 2 + B ** 2);
const H = (Math.atan2(B, A) * 180) / Math.PI;
return { L, C, H: H < 0 ? H + 360 : H };
}
function oklchToOklab(oklch: OKLCHColor): OKLABColor {
export function oklchToOklab(oklch: OKLCHColor): OKLABColor {
const { L, C, H } = oklch;
const rad = (H * Math.PI) / 180;
return {
@@ -338,15 +337,15 @@ function oklchToOklab(oklch: OKLCHColor): OKLABColor {
};
}
function rgbToOklch(rgb: RGBColor): OKLCHColor {
export function rgbToOklch(rgb: RGBColor): OKLCHColor {
return oklabToOklch(rgbToOklab(rgb));
}
function oklchToRgb(oklch: OKLCHColor): RGBColor {
export function oklchToRgb(oklch: OKLCHColor): RGBColor {
return oklabToRgb(oklchToOklab(oklch));
}
function rgbToXyz(rgb: RGBColor): [number, number, number] {
export function rgbToXyz(rgb: RGBColor): [number, number, number] {
const [r, g, b] = rgbToLinear(rgb);
return [
(r * 0.4124564 + g * 0.3575761 + b * 0.1804375) * 100,
@@ -355,7 +354,11 @@ function rgbToXyz(rgb: RGBColor): [number, number, number] {
];
}
function xyzToLab65(xyz: [number, number, number]): { L: number; A: number; B: number } {
export function xyzToLab65(xyz: [number, number, number]): {
L: number;
A: number;
B: number;
} {
const [x, y, z] = xyz.map((v, i) => {
const scaled = v / D65[i];
return scaled > 0.008856 ? Math.cbrt(scaled) : 7.787 * scaled + 16 / 116;
@@ -368,7 +371,7 @@ function xyzToLab65(xyz: [number, number, number]): { L: number; A: number; B: n
};
}
function rgbTolab65(rgb: RGBColor): { L: number; A: number; B: number } {
export function rgbTolab65(rgb: RGBColor): { L: number; A: number; B: number } {
return xyzToLab65(rgbToXyz(rgb));
}
@@ -376,7 +379,7 @@ function rgbTolab65(rgb: RGBColor): { L: number; A: number; B: number } {
Delta Phi Star perceptual lightness contrast by Andrew Somers:
https://github.com/Myndex/deltaphistar
*/
const PHI = 0.5 + Math.sqrt(1.25);
export const PHI = 0.5 + Math.sqrt(1.25);
export function dpsContrast(a: RGBColor, b: RGBColor) {
const dps = Math.abs(rgbTolab65(a).L ** PHI - rgbTolab65(b).L ** PHI);
@@ -387,7 +390,10 @@ export function dpsContrast(a: RGBColor, b: RGBColor) {
export function colorContrast(background: string, foreground: string[] = [LIGHT_BASE, DARK_BASE]) {
const bg = hexToRgbArray(background);
const best: { color?: RGBColor; contrast: number } = { color: undefined, contrast: 0 };
const best: { color?: RGBColor; contrast: number } = {
color: undefined,
contrast: 0,
};
for (const color of foreground) {
const c = hexToRgbArray(color);
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": false,
"declaration": true,
"outDir": "dist",
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react",
"incremental": true,
"types": [
"bun-types" // add Bun global
]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}
+17
View File
@@ -1,5 +1,22 @@
# gitbook
## 0.6.2
### Patch Changes
- 359bb97: Fix opening external links when the GitBook page is embedded in an iframe
- 6157583: Improve Markdown parsing
- 82cd9f2: Add support for anchor links in OpenAPI blocks
- Updated dependencies [445baaa]
- Updated dependencies [bb5c6a4]
- Updated dependencies [a3f1fea]
- Updated dependencies [6157583]
- Updated dependencies [7419ee7]
- Updated dependencies [82cd9f2]
- @gitbook/colors@0.2.0
- @gitbook/react-openapi@1.0.2
- @gitbook/openapi-parser@1.0.1
## 0.6.1
### Patch Changes
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gitbook",
"version": "0.6.1",
"version": "0.6.2",
"private": true,
"scripts": {
"dev": "env-cmd --silent -f ../../.env.local next dev",
@@ -19,6 +19,7 @@
"dependencies": {
"@gitbook/api": "^0.93.0",
"@gitbook/cache-do": "workspace:*",
"@gitbook/colors": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
"@gitbook/icons": "workspace:*",
"@gitbook/openapi-parser": "workspace:*",
@@ -1,11 +1,11 @@
import { CustomizationHeaderPreset } from '@gitbook/api';
import { colorContrast } from '@gitbook/colors';
import { redirect } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { NextRequest } from 'next/server';
import React from 'react';
import { googleFontsMap } from '@/fonts';
import { colorContrast } from '@/lib/colors';
import { getAbsoluteHref } from '@/lib/links';
import { filterOutNullable } from '@/lib/typescript';
import { getContentTitle } from '@/lib/utils';
@@ -1,7 +1,7 @@
import { SiteInsightsAd } from '@gitbook/api';
import { hexToRgba } from '@gitbook/colors';
import * as React from 'react';
import { hexToRgba } from '@/lib/colors';
import { getResizedImageURL } from '@/lib/images';
import { tcls } from '@/lib/tailwind';
@@ -3,12 +3,12 @@ import { Icon } from '@gitbook/icons';
import { OpenAPIOperation } from '@gitbook/react-openapi';
import React from 'react';
import { LoadingPane } from '@/components/primitives';
import { fetchOpenAPIBlock } from '@/lib/openapi/fetch';
import { tcls } from '@/lib/tailwind';
import { BlockProps } from '../Block';
import { PlainCodeBlock } from '../CodeBlock';
import { Heading } from '../Heading';
import './style.css';
import './scalar.css';
@@ -54,6 +54,31 @@ async function OpenAPIBody(props: BlockProps<DocumentBlockOpenAPI>) {
plus: <Icon icon="plus" />,
},
CodeBlock: PlainCodeBlock,
renderHeading: (headingProps) => (
<Heading
document={props.document}
ancestorBlocks={props.ancestorBlocks}
isEstimatedOffscreen={props.isEstimatedOffscreen}
context={props.context}
style={headingProps.deprecated ? 'line-through' : undefined}
block={{
object: 'block',
key: `${block.key}-heading`,
meta: block.meta,
data: {},
type: 'heading-2',
nodes: [
{
key: `${block.key}-heading-text`,
object: 'text',
leaves: [
{ text: headingProps.title, object: 'leaf', marks: [] },
],
},
],
}}
/>
),
defaultInteractiveOpened: context.mode === 'print',
id: block.meta?.id,
blockKey: block.key,
@@ -62,32 +87,3 @@ async function OpenAPIBody(props: BlockProps<DocumentBlockOpenAPI>) {
/>
);
}
function OpenAPIFallback() {
return (
<div
role="status"
aria-busy
className={'openapi-block ' + tcls('flex', 'flex-1', 'flex-col', 'gap-3')}
>
<LoadingPane
tile={12}
style={['rounded-md', 'h-[47px]', '[max-width:calc(48rem-1px)]']}
/>
<LoadingPane
tile={12}
style={['rounded-md', 'h-[35px]', '[max-width:calc(48rem-1px)]']}
/>
<div className={tcls('flex', 'gap-[25px]')}>
<div className={tcls('flex', 'flex-1', 'flex-col', 'gap-3')}>
<LoadingPane tile={24} style={['rounded-md', 'aspect-[2.5/1]', 'w-full']} />
<LoadingPane tile={24} style={['rounded-md', 'aspect-[2.5/1]', 'w-full']} />
</div>
<div className={tcls('flex', 'flex-1', 'flex-col', 'gap-3')}>
<LoadingPane tile={24} style={['rounded-md', 'aspect-[4/1]', 'w-full']} />
<LoadingPane tile={24} style={['rounded-md', 'aspect-[4/1]', 'w-full']} />
</div>
</div>
</div>
);
}
@@ -1,7 +1,5 @@
/* Layout Components */
.openapi-operation {
content-visibility: auto;
contain-intrinsic-height: 600px;
@apply flex-1 flex flex-col gap-4 mb-14;
}
@@ -18,16 +16,8 @@
@apply flex flex-col items-start justify-start gap-2;
}
.openapi-summary-title {
@apply font-semibold text-xl;
}
.openapi-summary-title[data-deprecated='true'] {
@apply line-through;
}
.openapi-deprecated {
@apply py-0.5 px-1.5 min-w-[1.625rem] font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded-full text-sm leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none;
@apply py-0.5 px-1.5 min-w-[1.625rem] font-normal w-fit justify-center items-center ring-1 ring-inset ring-tint bg-tint rounded text-sm leading-[calc(max(1.20em,1.25rem))] before:!content-none after:!content-none;
}
.openapi-deprecated-sunset-date {
@@ -15,31 +15,29 @@ import { ClassValue, tcls } from '@/lib/tailwind';
export function Text(props: { text: DocumentText }) {
const { text } = props;
return (
<>
{text.leaves.map((leaf, index) => {
return (
<React.Fragment key={index}>
{leaf.marks
// Sort to have code marks at the end, so that they don't interfere with other marks
.sort(
(a, b) => (a.type === 'code' ? 1 : 0) - (b.type === 'code' ? 1 : 0),
)
.reduce<React.ReactNode>((children, mark, index) => {
const Mark = MARK_STYLES[mark.type];
return text.leaves.map((leaf, index) => {
return (
<React.Fragment key={index}>
{leaf.marks
// Sort to have code marks at the end, so that they don't interfere with other marks
.sort((a, b) => (a.type === 'code' ? 1 : 0) - (b.type === 'code' ? 1 : 0))
.reduce<React.ReactNode>((children, mark, index) => {
const Mark = MARK_STYLES[mark.type];
if (!Mark) {
return children;
}
if (!Mark) {
return children;
}
// @ts-ignore
return <Mark mark={mark}>{children}</Mark>;
}, leaf.text)}
</React.Fragment>
);
})}
</>
);
return (
// @ts-ignore
<Mark key="mark" mark={mark}>
{children}
</Mark>
);
}, leaf.text)}
</React.Fragment>
);
});
}
const MARK_STYLES = {
@@ -9,18 +9,18 @@ import {
type CustomizationTint,
type SiteCustomizationSettings,
} from '@gitbook/api';
import { IconsProvider, IconStyle } from '@gitbook/icons';
import { fontNotoColorEmoji, fonts, ibmPlexMono } from '@/fonts';
import { getSpaceLanguage } from '@/intl/server';
import { getStaticFileURL } from '@/lib/assets';
import {
colorContrast,
colorScale,
type ColorScaleOptions,
DEFAULT_TINT_COLOR,
hexToRgb,
} from '@/lib/colors';
} from '@gitbook/colors';
import { IconsProvider, IconStyle } from '@gitbook/icons';
import { fontNotoColorEmoji, fonts, ibmPlexMono } from '@/fonts';
import { getSpaceLanguage } from '@/intl/server';
import { getStaticFileURL } from '@/lib/assets';
import { tcls } from '@/lib/tailwind';
import { ClientContexts } from './ClientContexts';
+1 -1
View File
@@ -3,7 +3,7 @@ import typography from '@tailwindcss/typography';
import type { Config } from 'tailwindcss';
import plugin from 'tailwindcss/plugin';
import { ColorCategory, hexToRgb, scale, shadesOfColor } from './src/lib/colors';
import { ColorCategory, hexToRgb, scale, shadesOfColor } from '@gitbook/colors';
export const shades = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
export const opacities = [0, 4, 8, 12, 16, 24, 40, 64, 72, 88, 96, 100];
+6
View File
@@ -1,5 +1,11 @@
# @gitbook/openapi-parser
## 1.0.1
### Patch Changes
- 6157583: Improve Markdown parsing
## 1.0.0
### Major Changes
+1 -1
View File
@@ -9,7 +9,7 @@
"default": "./dist/index.js"
}
},
"version": "1.0.0",
"version": "1.0.1",
"sideEffects": false,
"dependencies": {
"@scalar/openapi-parser": "^0.10.4",
+12
View File
@@ -1,5 +1,17 @@
# @gitbook/react-openapi
## 1.0.2
### Patch Changes
- bb5c6a4: Support multiple response media types and examples
- a3f1fea: Fix display of OpenAPI header description
- 6157583: Improve Markdown parsing
- 7419ee7: Show additional fields in OpenAPI block
- 82cd9f2: Add support for anchor links in OpenAPI blocks
- Updated dependencies [6157583]
- @gitbook/openapi-parser@1.0.1
## 1.0.1
### Patch Changes
+2 -1
View File
@@ -8,7 +8,7 @@
"default": "./dist/index.js"
}
},
"version": "1.0.1",
"version": "1.0.2",
"sideEffects": false,
"dependencies": {
"@gitbook/openapi-parser": "workspace:*",
@@ -16,6 +16,7 @@
"@scalar/oas-utils": "^0.2.101",
"clsx": "^2.1.1",
"flatted": "^3.2.9",
"json-xml-parse": "^1.3.0",
"react-aria-components": "^1.6.0",
"react-aria": "^3.37.0",
"usehooks-ts": "^3.1.0",
@@ -58,7 +58,9 @@ export function OpenAPICodeSample(props: {
(searchParams.size ? `?${searchParams.toString()}` : ''),
method: data.method,
body: requestBodyContent
? generateMediaTypeExample(requestBodyContent[1], { onlyRequired: true })
? generateMediaTypeExample(requestBodyContent[1], {
omitEmptyAndOptionalProperties: true,
})
: undefined,
headers: {
...getSecurityHeaders(data.securities),
@@ -25,14 +25,17 @@ export function OpenAPIOperation(props: {
blockKey: context.blockKey,
};
const description = resolveDescription(operation)?.trim();
const description = resolveDescription(operation);
return (
<div className={clsx('openapi-operation', className)}>
<div className="openapi-summary" id={context.id}>
<h2 className="openapi-summary-title" data-deprecated={operation.deprecated}>
{operation.summary}
</h2>
<div className="openapi-summary">
{operation.summary
? context.renderHeading({
deprecated: operation.deprecated ?? false,
title: operation.summary,
})
: null}
{operation.deprecated && <div className="openapi-deprecated">Deprecated</div>}
</div>
<div className="openapi-columns">
+6 -12
View File
@@ -1,6 +1,6 @@
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import { OpenAPISchemaProperties } from './OpenAPISchema';
import { resolveDescription } from './utils';
import { parameterToProperty, resolveDescription } from './utils';
import type { OpenAPIClientContext } from './types';
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
@@ -27,13 +27,11 @@ export function OpenAPIResponse(props: {
return (
<div className="openapi-response-body">
{headers.length > 0 ? (
<OpenAPIDisclosure context={context} label={'Headers'}>
<OpenAPIDisclosure context={context} label="Headers">
<OpenAPISchemaProperties
properties={headers.map(([name, header]) => ({
propertyName: name,
schema: header.schema ?? {},
required: header.required,
}))}
properties={headers.map(([name, header]) => {
return parameterToProperty({ name, ...header });
})}
context={context}
/>
</OpenAPIDisclosure>
@@ -41,11 +39,7 @@ export function OpenAPIResponse(props: {
<div className="openapi-responsebody">
<OpenAPISchemaProperties
id={`response-${context.blockKey}`}
properties={[
{
schema: mediaType.schema ?? {},
},
]}
properties={mediaType.schema ? [{ schema: mediaType.schema }] : []}
context={context}
/>
</div>
@@ -2,9 +2,9 @@ import type { OpenAPIV3 } from '@gitbook/openapi-parser';
import { generateSchemaExample } from './generateSchemaExample';
import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
import { checkIsReference, createStateKey, resolveDescription } from './utils';
import { stringifyOpenAPI } from './stringifyOpenAPI';
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
import { InteractiveSection } from './InteractiveSection';
import { json2xml } from './json2xml';
/**
* Display an example of the response content.
@@ -38,84 +38,51 @@ export function OpenAPIResponseExample(props: {
return Number(a) - Number(b);
});
const examples = responses
.map(([key, value]) => {
const responseObject = value;
const mediaTypeObject = (() => {
if (!responseObject.content) {
return null;
}
const key = Object.keys(responseObject.content)[0];
return (
responseObject.content['application/json'] ??
(key ? responseObject.content[key] : null)
);
})();
const tabs = responses
.map(([key, responseObject]) => {
const description = resolveDescription(responseObject);
if (!mediaTypeObject) {
if (checkIsReference(responseObject)) {
return {
key: key,
label: key,
description: resolveDescription(responseObject),
body: <OpenAPIEmptyResponseExample />,
description,
body: (
<OpenAPIExample
example={getExampleFromReference(responseObject)}
context={context}
syntax="json"
/>
),
};
}
const example = handleUnresolvedReference(
(() => {
const { examples, example } = mediaTypeObject;
if (examples) {
const key = Object.keys(examples)[0];
if (key) {
// @TODO handle multiple examples
const firstExample = examples[key];
if (firstExample) {
return firstExample;
}
}
}
if (example) {
return { value: example };
}
const schema = mediaTypeObject.schema;
if (!schema) {
return null;
}
return { value: generateSchemaExample(schema) };
})(),
);
if (!responseObject.content || Object.keys(responseObject.content).length === 0) {
return {
key: key,
label: key,
description,
body: <OpenAPIEmptyResponseExample />,
};
}
return {
key: key,
label: key,
description: resolveDescription(responseObject),
body: example?.value ? (
<context.CodeBlock
code={
typeof example.value === 'string'
? example.value
: stringifyOpenAPI(example.value, null, 2)
}
syntax="json"
/>
) : (
<OpenAPIEmptyResponseExample />
),
body: <OpenAPIResponse context={context} content={responseObject.content} />,
};
})
.filter((val): val is { key: string; label: string; body: any; description: string } =>
Boolean(val),
);
if (examples.length === 0) {
if (tabs.length === 0) {
return null;
}
return (
<OpenAPITabs stateKey={createStateKey('response-example')} items={examples}>
<OpenAPITabs stateKey={createStateKey('response-example')} items={tabs}>
<InteractiveSection header={<OpenAPITabsList />} className="openapi-response-example">
<OpenAPITabsPanels />
</InteractiveSection>
@@ -123,6 +90,212 @@ export function OpenAPIResponseExample(props: {
);
}
function OpenAPIResponse(props: {
context: OpenAPIContextProps;
content: {
[media: string]: OpenAPIV3.MediaTypeObject;
};
}) {
const { context, content } = props;
const entries = Object.entries(content);
const firstEntry = entries[0];
if (!firstEntry) {
throw new Error('One media type is required');
}
if (entries.length === 1) {
const [mediaType, mediaTypeObject] = firstEntry;
return (
<OpenAPIResponseMediaType
context={context}
mediaType={mediaType}
mediaTypeObject={mediaTypeObject}
/>
);
}
const tabs = entries.map((entry) => {
const [mediaType, mediaTypeObject] = entry;
return {
key: mediaType,
label: mediaType,
body: (
<OpenAPIResponseMediaType
context={context}
mediaType={mediaType}
mediaTypeObject={mediaTypeObject}
/>
),
};
});
return (
<OpenAPITabs stateKey={createStateKey('response-media-types')} items={tabs}>
<InteractiveSection
header={<OpenAPITabsList />}
className="openapi-response-media-types"
>
<OpenAPITabsPanels />
</InteractiveSection>
</OpenAPITabs>
);
}
function OpenAPIResponseMediaType(props: {
mediaTypeObject: OpenAPIV3.MediaTypeObject;
mediaType: string;
context: OpenAPIContextProps;
}) {
const { mediaTypeObject, mediaType } = props;
const examples = getExamplesFromMediaTypeObject({ mediaTypeObject, mediaType });
const syntax = getSyntaxFromMediaType(mediaType);
const firstExample = examples[0];
if (!firstExample) {
return <OpenAPIEmptyResponseExample />;
}
if (examples.length === 1) {
return (
<OpenAPIExample
example={firstExample.example}
context={props.context}
syntax={syntax}
/>
);
}
const tabs = examples.map((example) => {
return {
key: example.key,
label: example.example.summary || example.key,
body: (
<OpenAPIExample
example={firstExample.example}
context={props.context}
syntax={syntax}
/>
),
};
});
return (
<OpenAPITabs stateKey={createStateKey('response-media-type-examples')} items={tabs}>
<InteractiveSection
header={<OpenAPITabsList />}
className="openapi-response-media-type-examples"
>
<OpenAPITabsPanels />
</InteractiveSection>
</OpenAPITabs>
);
}
/**
* Display an example.
*/
function OpenAPIExample(props: {
example: OpenAPIV3.ExampleObject;
context: OpenAPIContextProps;
syntax: string;
}) {
const { example, context, syntax } = props;
const code = stringifyExample({ example, xml: syntax === 'xml' });
if (code === null) {
return <OpenAPIEmptyResponseExample />;
}
return <context.CodeBlock code={code} syntax={syntax} />;
}
function stringifyExample(args: { example: OpenAPIV3.ExampleObject; xml: boolean }): string | null {
const { example, xml } = args;
if (!example.value) {
return null;
}
if (typeof example.value === 'string') {
return example.value;
}
if (xml) {
return json2xml(example.value);
}
return JSON.stringify(example.value, null, 2);
}
/**
* Get the syntax from a media type.
*/
function getSyntaxFromMediaType(mediaType: string): string {
if (mediaType.includes('json')) {
return 'json';
}
if (mediaType === 'application/xml') {
return 'xml';
}
return 'text';
}
/**
* Get examples from a media type object.
*/
function getExamplesFromMediaTypeObject(args: {
mediaType: string;
mediaTypeObject: OpenAPIV3.MediaTypeObject;
}): { key: string; example: OpenAPIV3.ExampleObject }[] {
const { mediaTypeObject, mediaType } = args;
if (mediaTypeObject.examples) {
return Object.entries(mediaTypeObject.examples).map(([key, example]) => {
return {
key,
example: checkIsReference(example) ? getExampleFromReference(example) : example,
};
});
}
if (mediaTypeObject.example) {
return [{ key: 'default', example: { value: mediaTypeObject.example } }];
}
if (mediaTypeObject.schema) {
if (mediaType === 'application/xml') {
// @TODO normally we should use the name of the schema but we don't have it
// fix it when we got the reference name
const root = mediaTypeObject.schema.xml?.name ?? 'object';
return [
{
key: 'default',
example: {
value: {
[root]: generateSchemaExample(mediaTypeObject.schema, {
xml: mediaType === 'application/xml',
}),
},
},
},
];
}
return [
{
key: 'default',
example: { value: generateSchemaExample(mediaTypeObject.schema) },
},
];
}
return [];
}
/**
* Empty response example.
*/
function OpenAPIEmptyResponseExample() {
return (
<pre className="openapi-response-example-empty">
@@ -131,15 +304,9 @@ function OpenAPIEmptyResponseExample() {
);
}
function handleUnresolvedReference(
input: OpenAPIV3.ExampleObject | null,
): OpenAPIV3.ExampleObject | null {
const isReference = checkIsReference(input?.value);
if (isReference) {
// If we find a reference that wasn't resolved or needed to be resolved externally, render out the URL
return { value: input.value.$ref };
}
return input;
/**
* Generate an example from a reference object.
*/
function getExampleFromReference(ref: OpenAPIV3.ReferenceObject): OpenAPIV3.ExampleObject {
return { summary: 'Unresolved reference', value: { $ref: ref.$ref } };
}
+18 -6
View File
@@ -13,8 +13,8 @@ import { OpenAPIDisclosure } from './OpenAPIDisclosure';
type CircularRefsIds = Map<OpenAPIV3.SchemaObject, string>;
export interface OpenAPISchemaPropertyEntry {
propertyName?: string;
required?: boolean;
propertyName?: string | undefined;
required?: boolean | undefined;
schema: OpenAPIV3.SchemaObject;
}
@@ -47,7 +47,7 @@ export function OpenAPISchemaProperty(
? null
: getSchemaAlternatives(schema, new Set(circularRefs.keys()));
if ((properties && !!properties.length) || schema.type === 'object') {
if ((properties && properties.length > 0) || schema.type === 'object') {
return (
<InteractiveSection id={id} className={clsx('openapi-schema', className)}>
<OpenAPISchemaPresentation {...props} />
@@ -397,7 +397,7 @@ export function getSchemaTitle(
let type = 'any';
if (schema.enum) {
type = 'enum';
type = `${schema.type} · enum`;
// check array AND schema.items as this is sometimes null despite what the type indicates
} else if (schema.type === 'array' && !!schema.items) {
type = `${getSchemaTitle(schema.items)}[]`;
@@ -407,7 +407,7 @@ export function getSchemaTitle(
type = schema.type ?? 'object';
if (schema.format) {
type += ` ${schema.format}`;
type += ` · ${schema.format}`;
}
} else if ('anyOf' in schema) {
type = 'any of';
@@ -419,8 +419,20 @@ export function getSchemaTitle(
type = 'not';
}
if (schema.minimum || schema.minLength) {
type += ` · min: ${schema.minimum || schema.minLength}`;
}
if (schema.maximum || schema.maxLength) {
type += ` · max: ${schema.maximum || schema.maxLength}`;
}
if (schema.default) {
type += ` · default: ${schema.default}`;
}
if (schema.nullable) {
type = `nullable ${type}`;
type = `${type} | nullable`;
}
return type;
+2 -17
View File
@@ -8,7 +8,7 @@ import { OpenAPIResponses } from './OpenAPIResponses';
import { OpenAPISchemaProperties } from './OpenAPISchema';
import { OpenAPISecurities } from './OpenAPISecurities';
import type { OpenAPIClientContext, OpenAPIOperationData } from './types';
import { resolveDescription } from './utils';
import { parameterToProperty } from './utils';
/**
* Client component to render the spec for the request and response.
@@ -38,22 +38,7 @@ export function OpenAPISpec(props: { data: OpenAPIOperationData; context: OpenAP
header={group.label}
>
<OpenAPISchemaProperties
properties={group.parameters.map((parameter) => {
const description = resolveDescription(parameter);
return {
propertyName: parameter.name,
schema: {
// Description of the parameter is defined at the parameter level
// we use display it if the schema doesn't override it
description: description,
example: parameter.example,
// Deprecated can be defined at the parameter level
deprecated: parameter.deprecated,
...(parameter.schema ?? {}),
},
required: parameter.required,
};
})}
properties={group.parameters.map(parameterToProperty)}
context={context}
/>
</InteractiveSection>
@@ -0,0 +1,18 @@
// Bun Snapshot v1, https://goo.gl/fbAQLP
exports[`getUrlFromServerState indents correctly 1`] = `
"<?xml version="1.0"?>
<id>10</id>
<name>doggie</name>
<category>
<id>1</id>
<name>Dogs</name>
</category>
<photoUrls>string</photoUrls>
<tags>
<id>0</id>
<name>string</name>
</tags>
<status>available</status>
"
`;
@@ -3,18 +3,21 @@ import { getExampleFromSchema } from '@scalar/oas-utils/spec-getters';
type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue };
type ScalarGetExampleFromSchemaOptions = NonNullable<Parameters<typeof getExampleFromSchema>[1]>;
type GenerateSchemaExampleOptions = Pick<
ScalarGetExampleFromSchemaOptions,
'xml' | 'omitEmptyAndOptionalProperties' | 'mode'
>;
/**
* Generate a JSON example from a schema
*/
export function generateSchemaExample(
schema: OpenAPIV3.SchemaObject,
options: {
onlyRequired?: boolean;
} = {},
options?: GenerateSchemaExampleOptions,
): JSONValue | undefined {
return getExampleFromSchema(schema, {
emptyString: 'text',
omitEmptyAndOptionalProperties: options.onlyRequired,
variables: {
'date-time': new Date().toISOString(),
date: new Date().toISOString().split('T')[0],
@@ -28,6 +31,7 @@ export function generateSchemaExample(
byte: 'Ynl0ZXM=',
password: 'password',
},
...options,
});
}
@@ -36,9 +40,7 @@ export function generateSchemaExample(
*/
export function generateMediaTypeExample(
mediaType: OpenAPIV3.MediaTypeObject,
options: {
onlyRequired?: boolean;
} = {},
options?: GenerateSchemaExampleOptions,
): JSONValue | undefined {
if (mediaType.example) {
return mediaType.example;
@@ -0,0 +1,46 @@
import { describe, expect, it } from 'bun:test';
import { json2xml } from './json2xml';
describe('getUrlFromServerState', () => {
it('transforms JSON to xml', () => {
const xml = json2xml({
foo: 'bar',
});
expect(xml).toBe('<?xml version="1.0"?>\n<foo>bar</foo>\n');
});
it('wraps array items', () => {
const xml = json2xml({
urls: {
url: ['https://example.com', 'https://example.com'],
},
});
expect(xml).toBe(
'<?xml version="1.0"?>\n<urls>\n\t<url>https://example.com</url>\n\t<url>https://example.com</url>\n</urls>\n',
);
});
it('indents correctly', () => {
const xml = json2xml({
id: 10,
name: 'doggie',
category: {
id: 1,
name: 'Dogs',
},
photoUrls: ['string'],
tags: [
{
id: 0,
name: 'string',
},
],
status: 'available',
});
expect(xml).toMatchSnapshot();
});
});
+8
View File
@@ -0,0 +1,8 @@
import { jsXml } from 'json-xml-parse';
/**
* This function converts an object to XML.
*/
export function json2xml(data: Record<string, any>) {
return jsXml.toXmlString(data, { beautify: true });
}
+1
View File
@@ -6,6 +6,7 @@ import type {
export interface OpenAPIContextProps extends OpenAPIClientContext {
CodeBlock: React.ComponentType<{ code: string; syntax: string }>;
renderHeading: (props: { deprecated: boolean; title: string }) => React.ReactNode;
/** Spec url for the Scalar Api Client */
specUrl: string;
+79 -4
View File
@@ -1,6 +1,8 @@
import type { AnyObject, OpenAPIV3 } from '@gitbook/openapi-parser';
import type { AnyObject, OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser';
export function checkIsReference(input: unknown): input is OpenAPIV3.ReferenceObject {
export function checkIsReference(
input: unknown,
): input is OpenAPIV3.ReferenceObject | OpenAPIV3_1.ReferenceObject {
return typeof input === 'object' && !!input && '$ref' in input;
}
@@ -14,8 +16,81 @@ export function createStateKey(key: string, scope?: string) {
export function resolveDescription(object: AnyObject) {
return 'x-gitbook-description-html' in object &&
typeof object['x-gitbook-description-html'] === 'string'
? object['x-gitbook-description-html']
? object['x-gitbook-description-html'].trim()
: typeof object.description === 'string'
? object.description
? object.description.trim()
: undefined;
}
/**
* Extract descriptions from an object.
*/
export function extractDescriptions(object: AnyObject) {
return {
description: object.description,
['x-gitbook-description-html']:
'x-gitbook-description-html' in object
? object['x-gitbook-description-html']
: undefined,
};
}
/**
* Resolve the first example from an object.
*/
export function resolveFirstExample(object: AnyObject) {
if ('examples' in object && typeof object.examples === 'object' && object.examples) {
const keys = Object.keys(object.examples);
const firstKey = keys[0];
if (firstKey && object.examples[firstKey]) {
return object.examples[firstKey];
}
}
if ('example' in object && object.example !== undefined) {
return object.example;
}
return undefined;
}
/**
* Resolve the schema of a parameter.
* Extract the description, example and deprecated from parameter.
*/
export function resolveParameterSchema(
parameter: OpenAPIV3.ParameterBaseObject,
): OpenAPIV3.SchemaObject {
const schema = checkIsReference(parameter.schema) ? undefined : parameter.schema;
return {
// Description of the parameter is defined at the parameter level
// we use display it if the schema doesn't override it
...extractDescriptions(parameter),
example: resolveFirstExample(parameter),
// Deprecated can be defined at the parameter level
deprecated: parameter.deprecated,
...schema,
};
}
/**
* Transform a parameter object to a property object.
*/
export function parameterToProperty(
parameter: OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject | OpenAPIV3_1.ReferenceObject,
): {
propertyName: string | undefined;
schema: OpenAPIV3.SchemaObject;
required: boolean | undefined;
} {
if (checkIsReference(parameter)) {
return {
propertyName: parameter.$ref ?? 'Unknown ref',
schema: {},
required: undefined,
};
}
return {
propertyName: parameter.name,
schema: resolveParameterSchema(parameter),
required: parameter.required,
};
}