mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-16 07:35:16 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 304042017c | |||
| 82cd9f2979 | |||
| a3f1fea27b | |||
| bb5c6a42e7 | |||
| 445baaaa61 | |||
| 7419ee7dba |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Fix opening external links when the GitBook page is embedded in an iframe
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
'@gitbook/openapi-parser': patch
|
||||
'@gitbook/react-openapi': patch
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Improve Markdown parsing
|
||||
@@ -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=="],
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
dist/
|
||||
@@ -0,0 +1,7 @@
|
||||
# @gitbook/colors
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 445baaa: Initial release
|
||||
@@ -0,0 +1,3 @@
|
||||
# `@gitbook/colors`
|
||||
|
||||
A set of default colors and transformation functions used throughout the GitBook Open and app.
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
-1
@@ -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';
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @gitbook/openapi-parser
|
||||
|
||||
## 1.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 6157583: Improve Markdown parsing
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 } };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user