mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-20 01:25:16 +00:00
Revised client state with context
This commit is contained in:
@@ -90,7 +90,6 @@ export default async function Page(props: {
|
||||
// Display the page feedback in the page footer if the aside is not visible
|
||||
withPageFeedback && !page.layout.outline
|
||||
}
|
||||
searchParams={searchParams}
|
||||
/>
|
||||
{page.layout.outline ? (
|
||||
<PageAside
|
||||
|
||||
@@ -46,8 +46,6 @@ export interface DocumentContext {
|
||||
* https://linear.app/gitbook-x/issue/RND-3588/gitbook-open-code-syntax-highlighting-runs-out-of-memory-after-a
|
||||
*/
|
||||
shouldHighlightCode: (spaceId: string | undefined) => boolean;
|
||||
|
||||
searchParams?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface DocumentContextProps {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { DocumentBlockSwagger } from '@gitbook/api';
|
||||
import { Icon } from '@gitbook/icons';
|
||||
import { OpenAPIOperation, OpenAPIOperationData } from '@gitbook/react-openapi';
|
||||
import { OpenAPIOperation } from '@gitbook/react-openapi';
|
||||
import React from 'react';
|
||||
|
||||
import { LoadingPane } from '@/components/primitives';
|
||||
import { fetchOpenAPIBlock } from '@/lib/openapi';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import OpenAPIContext from './OpenAPIContext';
|
||||
import { BlockProps } from '../Block';
|
||||
import { PlainCodeBlock } from '../CodeBlock';
|
||||
|
||||
@@ -45,27 +46,23 @@ async function OpenAPIBody(props: BlockProps<DocumentBlockSwagger>) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const enumSelectors =
|
||||
context.searchParams && context.searchParams.block === block.key
|
||||
? parseModifiers(data, context.searchParams)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<OpenAPIOperation
|
||||
data={data}
|
||||
context={{
|
||||
icons: {
|
||||
chevronDown: <Icon icon="chevron-down" />,
|
||||
chevronRight: <Icon icon="chevron-right" />,
|
||||
},
|
||||
CodeBlock: PlainCodeBlock,
|
||||
defaultInteractiveOpened: context.mode === 'print',
|
||||
id: block.meta?.id,
|
||||
enumSelectors,
|
||||
blockKey: block.key,
|
||||
}}
|
||||
className="openapi-block"
|
||||
/>
|
||||
<OpenAPIContext block={block} data={data}>
|
||||
<OpenAPIOperation
|
||||
data={data}
|
||||
context={{
|
||||
icons: {
|
||||
chevronDown: <Icon icon="chevron-down" />,
|
||||
chevronRight: <Icon icon="chevron-right" />,
|
||||
},
|
||||
CodeBlock: PlainCodeBlock,
|
||||
defaultInteractiveOpened: context.mode === 'print',
|
||||
id: block.meta?.id,
|
||||
blockKey: block.key
|
||||
}}
|
||||
className="openapi-block"
|
||||
/>
|
||||
</OpenAPIContext>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,27 +94,3 @@ function OpenAPIFallback() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function parseModifiers(data: OpenAPIOperationData, params: Record<string, string>) {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const { server: serverQueryParam } = params;
|
||||
const serverIndex =
|
||||
serverQueryParam && !isNaN(Number(serverQueryParam))
|
||||
? Math.max(0, Math.min(Number(serverQueryParam), data.servers.length - 1))
|
||||
: 0;
|
||||
const server = data.servers[serverIndex];
|
||||
if (server) {
|
||||
return Object.keys(server.variables ?? {}).reduce<Record<string, number>>(
|
||||
(result, key) => {
|
||||
const selection = Number(params[key]);
|
||||
if (!isNaN(selection)) {
|
||||
result[key] = selection;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
{ server: serverIndex },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
'use client'
|
||||
|
||||
import { DocumentBlock } from "@gitbook/api";
|
||||
import { OpenAPIOperationData } from "@gitbook/react-openapi";
|
||||
import {OpenAPIContextProvider} from "@gitbook/react-openapi/client";
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import * as React from 'react';
|
||||
|
||||
export default function OpenAPIContext(props: { children: React.ReactNode; block: DocumentBlock; data: OpenAPIOperationData }) {
|
||||
const { block, children, data } = props;
|
||||
const [isPending, startTransition] = React.useTransition();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
return <OpenAPIContextProvider isPending={isPending} data={data} params={searchParams.get("block") === block.key ? Object.fromEntries(searchParams.entries()) : undefined} onUpdate={async (params) => {
|
||||
startTransition(() => {
|
||||
const queryParams = new URLSearchParams(params ?? '');
|
||||
router.replace(`?${queryParams}`, { scroll: false });
|
||||
})
|
||||
}}>{children}</OpenAPIContextProvider>
|
||||
}
|
||||
@@ -34,7 +34,6 @@ export function PageBody(props: {
|
||||
document: JSONDocument | null;
|
||||
context: ContentRefContext;
|
||||
withPageFeedback: boolean;
|
||||
searchParams: Record<string, string>;
|
||||
}) {
|
||||
const {
|
||||
space,
|
||||
@@ -45,7 +44,6 @@ export function PageBody(props: {
|
||||
page,
|
||||
document,
|
||||
withPageFeedback,
|
||||
searchParams,
|
||||
} = props;
|
||||
|
||||
const asFullWidth = document ? hasFullWidthBlock(document) : false;
|
||||
@@ -98,7 +96,6 @@ export function PageBody(props: {
|
||||
resolveContentRef: (ref, options) =>
|
||||
resolveContentRef(ref, context, options),
|
||||
shouldHighlightCode,
|
||||
searchParams,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"types": "./dist/index.d.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"./client": "./src/client.ts"
|
||||
},
|
||||
"version": "0.6.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -4,10 +4,9 @@ import { CodeSampleInput, codeSampleGenerators } from './code-samples';
|
||||
import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation';
|
||||
import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
|
||||
import { InteractiveSection } from './InteractiveSection';
|
||||
import { getServersURL } from './OpenAPIServerURL';
|
||||
import { ScalarApiButton } from './ScalarApiButton';
|
||||
import { OpenAPIContextProps } from './types';
|
||||
import { noReference } from './utils';
|
||||
import { getServersURL, noReference } from './utils';
|
||||
|
||||
/**
|
||||
* Display code samples to execute the operation.
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react';
|
||||
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
|
||||
import { getServersURL } from './utils';
|
||||
|
||||
type OpenAPIContextProps = { isPending?: boolean; serverUrl?: string; state?: Record<string, string> | null, onUpdate: (params: Record<string, string> | null) => void; }
|
||||
const OpenAPIContext = React.createContext<OpenAPIContextProps | null>(null);
|
||||
|
||||
export function useOpenAPIContext() {
|
||||
return React.useContext(OpenAPIContext);
|
||||
}
|
||||
|
||||
export function OpenAPIContextProvider(props: { children: React.ReactNode; data: OpenAPIOperationData; isPending?: boolean; params?: Record<string, string>; onUpdate: OpenAPIContextProps['onUpdate']; }) {
|
||||
const { children, data, isPending, params, onUpdate } = props;
|
||||
|
||||
const clientState = React.useMemo(() => {
|
||||
if (!params) { return null; }
|
||||
return parseClientStateModifiers(data, params);
|
||||
}, [data, params]);
|
||||
const serverUrl = getServersURL(data.servers, clientState ?? undefined)
|
||||
|
||||
return <OpenAPIContext.Provider value={{
|
||||
isPending,
|
||||
state: clientState,
|
||||
serverUrl,
|
||||
onUpdate
|
||||
}}>{children}</OpenAPIContext.Provider>
|
||||
}
|
||||
|
||||
|
||||
function parseClientStateModifiers(data: OpenAPIOperationData, params: Record<string, string>) {
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
const serverQueryParam = params['server'];
|
||||
const serverIndex =
|
||||
serverQueryParam && !isNaN(Number(serverQueryParam))
|
||||
? Math.max(0, Math.min(Number(serverQueryParam), data.servers.length - 1))
|
||||
: 0;
|
||||
const server = data.servers[serverIndex];
|
||||
return server ? Object.keys(server.variables ?? {}).reduce<Record<string, string>>(
|
||||
(result, key) => {
|
||||
const selection = params[key];
|
||||
if (!isNaN(Number(selection))) {
|
||||
result[key] = selection;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
{ server: `${serverIndex}`, edit: params['edit'] },
|
||||
) : null;
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation';
|
||||
import { Markdown } from './Markdown';
|
||||
import { OpenAPICodeSample } from './OpenAPICodeSample';
|
||||
import { OpenAPIResponseExample } from './OpenAPIResponseExample';
|
||||
import { getServersURL, OpenAPIServerURL } from './OpenAPIServerURL';
|
||||
import { OpenAPIServerURL } from './OpenAPIServerURL';
|
||||
import { OpenAPISpec } from './OpenAPISpec';
|
||||
import { OpenAPIClientContext, OpenAPIContextProps } from './types';
|
||||
import { ScalarApiClient } from './ScalarApiButton';
|
||||
@@ -25,11 +25,10 @@ export async function OpenAPIOperation(props: {
|
||||
defaultInteractiveOpened: context.defaultInteractiveOpened,
|
||||
icons: context.icons,
|
||||
blockKey: context.blockKey,
|
||||
enumSelectors: context.enumSelectors,
|
||||
};
|
||||
|
||||
return (
|
||||
<ScalarApiClient serverUrl={getServersURL(data.servers, context.enumSelectors)}>
|
||||
<ScalarApiClient>
|
||||
<div className={classNames('openapi-operation', className)}>
|
||||
<div className="openapi-intro">
|
||||
<h2 className="openapi-summary" id={context.id}>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
import { OpenAPIServerURLVariable } from './OpenAPIServerURLVariable';
|
||||
import { OpenAPIClientContext } from './types';
|
||||
import { ServerURLForm } from './OpenAPIServerURLForm';
|
||||
import { ServerSelector } from './ServerSelector';
|
||||
import { useOpenAPIContext } from './OpenAPIContextProvider';
|
||||
import { parseServerURL } from './utils';
|
||||
|
||||
/**
|
||||
* Show the url of the server with variables replaced by their default values.
|
||||
@@ -14,9 +17,12 @@ export function OpenAPIServerURL(props: {
|
||||
path?: string;
|
||||
}) {
|
||||
const { path, servers, context } = props;
|
||||
const serverIndex = context.enumSelectors?.server ?? 0;
|
||||
const ctx = useOpenAPIContext();
|
||||
|
||||
const serverIndex = !isNaN(Number(ctx?.state?.server)) ? Number(ctx?.state?.server) : 0;
|
||||
const server = servers[serverIndex];
|
||||
const parts = parseServerURL(server?.url ?? '');
|
||||
console.log({ ctxState: ctx?.state })
|
||||
|
||||
return (
|
||||
<ServerURLForm context={context} servers={servers} serverIndex={serverIndex}>
|
||||
@@ -33,7 +39,8 @@ export function OpenAPIServerURL(props: {
|
||||
key={i}
|
||||
name={part.name}
|
||||
variable={server.variables[part.name]}
|
||||
enumIndex={context.enumSelectors?.[part.name]}
|
||||
selectionIndex={Number(ctx?.state?.[part.name])}
|
||||
selectable={Boolean(ctx?.state?.edit)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -42,40 +49,3 @@ export function OpenAPIServerURL(props: {
|
||||
</ServerURLForm>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default URL for the server.
|
||||
*/
|
||||
export function getServersURL(
|
||||
servers: OpenAPIV3.ServerObject[],
|
||||
selectors?: Record<string, number>,
|
||||
): string {
|
||||
const serverIndex = selectors && !isNaN(selectors.server) ? Number(selectors.server) : 0;
|
||||
const server = servers[serverIndex];
|
||||
const parts = parseServerURL(server?.url ?? '');
|
||||
|
||||
return parts
|
||||
.map((part) => {
|
||||
if (part.kind === 'text') {
|
||||
return part.text;
|
||||
} else {
|
||||
return selectors && !isNaN(selectors[part.name])
|
||||
? server.variables?.[part.name]?.enum?.[selectors[part.name]]
|
||||
: (server.variables?.[part.name]?.default ?? `{${part.name}}`);
|
||||
}
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
function parseServerURL(url: string) {
|
||||
const parts = url.split(/{([^}]+)}/g);
|
||||
const result: Array<{ kind: 'variable'; name: string } | { kind: 'text'; text: string }> = [];
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (i % 2 === 0) {
|
||||
result.push({ kind: 'text', text: parts[i] });
|
||||
} else {
|
||||
result.push({ kind: 'variable', name: parts[i] });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { OpenAPIClientContext } from './types';
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
import { ServerSelector } from './ServerSelector';
|
||||
import { useOpenAPIContext } from './OpenAPIContextProvider';
|
||||
|
||||
export function ServerURLForm(props: {
|
||||
children: React.ReactNode;
|
||||
@@ -13,50 +13,59 @@ export function ServerURLForm(props: {
|
||||
serverIndex: number;
|
||||
}) {
|
||||
const { children, context, servers, serverIndex } = props;
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = React.useTransition();
|
||||
|
||||
const ctx = useOpenAPIContext();
|
||||
const server = servers[serverIndex];
|
||||
const formRef = React.useRef<HTMLFormElement>(null);
|
||||
|
||||
function switchServer(index: number) {
|
||||
startTransition(() => {
|
||||
if (index !== serverIndex) {
|
||||
let params = new URLSearchParams(
|
||||
`block=${context.blockKey}&server=${index ?? '0'}`,
|
||||
);
|
||||
router.push(`?${params}`, { scroll: false });
|
||||
}
|
||||
});
|
||||
if (index !== serverIndex) {
|
||||
update({
|
||||
server: `${index}` ?? '0',
|
||||
...(ctx?.state?.edit ? { edit: 'true' } : undefined)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateServerVariables(formData: FormData) {
|
||||
startTransition(() => {
|
||||
let params = new URLSearchParams(
|
||||
`block=${context.blockKey}&server=${formData.get('server') ?? '0'}`,
|
||||
);
|
||||
const variableKeys = Object.keys(server.variables ?? {});
|
||||
for (const pair of formData.entries()) {
|
||||
if (variableKeys.includes(pair[0]) && !isNaN(Number(pair[1]))) {
|
||||
params.set(pair[0], `${pair[1]}`);
|
||||
}
|
||||
const variableKeys = Object.keys(server.variables ?? {});
|
||||
const variables: Record<string, string> = {};
|
||||
for (const pair of formData) {
|
||||
if (variableKeys.includes(pair[0]) && !isNaN(Number(pair[1]))) {
|
||||
variables[pair[0]] = `${pair[1]}`;
|
||||
}
|
||||
router.push(`?${params}`, { scroll: false });
|
||||
}
|
||||
update({
|
||||
server: `${formData.get('server')}` ?? '0',
|
||||
...variables,
|
||||
...(ctx?.state?.edit ? { edit: 'true' } : undefined)
|
||||
});
|
||||
}
|
||||
|
||||
function update(variables?: Record<string, string>) {
|
||||
if (!context.blockKey) { return; }
|
||||
ctx?.onUpdate({
|
||||
block: context.blockKey,
|
||||
...variables,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<form ref={formRef} action={updateServerVariables} className="contents">
|
||||
<fieldset disabled={isPending} className="contents">
|
||||
<form ref={formRef} onSubmit={e => { e.preventDefault(); updateServerVariables(new FormData(e.currentTarget)); }} className="contents">
|
||||
<fieldset disabled={ctx?.isPending} className="contents">
|
||||
<input type="hidden" name="block" value={context.blockKey} />
|
||||
{children}
|
||||
{servers.length > 1 ? (
|
||||
{ctx?.state?.edit && servers.length > 1 ? (
|
||||
<ServerSelector
|
||||
servers={servers}
|
||||
currentIndex={serverIndex}
|
||||
onChange={switchServer}
|
||||
servers={servers}
|
||||
currentIndex={serverIndex}
|
||||
onChange={switchServer}
|
||||
/>
|
||||
) : null}
|
||||
<button className='inline-flex pl-4' onClick={() => {
|
||||
const state = { ...ctx?.state };
|
||||
delete state.edit;
|
||||
update({ server: `${serverIndex}`, ...state, ...(ctx?.state?.edit ? undefined : { edit: 'true' }) });
|
||||
}} aria-label={ctx?.state?.edit ? "Clear" : "Edit"}>{ctx?.state?.edit ? 'X' : 'Edit'}</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import classNames from 'classnames';
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
import { OpenAPIClientContext } from './types';
|
||||
|
||||
/**
|
||||
* Interactive component to show the value of a server variable and let the user change it.
|
||||
@@ -11,18 +9,23 @@ import { OpenAPIClientContext } from './types';
|
||||
export function OpenAPIServerURLVariable(props: {
|
||||
name: string;
|
||||
variable: OpenAPIV3.ServerVariableObject;
|
||||
enumIndex?: number;
|
||||
selectionIndex?: number;
|
||||
selectable: boolean;
|
||||
}) {
|
||||
const { enumIndex, name, variable } = props;
|
||||
const { selectable, selectionIndex, name, variable } = props;
|
||||
|
||||
if (variable.enum && variable.enum.length > 0) {
|
||||
if (!selectable) {
|
||||
return <span className={classNames('openapi-url-var')}>{!isNaN(Number(selectionIndex)) ? variable.enum[Number(selectionIndex)] : variable.default}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<EnumSelect
|
||||
<VariableSelector
|
||||
name={name}
|
||||
variable={variable}
|
||||
value={
|
||||
!isNaN(Number(enumIndex))
|
||||
? enumIndex
|
||||
!isNaN(Number(selectionIndex))
|
||||
? selectionIndex
|
||||
: variable.enum.findIndex((v) => v === variable.default)
|
||||
}
|
||||
/>
|
||||
@@ -35,7 +38,7 @@ export function OpenAPIServerURLVariable(props: {
|
||||
/**
|
||||
* Render a select if there is an enum for a Server URL variable
|
||||
*/
|
||||
function EnumSelect(props: {
|
||||
function VariableSelector(props: {
|
||||
value?: number;
|
||||
name: string;
|
||||
variable: OpenAPIV3.ServerVariableObject;
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
import React from 'react';
|
||||
|
||||
import { OpenAPIOperationData, fromJSON } from './fetchOpenAPIOperation';
|
||||
import { useOpenAPIContext } from './OpenAPIContextProvider';
|
||||
import { getServersURL } from './utils';
|
||||
|
||||
const ApiClientReact = React.lazy(async () => {
|
||||
const mod = await import('@scalar/api-client-react');
|
||||
@@ -55,8 +57,10 @@ export function ScalarApiButton(props: {
|
||||
/**
|
||||
* Wrap the rendering with a context to open the scalar modal.
|
||||
*/
|
||||
export function ScalarApiClient(props: { children: React.ReactNode; serverUrl?: string }) {
|
||||
const { children, serverUrl } = props;
|
||||
export function ScalarApiClient(props: { children: React.ReactNode; }) {
|
||||
const { children } = props;
|
||||
|
||||
const ctx = useOpenAPIContext();
|
||||
|
||||
const [active, setActive] = React.useState<null | {
|
||||
operationData: OpenAPIOperationData | null;
|
||||
@@ -125,12 +129,12 @@ export function ScalarApiClient(props: { children: React.ReactNode; serverUrl?:
|
||||
headers: request.headers.map((header: Header) => {
|
||||
return { ...header, enabled: true };
|
||||
}),
|
||||
url: serverUrl ?? operationData.servers[0]?.url,
|
||||
url: ctx?.serverUrl ?? operationData.servers[0]?.url,
|
||||
body: request.postData?.text,
|
||||
};
|
||||
|
||||
return data;
|
||||
}, [active, serverUrl]);
|
||||
}, [active, ctx?.state?.serverUrl]);
|
||||
|
||||
return (
|
||||
<ScalarContext.Provider value={open}>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
'use client';
|
||||
export {OpenAPIContextProvider} from './OpenAPIContextProvider';
|
||||
@@ -11,3 +11,41 @@ export function noReference<T>(input: T | OpenAPIV3.ReferenceObject): T {
|
||||
export function createStateKey(key: string, scope?: string) {
|
||||
return scope ? `${scope}_${key}` : key;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the default URL for the server.
|
||||
*/
|
||||
export function getServersURL(
|
||||
servers: OpenAPIV3.ServerObject[],
|
||||
selectors?: Record<string, string>,
|
||||
): string {
|
||||
const serverIndex = selectors && !isNaN(Number(selectors.server)) ? Number(selectors.server) : 0;
|
||||
const server = servers[serverIndex];
|
||||
const parts = parseServerURL(server?.url ?? '');
|
||||
|
||||
return parts
|
||||
.map((part) => {
|
||||
if (part.kind === 'text') {
|
||||
return part.text;
|
||||
} else {
|
||||
return selectors && !isNaN(Number(selectors[part.name]))
|
||||
? server.variables?.[part.name]?.enum?.[Number(selectors[part.name])]
|
||||
: (server.variables?.[part.name]?.default ?? `{${part.name}}`);
|
||||
}
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
export function parseServerURL(url: string) {
|
||||
const parts = url.split(/{([^}]+)}/g);
|
||||
const result: Array<{ kind: 'variable'; name: string } | { kind: 'text'; text: string }> = [];
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
if (i % 2 === 0) {
|
||||
result.push({ kind: 'text', text: parts[i] });
|
||||
} else {
|
||||
result.push({ kind: 'variable', name: parts[i] });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user