Files
gitbook/src/lib/document.ts
T
Samy Pessé 6cab31faf6 Stream AI answers to questions (#138)
* Stream AI answers to questions

* Use a different polyfill

* Revert "Use a different polyfill"

This reverts commit b8474c56301faf3180eebf81f697703c31447500.

* Update api

* Patch event-source-polyfill to support Cloudflare

* Patch the min version

* Update api client and remove eventsource polyfill

* Cleanup
2024-02-08 15:53:05 +01:00

158 lines
4.0 KiB
TypeScript

import {
DocumentText,
DocumentInline,
DocumentFragment,
JSONDocument,
DocumentBlock,
ContentRef,
} from '@gitbook/api';
import assertNever from 'assert-never';
import { fetchOpenAPIBlock } from './openapi';
import { ResolvedContentRef } from './references';
export interface DocumentSection {
id: string;
tag?: string;
title: string;
depth: number;
}
/**
* Check if the document contains one block that should be rendered in full-width mode.
*/
export function hasFullWidthBlock(document: JSONDocument): { apiBlock: boolean } | false {
let fullWidth = false;
for (const node of document.nodes) {
if (node.data && 'fullWidth' in node.data && node.data.fullWidth) {
fullWidth = true;
}
if (node.type === 'swagger') {
return { apiBlock: true };
}
}
return fullWidth ? { apiBlock: false } : false;
}
/**
* Extract a list of sections from a document.
*/
export async function getDocumentSections(
document: JSONDocument,
resolveContentRef: (ref: ContentRef) => Promise<ResolvedContentRef | null>,
): Promise<DocumentSection[]> {
const sections: DocumentSection[] = [];
let depth = 0;
for (const block of document.nodes) {
if (
(block.type === 'heading-1' ||
block.type === 'heading-2' ||
block.type === 'heading-3') &&
block.meta?.id
) {
if (block.type === 'heading-1') {
depth = 1;
}
const title = getNodeText(block);
const id = block.meta?.id;
sections.push({
id,
title,
depth: block.type === 'heading-1' ? 1 : depth > 0 ? 2 : 1,
});
}
if (block.type === 'swagger' && block.meta?.id) {
const operation = await fetchOpenAPIBlock(block, resolveContentRef);
if (operation) {
sections.push({
id: block.meta.id,
tag: operation.method.toUpperCase(),
title: operation.operation.summary ?? operation.path,
depth: 1,
});
}
}
}
return sections;
}
/**
* Get the text of a block/inline.
*/
export function getNodeText(
node: JSONDocument | DocumentText | DocumentFragment | DocumentInline | DocumentBlock,
): string {
switch (node.object) {
case 'text':
return node.leaves.map((leaf) => leaf.text).join('');
case 'document':
case 'fragment':
case 'block':
case 'inline':
if (!('nodes' in node)) {
return '';
}
return node.nodes.map((child) => getNodeText(child)).join('');
default:
assertNever(node);
}
}
/**
* Get a fragment by its type in a node.
*/
export function getNodeFragmentByType(
node: DocumentInline | DocumentBlock,
type: string,
): DocumentFragment | null {
if (!('fragments' in node)) {
return null;
}
const fragment = node.fragments.find((child: any) => child.type === type);
return fragment ?? null;
}
/**
* Get a fragment by its `fragment` name in a node.
*/
export function getNodeFragmentByName(
node: DocumentInline | DocumentBlock,
name: string,
): DocumentFragment | null {
if (!('fragments' in node)) {
return null;
}
const fragment = node.fragments?.find((child: any) => child.fragment === name);
return fragment ?? null;
}
/**
* Test if a node is empty.
*/
export function isNodeEmpty(
node: DocumentText | DocumentFragment | DocumentInline | DocumentBlock | JSONDocument,
): boolean {
if ((node.object === 'block' || node.object === 'inline') && node.isVoid) {
return false;
}
if (node.object !== 'text' && 'nodes' in node) {
if (node.nodes.length > 1) {
return false;
}
return node.nodes.every((child) => isNodeEmpty(child));
}
const text = getNodeText(node);
return text.trim().length === 0;
}