mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
Turn on the "noUncheckedIndexedAccess" flag in all TS config files (#3587)
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
|
||||
@@ -121,17 +121,6 @@ export function getCacheTag(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cache tag for a given URL.
|
||||
*/
|
||||
export function getCacheTagForURL(url: string | URL) {
|
||||
const parsedURL = url instanceof URL ? url : new URL(url);
|
||||
return getCacheTag({
|
||||
tag: 'url',
|
||||
hostname: parsedURL.hostname,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tags for a computed content source.
|
||||
*/
|
||||
@@ -182,7 +171,7 @@ export function getComputedContentSourceCacheTags(
|
||||
break;
|
||||
default:
|
||||
// Do not throw for unknown dependency types
|
||||
// as it might mean we are lacking behind the API version
|
||||
// as it might mean we are lagging behind the API version
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -198,9 +187,8 @@ export function getComputedContentSourceCacheTags(
|
||||
}
|
||||
|
||||
// We invalidate the computed content when a new version of the integration is deployed.
|
||||
|
||||
if (source.type.startsWith('integration:')) {
|
||||
const integration = source.type.split(':')[1];
|
||||
const integration = source.type.split(':')[1]!;
|
||||
tags.push(
|
||||
getCacheTag({
|
||||
tag: 'integration',
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
|
||||
@@ -8,7 +8,7 @@ type RGBColor = [number, number, number];
|
||||
type OKLABColor = { L: number; A: number; B: number };
|
||||
type OKLCHColor = { L: number; C: number; H: number };
|
||||
|
||||
const D65 = [95.047, 100.0, 108.883]; // Reference white (D65)
|
||||
const D65 = [95.047, 100.0, 108.883] as const; // Reference white (D65)
|
||||
|
||||
export enum ColorCategory {
|
||||
backgrounds = 'backgrounds',
|
||||
@@ -211,8 +211,8 @@ export function colorScale(
|
||||
const result = [];
|
||||
|
||||
for (let index = 0; index < mapping.length; index++) {
|
||||
const targetL =
|
||||
foregroundColor.L * mapping[index] + backgroundColor.L * (1 - mapping[index]);
|
||||
const step = mapping[index]!;
|
||||
const targetL = foregroundColor.L * step + backgroundColor.L * (1 - step);
|
||||
|
||||
if (
|
||||
index === 8 &&
|
||||
@@ -295,7 +295,7 @@ export function rgbArrayToHex(rgb: RGBColor): string {
|
||||
|
||||
export function getColor(percentage: number, start: RGBColor, end: RGBColor) {
|
||||
const rgb = end.map((channel, index) => {
|
||||
return Math.round(channel + percentage * (start[index] - channel));
|
||||
return Math.round(channel + percentage * (start[index]! - channel));
|
||||
});
|
||||
|
||||
return rgbArrayToHex(rgb as RGBColor);
|
||||
@@ -392,14 +392,14 @@ export function xyzToLab65(xyz: [number, number, number]): {
|
||||
B: number;
|
||||
} {
|
||||
const [x, y, z] = xyz.map((v, i) => {
|
||||
const scaled = v / D65[i];
|
||||
const scaled = v / D65[i]!;
|
||||
return scaled > 0.008856 ? Math.cbrt(scaled) : 7.787 * scaled + 16 / 116;
|
||||
});
|
||||
|
||||
return {
|
||||
L: 116 * y - 16,
|
||||
A: 500 * (x - y),
|
||||
B: 200 * (y - z),
|
||||
L: 116 * y! - 16,
|
||||
A: 500 * (x! - y!),
|
||||
B: 200 * (y! - z!),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
|
||||
@@ -67,7 +67,7 @@ function getBestUnicodeRange(text: string, ranges: Record<string, string>): stri
|
||||
|
||||
const body = token.slice(2); // drop "U+"
|
||||
const [startHex, endHex] = body.split('-');
|
||||
const start = Number.parseInt(startHex, 16);
|
||||
const start = Number.parseInt(startHex!, 16);
|
||||
const end = endHex ? Number.parseInt(endHex, 16) : start;
|
||||
|
||||
if (Number.isNaN(start) || Number.isNaN(end) || end < start) return null;
|
||||
@@ -92,7 +92,7 @@ function getBestUnicodeRange(text: string, ranges: Record<string, string>): stri
|
||||
|
||||
for (const [label, rangesArr] of Object.entries(parsed)) {
|
||||
if (rangesArr.some(([lo, hi]) => cp >= lo && cp <= hi)) {
|
||||
hits[label]++;
|
||||
hits[label]!++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"module": "ESNext",
|
||||
"target": "es2022",
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
|
||||
@@ -11,7 +11,7 @@ const args = process.argv.slice(2);
|
||||
const versionId = args[0];
|
||||
|
||||
// The preview URL is in the format https://<versionId>-gitbook-open-v2-server-preview.gitbook.workers.dev
|
||||
const previewHostname = `${versionId.split('-')[0]}-gitbook-open-v2-server-preview.gitbook.workers.dev`;
|
||||
const previewHostname = `${versionId?.split('-')[0]}-gitbook-open-v2-server-preview.gitbook.workers.dev`;
|
||||
|
||||
let updatedFile = file.replace(
|
||||
/"PREVIEW_HOSTNAME": "TO_REPLACE"/,
|
||||
|
||||
@@ -39,7 +39,9 @@ export async function streamRenderAIMessage(
|
||||
|
||||
if (message.steps[stepIndex]) {
|
||||
message.steps = [...message.steps];
|
||||
// @ts-expect-error
|
||||
message.steps[stepIndex] = { ...message.steps[stepIndex] };
|
||||
// @ts-expect-error
|
||||
callback(message.steps[stepIndex]);
|
||||
} else {
|
||||
message.steps = [
|
||||
@@ -52,6 +54,7 @@ export async function streamRenderAIMessage(
|
||||
},
|
||||
},
|
||||
];
|
||||
// @ts-expect-error
|
||||
callback(message.steps[stepIndex]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -111,14 +111,15 @@ function DefaultAction(props: AIActionsDropdownProps) {
|
||||
(assistant) => assistant.ui === true && assistant.pageAction
|
||||
);
|
||||
|
||||
if (assistants.length) {
|
||||
return <OpenAIAssistant assistant={assistants[0]} type="button" />;
|
||||
const assistant = assistants[0];
|
||||
if (assistant) {
|
||||
return <OpenAIAssistant assistant={assistant} type="button" />;
|
||||
}
|
||||
|
||||
if (actions.markdown) {
|
||||
return (
|
||||
<CopyMarkdown
|
||||
isDefaultAction={!assistants.length}
|
||||
isDefaultAction={!assistant}
|
||||
markdownPageUrl={markdownPageUrl}
|
||||
type="button"
|
||||
/>
|
||||
|
||||
@@ -119,7 +119,7 @@ export async function highlight(
|
||||
currentIndex += 1; // for the \n
|
||||
|
||||
return {
|
||||
highlighted: Boolean(lineBlock.data.highlighted),
|
||||
highlighted: Boolean(lineBlock?.data.highlighted),
|
||||
tokens: result,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -7,13 +7,16 @@ import { Block, type BlockProps } from './Block';
|
||||
import { Blocks } from './Blocks';
|
||||
import { getBlockTextStyle } from './spacing';
|
||||
|
||||
export function Hint(props: BlockProps<DocumentBlockHint>) {
|
||||
const { block, style, ancestorBlocks, ...contextProps } = props;
|
||||
export function Hint({
|
||||
block,
|
||||
style,
|
||||
ancestorBlocks,
|
||||
...contextProps
|
||||
}: BlockProps<DocumentBlockHint>) {
|
||||
const hintStyle = HINT_STYLES[block.data.style] ?? HINT_STYLES.info;
|
||||
const firstLine = getBlockTextStyle(block.nodes[0]);
|
||||
|
||||
const firstNode = block.nodes[0];
|
||||
const hasHeading = ['heading-1', 'heading-2', 'heading-3'].includes(block.nodes[0].type);
|
||||
const firstNode = block.nodes[0]!;
|
||||
const firstLine = getBlockTextStyle(firstNode);
|
||||
const hasHeading = ['heading-1', 'heading-2', 'heading-3'].includes(firstNode.type);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -19,9 +19,9 @@ export function ListItem(props: BlockProps<DocumentBlockListItem>) {
|
||||
|
||||
const parent = ancestorBlocks[ancestorBlocks.length - 1];
|
||||
assert(
|
||||
(parent && parent.type === 'list-ordered') ||
|
||||
parent.type === 'list-unordered' ||
|
||||
parent.type === 'list-tasks',
|
||||
parent?.type === 'list-ordered' ||
|
||||
parent?.type === 'list-unordered' ||
|
||||
parent?.type === 'list-tasks',
|
||||
'Invalid parent list type'
|
||||
);
|
||||
|
||||
@@ -112,11 +112,11 @@ function getListItemDepth(input: {
|
||||
|
||||
for (let i = ancestorBlocks.length - 1; i >= 0; i--) {
|
||||
const block = ancestorBlocks[i];
|
||||
if (block.type === type) {
|
||||
if (block?.type === type) {
|
||||
depth = depth + 1;
|
||||
continue;
|
||||
}
|
||||
if (block.type === 'list-item') {
|
||||
if (block?.type === 'list-item') {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -10,7 +10,7 @@ export function StepperStep(props: BlockProps<DocumentBlockStepperStep>) {
|
||||
const { block, style, ancestorBlocks, ...contextProps } = props;
|
||||
|
||||
const ancestor = ancestorBlocks[ancestorBlocks.length - 1];
|
||||
assert(ancestor.type === 'stepper', 'Ancestor block must be a stepper');
|
||||
assert(ancestor?.type === 'stepper', 'Ancestor block must be a stepper');
|
||||
|
||||
const index = ancestor.nodes.indexOf(block);
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ export function RecordRow(
|
||||
autoSizedColumns,
|
||||
fixedColumns,
|
||||
});
|
||||
// @ts-expect-error
|
||||
const verticalAlignment = getColumnVerticalAlignment(block.data.definition[column]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -25,7 +25,9 @@ export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) {
|
||||
/* Only show the header when configured and not empty */
|
||||
const withHeader =
|
||||
!view.hideHeader &&
|
||||
view.columns.some((columnId) => block.data.definition[columnId].title.trim().length > 0);
|
||||
view.columns.some(
|
||||
(columnId) => (block.data.definition[columnId]?.title.trim().length ?? 0) > 0
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={tcls(style, styles.tableWrapper)}>
|
||||
@@ -43,28 +45,31 @@ export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) {
|
||||
)}
|
||||
>
|
||||
<div role="row" className={tcls('flex', 'w-full')}>
|
||||
{view.columns.map((column) => (
|
||||
<div
|
||||
key={column}
|
||||
role="columnheader"
|
||||
className={tcls(
|
||||
styles.columnHeader,
|
||||
getColumnAlignment(block.data.definition[column])
|
||||
)}
|
||||
style={{
|
||||
width: getColumnWidth({
|
||||
column,
|
||||
columnWidths,
|
||||
autoSizedColumns,
|
||||
fixedColumns,
|
||||
}),
|
||||
minWidth: columnWidths?.[column] || '100px',
|
||||
}}
|
||||
title={block.data.definition[column].title}
|
||||
>
|
||||
{block.data.definition[column].title}
|
||||
</div>
|
||||
))}
|
||||
{view.columns.map((column) => {
|
||||
const definition = block.data.definition[column]!;
|
||||
return (
|
||||
<div
|
||||
key={column}
|
||||
role="columnheader"
|
||||
className={tcls(
|
||||
styles.columnHeader,
|
||||
getColumnAlignment(definition)
|
||||
)}
|
||||
style={{
|
||||
width: getColumnWidth({
|
||||
column,
|
||||
columnWidths,
|
||||
autoSizedColumns,
|
||||
fixedColumns,
|
||||
}),
|
||||
minWidth: columnWidths?.[column] || '100px',
|
||||
}}
|
||||
title={definition.title}
|
||||
>
|
||||
{definition.title}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -206,7 +206,7 @@ export function DynamicTabs(
|
||||
'max-w-full',
|
||||
'truncate',
|
||||
|
||||
active.id === tab.id
|
||||
active?.id === tab.id
|
||||
? [
|
||||
'shrink-0',
|
||||
'active-tab',
|
||||
@@ -222,7 +222,7 @@ export function DynamicTabs(
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={active.id === tab.id}
|
||||
aria-selected={active?.id === tab.id}
|
||||
aria-controls={getTabPanelId(tab.id)}
|
||||
id={getTabButtonId(tab.id)}
|
||||
onClick={() => {
|
||||
@@ -255,7 +255,7 @@ export function DynamicTabs(
|
||||
role="tabpanel"
|
||||
id={getTabPanelId(tab.id)}
|
||||
aria-labelledby={getTabButtonId(tab.id)}
|
||||
className={tcls('p-4', tab.id !== active.id ? 'hidden' : null)}
|
||||
className={tcls('p-4', tab.id !== active?.id ? 'hidden' : null)}
|
||||
>
|
||||
{tabsBody[index]}
|
||||
</div>
|
||||
|
||||
@@ -43,9 +43,9 @@ export function getBlockTextStyle(block: DocumentBlock): {
|
||||
case 'list-ordered':
|
||||
case 'list-tasks':
|
||||
case 'list-unordered':
|
||||
return getBlockTextStyle(block.nodes[0]);
|
||||
return getBlockTextStyle(block.nodes[0]!);
|
||||
case 'list-item':
|
||||
return getBlockTextStyle(block.nodes[0]);
|
||||
return getBlockTextStyle(block.nodes[0]!);
|
||||
default:
|
||||
return {
|
||||
textSize: 'text-base',
|
||||
|
||||
@@ -21,7 +21,7 @@ export function isBlockOffscreen(
|
||||
|
||||
const allAncestors = [document, ...ancestorBlocks];
|
||||
for (let index = allAncestors.length - 1; index >= 0; index--) {
|
||||
const parent = allAncestors[index];
|
||||
const parent = allAncestors[index]!;
|
||||
const offset = getBlockOffset({ block: current, parent });
|
||||
|
||||
if (offset > screenHeight) {
|
||||
|
||||
@@ -19,14 +19,8 @@ const SECTION_INTERSECTING_THRESHOLD = 0.9;
|
||||
*/
|
||||
const ACTIVE_ITEM_OFFSET = 100;
|
||||
|
||||
export function ScrollSectionsList(props: { sections: DocumentSection[] }) {
|
||||
const { sections } = props;
|
||||
|
||||
const ids = React.useMemo(() => {
|
||||
return sections.map((section) => {
|
||||
return section.id;
|
||||
});
|
||||
}, [sections]);
|
||||
export function ScrollSectionsList({ sections }: { sections: DocumentSection[] }) {
|
||||
const ids = React.useMemo(() => sections.map(({ id }) => id), [sections]);
|
||||
|
||||
const enabled = useBodyLoaded();
|
||||
|
||||
|
||||
@@ -140,7 +140,13 @@ export async function CustomizationRootLayout(props: {
|
||||
customization.styling.primaryColor.light
|
||||
)
|
||||
};
|
||||
--header-link: ${hexToRgb(customization.header.linkColor?.light ?? colorContrast(tintColor?.light ?? customization.styling.primaryColor.light))};
|
||||
--header-link: ${hexToRgb(
|
||||
// @ts-expect-error
|
||||
customization.header.linkColor?.light ??
|
||||
colorContrast(
|
||||
tintColor?.light ?? customization.styling.primaryColor.light
|
||||
)
|
||||
)};
|
||||
|
||||
${generateColorVariable('info', infoColor.light)}
|
||||
${generateColorVariable('warning', warningColor.light)}
|
||||
@@ -154,7 +160,13 @@ export async function CustomizationRootLayout(props: {
|
||||
${generateColorVariable('neutral', DEFAULT_TINT_COLOR, { darkMode: true })}
|
||||
|
||||
--header-background: ${hexToRgb(customization.header.backgroundColor?.dark ?? tintColor?.dark ?? customization.styling.primaryColor.dark)};
|
||||
--header-link: ${hexToRgb(customization.header.linkColor?.dark ?? colorContrast(tintColor?.dark ?? customization.styling.primaryColor.dark))};
|
||||
--header-link: ${hexToRgb(
|
||||
// @ts-expect-error
|
||||
customization.header.linkColor?.dark ??
|
||||
colorContrast(
|
||||
tintColor?.dark ?? customization.styling.primaryColor.dark
|
||||
)
|
||||
)};
|
||||
|
||||
${generateColorVariable('info', infoColor.dark, { darkMode: true })}
|
||||
${generateColorVariable('warning', warningColor.dark, { darkMode: true })}
|
||||
@@ -325,6 +337,7 @@ function generateColorVariable(
|
||||
return Object.entries(shades)
|
||||
.map(([key, value]) => {
|
||||
const rgbValue = hexToRgb(value); // Check the original hex value
|
||||
// @ts-expect-error
|
||||
const contrastValue = withContrast ? hexToRgb(colorContrast(value)) : undefined; // Add contrast if needed
|
||||
return `--${name}-${key}: ${rgbValue}; ${
|
||||
contrastValue ? `--contrast-${name}-${key}: ${contrastValue};` : ''
|
||||
|
||||
@@ -55,7 +55,7 @@ export function SearchContainer(props: SearchContainerProps) {
|
||||
initialRef.current = true;
|
||||
|
||||
// For simplicity we're only triggering the first assistant
|
||||
assistants[0].open(state?.ask ?? undefined);
|
||||
assistants[0]?.open(state?.ask ?? undefined);
|
||||
}, [state?.ask, assistants.length, assistants[0]?.open]);
|
||||
|
||||
const onClose = React.useCallback(
|
||||
|
||||
@@ -296,7 +296,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
||||
key={item.id}
|
||||
question={item.question}
|
||||
active={index === cursor}
|
||||
assistant={assistants[0]}
|
||||
assistant={assistants[0]!}
|
||||
recommended
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -71,8 +71,7 @@ export interface AskAnswerResult {
|
||||
export async function searchAllSiteContent(query: string): Promise<OrderedComputedResult[]> {
|
||||
return traceErrorOnly('Search.searchAllSiteContent', async () => {
|
||||
const context = await getServerActionBaseContext();
|
||||
|
||||
return await searchSiteContent(context, {
|
||||
return searchSiteContent(context, {
|
||||
query,
|
||||
scope: { mode: 'all' },
|
||||
});
|
||||
@@ -270,7 +269,7 @@ async function searchSiteContent(
|
||||
|
||||
return (
|
||||
await Promise.all(
|
||||
searchResults.map(async (spaceItem) => {
|
||||
searchResults.map((spaceItem) => {
|
||||
const found = findSiteSpaceBy(
|
||||
structure,
|
||||
(siteSpace) => siteSpace.space.id === spaceItem.id
|
||||
|
||||
@@ -25,7 +25,7 @@ export function useHasBeenInViewport(
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry.isIntersecting) {
|
||||
if (entry?.isIntersecting) {
|
||||
setVisible(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
|
||||
@@ -30,8 +30,9 @@ export function useInViewportListener(
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
isIntersectingRef.current = entry.isIntersecting;
|
||||
listenerRef.current(entry.isIntersecting, () => {
|
||||
const isIntersecting = entry?.isIntersecting ?? false;
|
||||
isIntersectingRef.current = isIntersecting;
|
||||
listenerRef.current(isIntersecting, () => {
|
||||
observer.disconnect();
|
||||
});
|
||||
},
|
||||
|
||||
@@ -5,19 +5,22 @@ import React from 'react';
|
||||
*/
|
||||
export function useScrollActiveId(
|
||||
ids: string[],
|
||||
options: {
|
||||
{
|
||||
rootMargin,
|
||||
threshold = 0.5,
|
||||
enabled,
|
||||
}: {
|
||||
rootMargin?: string;
|
||||
threshold?: number;
|
||||
enabled: boolean;
|
||||
} = { enabled: true }
|
||||
) {
|
||||
const { rootMargin, threshold = 0.5, enabled } = options;
|
||||
|
||||
const [activeId, setActiveId] = React.useState<string>(ids[0]);
|
||||
const [activeId, setActiveId] = React.useState<string>(ids[0]!);
|
||||
const sectionsIntersectingMap = React.useRef<Map<string, boolean>>(new Map());
|
||||
|
||||
React.useEffect(() => {
|
||||
const defaultActiveId = ids[0];
|
||||
// @ts-expect-error
|
||||
setActiveId((activeId) => (ids.indexOf(activeId) !== -1 ? activeId : defaultActiveId));
|
||||
if (!enabled) {
|
||||
return;
|
||||
|
||||
@@ -41,8 +41,8 @@ export function ZoomImage(
|
||||
// Since the image is removed from the DOM when the modal is opened,
|
||||
// We only care when the size is defined.
|
||||
if (imgEntry && imgEntry.contentRect.width !== 0) {
|
||||
viewWidth = entries[0]?.contentRect.width;
|
||||
setPlaceholderRect(entries[0].contentRect);
|
||||
viewWidth = imgEntry.contentRect.width;
|
||||
setPlaceholderRect(imgEntry.contentRect);
|
||||
onChange();
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3,201 +3,180 @@ import type { CustomizationFontDefinition } from '@gitbook/api';
|
||||
import stylelint from 'stylelint';
|
||||
import { generateFontFacesCSS, getFontSourcesToPreload } from './custom';
|
||||
|
||||
const TEST_FONTS: { [key in string]: CustomizationFontDefinition } = {
|
||||
basic: {
|
||||
id: 'open-sans',
|
||||
custom: true,
|
||||
fontFamily: 'Open Sans',
|
||||
fontFaces: [
|
||||
{
|
||||
weight: 400,
|
||||
sources: [
|
||||
{
|
||||
url: 'https://example.com/fonts/opensans-regular.woff2',
|
||||
format: 'woff2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
weight: 700,
|
||||
sources: [
|
||||
{
|
||||
url: 'https://example.com/fonts/opensans-bold.woff2',
|
||||
format: 'woff2',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
permissions: {
|
||||
edit: false,
|
||||
const TEST_FONTS_BASIC: CustomizationFontDefinition = {
|
||||
id: 'open-sans',
|
||||
custom: true,
|
||||
fontFamily: 'Open Sans',
|
||||
fontFaces: [
|
||||
{
|
||||
weight: 400,
|
||||
sources: [
|
||||
{
|
||||
url: 'https://example.com/fonts/opensans-regular.woff2',
|
||||
format: 'woff2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
weight: 700,
|
||||
sources: [
|
||||
{
|
||||
url: 'https://example.com/fonts/opensans-bold.woff2',
|
||||
format: 'woff2',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
permissions: {
|
||||
edit: false,
|
||||
},
|
||||
};
|
||||
|
||||
multiWeight: {
|
||||
id: 'roboto',
|
||||
custom: true,
|
||||
fontFamily: 'Roboto',
|
||||
fontFaces: [
|
||||
{
|
||||
weight: 300,
|
||||
sources: [
|
||||
{
|
||||
url: 'https://example.com/fonts/roboto-light.woff2',
|
||||
format: 'woff2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
weight: 400,
|
||||
sources: [
|
||||
{
|
||||
url: 'https://example.com/fonts/roboto-regular.woff2',
|
||||
format: 'woff2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
weight: 500,
|
||||
sources: [
|
||||
{
|
||||
url: 'https://example.com/fonts/roboto-medium.woff2',
|
||||
format: 'woff2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
weight: 700,
|
||||
sources: [
|
||||
{
|
||||
url: 'https://example.com/fonts/roboto-bold.woff2',
|
||||
format: 'woff2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
weight: 900,
|
||||
sources: [
|
||||
{
|
||||
url: 'https://example.com/fonts/roboto-black.woff2',
|
||||
format: 'woff2',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
permissions: {
|
||||
edit: false,
|
||||
const TEST_FONTS_MULTI_WEIGHT: CustomizationFontDefinition = {
|
||||
id: 'roboto',
|
||||
custom: true,
|
||||
fontFamily: 'Roboto',
|
||||
fontFaces: [
|
||||
{
|
||||
weight: 300,
|
||||
sources: [
|
||||
{
|
||||
url: 'https://example.com/fonts/roboto-light.woff2',
|
||||
format: 'woff2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
weight: 400,
|
||||
sources: [
|
||||
{
|
||||
url: 'https://example.com/fonts/roboto-regular.woff2',
|
||||
format: 'woff2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
weight: 500,
|
||||
sources: [
|
||||
{
|
||||
url: 'https://example.com/fonts/roboto-medium.woff2',
|
||||
format: 'woff2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
weight: 700,
|
||||
sources: [
|
||||
{
|
||||
url: 'https://example.com/fonts/roboto-bold.woff2',
|
||||
format: 'woff2',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
weight: 900,
|
||||
sources: [
|
||||
{
|
||||
url: 'https://example.com/fonts/roboto-black.woff2',
|
||||
format: 'woff2',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
permissions: {
|
||||
edit: false,
|
||||
},
|
||||
};
|
||||
|
||||
multiSource: {
|
||||
id: 'lato',
|
||||
custom: true,
|
||||
fontFamily: 'Lato',
|
||||
fontFaces: [
|
||||
{
|
||||
weight: 400,
|
||||
sources: [
|
||||
{
|
||||
url: 'https://example.com/fonts/lato-regular.woff2',
|
||||
format: 'woff2',
|
||||
},
|
||||
{ url: 'https://example.com/fonts/lato-regular.woff', format: 'woff' },
|
||||
],
|
||||
},
|
||||
],
|
||||
permissions: {
|
||||
edit: false,
|
||||
const TEST_FONTS_MULTI_SOURCE: CustomizationFontDefinition = {
|
||||
id: 'lato',
|
||||
custom: true,
|
||||
fontFamily: 'Lato',
|
||||
fontFaces: [
|
||||
{
|
||||
weight: 400,
|
||||
sources: [
|
||||
{
|
||||
url: 'https://example.com/fonts/lato-regular.woff2',
|
||||
format: 'woff2',
|
||||
},
|
||||
{ url: 'https://example.com/fonts/lato-regular.woff', format: 'woff' },
|
||||
],
|
||||
},
|
||||
],
|
||||
permissions: {
|
||||
edit: false,
|
||||
},
|
||||
};
|
||||
|
||||
missingFormat: {
|
||||
id: 'source-sans',
|
||||
custom: true,
|
||||
fontFamily: 'Source Sans Pro',
|
||||
fontFaces: [
|
||||
{
|
||||
weight: 400,
|
||||
sources: [
|
||||
{ url: 'https://example.com/fonts/sourcesans-regular.woff2' },
|
||||
{
|
||||
url: 'https://example.com/fonts/sourcesans-regular.woff',
|
||||
format: 'woff',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
permissions: {
|
||||
edit: false,
|
||||
const TEST_FONTS_MISSING_FORMAT: CustomizationFontDefinition = {
|
||||
id: 'source-sans',
|
||||
custom: true,
|
||||
fontFamily: 'Source Sans Pro',
|
||||
fontFaces: [
|
||||
{
|
||||
weight: 400,
|
||||
sources: [
|
||||
{ url: 'https://example.com/fonts/sourcesans-regular.woff2' },
|
||||
{
|
||||
url: 'https://example.com/fonts/sourcesans-regular.woff',
|
||||
format: 'woff',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
permissions: {
|
||||
edit: false,
|
||||
},
|
||||
};
|
||||
|
||||
empty: {
|
||||
id: 'empty-font',
|
||||
custom: true,
|
||||
fontFamily: 'Empty Font',
|
||||
fontFaces: [],
|
||||
permissions: {
|
||||
edit: false,
|
||||
},
|
||||
const TEST_FONTS_EMPTY: CustomizationFontDefinition = {
|
||||
id: 'empty-font',
|
||||
custom: true,
|
||||
fontFamily: 'Empty Font',
|
||||
fontFaces: [],
|
||||
permissions: {
|
||||
edit: false,
|
||||
},
|
||||
};
|
||||
|
||||
specialChars: {
|
||||
id: 'special-font',
|
||||
custom: true,
|
||||
fontFamily: 'Special Font & Co.',
|
||||
fontFaces: [
|
||||
{
|
||||
weight: 400,
|
||||
sources: [{ url: 'https://example.com/fonts/special.woff2', format: 'woff2' }],
|
||||
},
|
||||
],
|
||||
permissions: {
|
||||
edit: false,
|
||||
const TEST_FONTS_SPECIAL_CHARS: CustomizationFontDefinition = {
|
||||
id: 'special-font',
|
||||
custom: true,
|
||||
fontFamily: 'Special Font & Co.',
|
||||
fontFaces: [
|
||||
{
|
||||
weight: 400,
|
||||
sources: [{ url: 'https://example.com/fonts/special.woff2', format: 'woff2' }],
|
||||
},
|
||||
],
|
||||
permissions: {
|
||||
edit: false,
|
||||
},
|
||||
};
|
||||
|
||||
complex: {
|
||||
id: 'complex-font',
|
||||
custom: true,
|
||||
fontFamily: 'Complex Font',
|
||||
fontFaces: [
|
||||
{
|
||||
weight: 400,
|
||||
sources: [
|
||||
{ url: 'https://example.com/fonts/regular.woff2' },
|
||||
{ url: 'https://example.com/fonts/regular.woff' },
|
||||
],
|
||||
},
|
||||
{
|
||||
weight: 700,
|
||||
sources: [
|
||||
{ url: 'https://example.com/fonts/bold.woff2' },
|
||||
{ url: 'https://example.com/fonts/bold.woff' },
|
||||
],
|
||||
},
|
||||
],
|
||||
permissions: {
|
||||
edit: false,
|
||||
const TEST_FONTS_COMPLEX: CustomizationFontDefinition = {
|
||||
id: 'complex-font',
|
||||
custom: true,
|
||||
fontFamily: 'Complex Font',
|
||||
fontFaces: [
|
||||
{
|
||||
weight: 400,
|
||||
sources: [
|
||||
{ url: 'https://example.com/fonts/regular.woff2' },
|
||||
{ url: 'https://example.com/fonts/regular.woff' },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
variousURLs: {
|
||||
id: 'various-urls',
|
||||
custom: true,
|
||||
fontFamily: 'Various URLs Font',
|
||||
fontFaces: [
|
||||
{
|
||||
weight: 400,
|
||||
sources: [
|
||||
{ url: 'https://example.com/fonts.woff2' },
|
||||
{ url: 'https://example.com/fonts.woff' },
|
||||
{ url: 'https://example.com/fonts.woff2' },
|
||||
],
|
||||
},
|
||||
],
|
||||
permissions: {
|
||||
edit: false,
|
||||
{
|
||||
weight: 700,
|
||||
sources: [
|
||||
{ url: 'https://example.com/fonts/bold.woff2' },
|
||||
{ url: 'https://example.com/fonts/bold.woff' },
|
||||
],
|
||||
},
|
||||
],
|
||||
permissions: {
|
||||
edit: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -229,7 +208,7 @@ async function isCSSValid(css: string): Promise<boolean> {
|
||||
|
||||
describe('generateFontFacesCSS', () => {
|
||||
test('basic case with regular and bold weights', async () => {
|
||||
const css = generateFontFacesCSS(TEST_FONTS.basic, 'content');
|
||||
const css = generateFontFacesCSS(TEST_FONTS_BASIC, 'content');
|
||||
|
||||
const isValid = await isCSSValid(css);
|
||||
expect(isValid).toBe(true);
|
||||
@@ -244,7 +223,7 @@ describe('generateFontFacesCSS', () => {
|
||||
});
|
||||
|
||||
test('mono type', async () => {
|
||||
const css = generateFontFacesCSS(TEST_FONTS.basic, 'mono');
|
||||
const css = generateFontFacesCSS(TEST_FONTS_BASIC, 'mono');
|
||||
|
||||
const isValid = await isCSSValid(css);
|
||||
expect(isValid).toBe(true);
|
||||
@@ -259,7 +238,7 @@ describe('generateFontFacesCSS', () => {
|
||||
});
|
||||
|
||||
test('multiple font weights', async () => {
|
||||
const css = generateFontFacesCSS(TEST_FONTS.multiWeight, 'content');
|
||||
const css = generateFontFacesCSS(TEST_FONTS_MULTI_WEIGHT, 'content');
|
||||
|
||||
const isValid = await isCSSValid(css);
|
||||
expect(isValid).toBe(true);
|
||||
@@ -270,7 +249,7 @@ describe('generateFontFacesCSS', () => {
|
||||
});
|
||||
|
||||
test('multiple sources for a single weight', async () => {
|
||||
const css = generateFontFacesCSS(TEST_FONTS.multiSource, 'content');
|
||||
const css = generateFontFacesCSS(TEST_FONTS_MULTI_SOURCE, 'content');
|
||||
|
||||
const isValid = await isCSSValid(css);
|
||||
expect(isValid).toBe(true);
|
||||
@@ -281,7 +260,7 @@ describe('generateFontFacesCSS', () => {
|
||||
});
|
||||
|
||||
test('missing format property', async () => {
|
||||
const css = generateFontFacesCSS(TEST_FONTS.missingFormat, 'content');
|
||||
const css = generateFontFacesCSS(TEST_FONTS_MISSING_FORMAT, 'content');
|
||||
|
||||
const isValid = await isCSSValid(css);
|
||||
expect(isValid).toBe(true);
|
||||
@@ -292,13 +271,13 @@ describe('generateFontFacesCSS', () => {
|
||||
});
|
||||
|
||||
test('empty font faces array', async () => {
|
||||
const css = generateFontFacesCSS(TEST_FONTS.empty, 'content');
|
||||
const css = generateFontFacesCSS(TEST_FONTS_EMPTY, 'content');
|
||||
|
||||
expect(css).toBe('');
|
||||
});
|
||||
|
||||
test('font with special characters in name', async () => {
|
||||
const css = generateFontFacesCSS(TEST_FONTS.specialChars, 'content');
|
||||
const css = generateFontFacesCSS(TEST_FONTS_SPECIAL_CHARS, 'content');
|
||||
|
||||
// Validate CSS syntax
|
||||
const isValid = await isCSSValid(css);
|
||||
@@ -308,13 +287,13 @@ describe('generateFontFacesCSS', () => {
|
||||
|
||||
describe('getFontSourcesToPreload', () => {
|
||||
const preloadTestCases = [
|
||||
{ name: 'basic case', font: TEST_FONTS.basic, expectedCount: 2 },
|
||||
{ name: 'multiple weights', font: TEST_FONTS.multiWeight, expectedCount: 2 },
|
||||
{ name: 'multiple sources', font: TEST_FONTS.multiSource, expectedCount: 2 },
|
||||
{ name: 'missing format', font: TEST_FONTS.missingFormat, expectedCount: 2 },
|
||||
{ name: 'empty font faces', font: TEST_FONTS.empty, expectedCount: 0 },
|
||||
{ name: 'special characters', font: TEST_FONTS.specialChars, expectedCount: 1 },
|
||||
{ name: 'complex case', font: TEST_FONTS.complex, expectedCount: 4 },
|
||||
{ name: 'basic case', font: TEST_FONTS_BASIC, expectedCount: 2 },
|
||||
{ name: 'multiple weights', font: TEST_FONTS_MULTI_WEIGHT, expectedCount: 2 },
|
||||
{ name: 'multiple sources', font: TEST_FONTS_MULTI_SOURCE, expectedCount: 2 },
|
||||
{ name: 'missing format', font: TEST_FONTS_MISSING_FORMAT, expectedCount: 2 },
|
||||
{ name: 'empty font faces', font: TEST_FONTS_EMPTY, expectedCount: 0 },
|
||||
{ name: 'special characters', font: TEST_FONTS_SPECIAL_CHARS, expectedCount: 1 },
|
||||
{ name: 'complex case', font: TEST_FONTS_COMPLEX, expectedCount: 4 },
|
||||
];
|
||||
|
||||
preloadTestCases.forEach(({ name, font, expectedCount }) => {
|
||||
|
||||
@@ -28,6 +28,7 @@ export function t(
|
||||
const [partToPush, partToReplace] = currentStringToReplace.split(`\${${i + 1}}`);
|
||||
parts.push(<React.Fragment key={`string-${i}`}>{partToPush}</React.Fragment>);
|
||||
parts.push(<React.Fragment key={`arg-${i}`}>{arg}</React.Fragment>);
|
||||
// @ts-expect-error
|
||||
currentStringToReplace = partToReplace;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -195,7 +195,7 @@ export async function fetchSiteContextByURLLookup(
|
||||
baseContext: GitBookBaseContext,
|
||||
data: SiteURLData
|
||||
): Promise<GitBookSiteContext> {
|
||||
return await fetchSiteContextByIds(baseContext, {
|
||||
return fetchSiteContextByIds(baseContext, {
|
||||
organization: data.organization,
|
||||
site: data.site,
|
||||
siteSection: data.siteSection,
|
||||
|
||||
@@ -111,7 +111,7 @@ export function getURLLookupAlternatives(input: URL) {
|
||||
// Mark the longuest entry to lookup as primary
|
||||
alternatives.sort((a, b) => b.extraPath.length - a.extraPath.length);
|
||||
if (alternatives.length > 0) {
|
||||
alternatives[alternatives.length - 1].primary = true;
|
||||
alternatives[alternatives.length - 1]!.primary = true;
|
||||
}
|
||||
|
||||
return { urls: alternatives, basePath, changeRequest, revision };
|
||||
|
||||
@@ -110,7 +110,7 @@ function isEmptyMarkdownPage(markdown: string): boolean {
|
||||
if (
|
||||
node.type === 'paragraph' &&
|
||||
node.children.length === 1 &&
|
||||
node.children[0].type === 'text' &&
|
||||
node.children[0]!.type === 'text' &&
|
||||
!node.children[0].value.trim()
|
||||
) {
|
||||
continue;
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('#enrichFilesystem', () => {
|
||||
rootURL: null,
|
||||
});
|
||||
const enriched = await enrichFilesystem(filesystem);
|
||||
expect(enriched[0].specification.paths['/pet'].put['x-gitbook-description-html']).toBe(
|
||||
expect(enriched[0]?.specification.paths['/pet'].put['x-gitbook-description-html']).toBe(
|
||||
'<p>Social platform</p>'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,5 +9,5 @@ export function getPreviewRequestIdentifier(requestURL: URL): string {
|
||||
// For preview requests, we extract the site ID from the pathname
|
||||
// e.g. https://preview/site_id/...
|
||||
const pathname = requestURL.pathname.slice(1).split('/');
|
||||
return pathname[0];
|
||||
return pathname[0]!;
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ export function getVisitorUnsignedClaims(args: {
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
Object.assign(claims, parsed);
|
||||
}
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
console.warn(`Invalid JSON in unsigned claim cookie "${cookie.name}"`);
|
||||
}
|
||||
}
|
||||
@@ -188,7 +188,7 @@ function setVisitorClaimByPath(
|
||||
let current = claims;
|
||||
|
||||
for (let index = 0; index < keys.length; index++) {
|
||||
const key = keys[index];
|
||||
const key = keys[index]!;
|
||||
|
||||
if (index === keys.length - 1) {
|
||||
current[key] = value;
|
||||
|
||||
@@ -337,8 +337,6 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
pathname,
|
||||
].join('/');
|
||||
|
||||
console.log(`rewriting ${request.nextUrl.toString()} to ${route}`);
|
||||
|
||||
const rewrittenURL = new URL(`/${route}`, request.nextUrl.toString());
|
||||
rewrittenURL.search = request.nextUrl.search; // Preserve the original search params
|
||||
|
||||
@@ -541,7 +539,7 @@ function encodePathInSiteContent(rawPathname: string): {
|
||||
const embedPage = pathname.match(/^~gitbook\/embed\/page\/(\S+)$/);
|
||||
if (embedPage) {
|
||||
return {
|
||||
pathname: `~gitbook/embed/page/${encodeURIComponent(embedPage[1])}`,
|
||||
pathname: `~gitbook/embed/page/${encodeURIComponent(embedPage[1]!)}`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
|
||||
body: colorContrast(
|
||||
customization.header.backgroundColor?.[theme] || colors.background,
|
||||
[baseColors.light, baseColors.dark]
|
||||
),
|
||||
)!,
|
||||
};
|
||||
gridAsset = colors.body === baseColors.light ? gridWhite : gridBlack;
|
||||
break;
|
||||
@@ -134,15 +134,15 @@ export async function serveOGImage(baseContext: GitBookSiteContext, params: Page
|
||||
gradient: colorContrast(customization.styling.primaryColor[theme], [
|
||||
baseColors.light,
|
||||
baseColors.dark,
|
||||
]),
|
||||
])!,
|
||||
title: colorContrast(customization.styling.primaryColor[theme], [
|
||||
baseColors.light,
|
||||
baseColors.dark,
|
||||
]),
|
||||
])!,
|
||||
body: colorContrast(customization.styling.primaryColor[theme], [
|
||||
baseColors.light,
|
||||
baseColors.dark,
|
||||
]),
|
||||
])!,
|
||||
};
|
||||
gridAsset = colors.body === baseColors.light ? gridWhite : gridBlack;
|
||||
break;
|
||||
|
||||
@@ -36,14 +36,13 @@ function generateVarShades(varName: string, filter: ColorCategory[] = []) {
|
||||
*/
|
||||
function generateShades(color: string) {
|
||||
const rawShades = shadesOfColor(color);
|
||||
const shadeMap = shades.reduce(
|
||||
(acc, shade) => {
|
||||
acc[shade] = `rgb(${hexToRgb(rawShades[`${shade}`])} / <alpha-value>)`;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
const shadeMap = shades.reduce((acc: Record<string, string>, shade) => {
|
||||
// @ts-expect-error
|
||||
acc[shade] = `rgb(${hexToRgb(rawShades[`${shade}`])} / <alpha-value>)`;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// @ts-expect-error
|
||||
shadeMap.DEFAULT = shadeMap[500];
|
||||
|
||||
return shadeMap;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"module": "esnext",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"module": "ESNext",
|
||||
"target": "es2022",
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
|
||||
Reference in New Issue
Block a user