@terrafirmagreg-team/kit 1.2.8 → 1.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/components/blocks/nav-title.tsx +4 -0
- package/src/components/dump-status/stale-badge.tsx +18 -0
- package/src/components/layouts/layout.tsx +10 -3
- package/src/components/views/not-found.tsx +43 -8
- package/src/components/views/routes.tsx +8 -6
- package/src/components/views/unavailable.tsx +29 -0
- package/src/hooks/use-dump-status.ts +32 -0
- package/src/hooks/use-nav-options.ts +7 -4
- package/src/lib/datasets.ts +5 -3
- package/src/lib/locale/renderer.ts +8 -6
- package/src/lib/toc-title.ts +22 -0
- package/src/types/features.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terrafirmagreg-team/kit",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Fumadocs content engine",
|
|
6
6
|
"license": "LGPL-3.0-only",
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@base-ui/react": "^1.9.0",
|
|
26
26
|
"@icons-pack/react-simple-icons": "13.15.1",
|
|
27
|
-
"@terrafirmagreg-team/data": "^1.2.
|
|
28
|
-
"@terrafirmagreg-team/mdx": "^1.2.
|
|
27
|
+
"@terrafirmagreg-team/data": "^1.2.2",
|
|
28
|
+
"@terrafirmagreg-team/mdx": "^1.2.4",
|
|
29
29
|
"@terrafirmagreg-team/sites": "^1.2.1",
|
|
30
|
-
"@terrafirmagreg-team/translate": "^1.2.
|
|
31
|
-
"@terrafirmagreg-team/ui": "^1.2.
|
|
30
|
+
"@terrafirmagreg-team/translate": "^1.2.6",
|
|
31
|
+
"@terrafirmagreg-team/ui": "^1.2.7",
|
|
32
32
|
"fumadocs-core": "16.17.10",
|
|
33
33
|
"lucide-react": "^1.63.0",
|
|
34
34
|
"server-only": "^0.0.1",
|
|
@@ -13,10 +13,12 @@ import {
|
|
|
13
13
|
} from '@terrafirmagreg-team/sites/location';
|
|
14
14
|
import { setVersionPreference } from '@terrafirmagreg-team/kit/hooks/use-version-pref';
|
|
15
15
|
import { useVersionedDataset } from '@terrafirmagreg-team/kit/hooks/use-versioned-dataset';
|
|
16
|
+
import { useSectionNames } from '@terrafirmagreg-team/kit/hooks/use-section-names';
|
|
16
17
|
import { VersionMenu } from '@terrafirmagreg-team/kit/components/version-menu';
|
|
17
18
|
|
|
18
19
|
export function NavTitle({ className, ...props }: ComponentProps<'a'>) {
|
|
19
20
|
const { section, dataset } = parseSectionLocation(usePathname());
|
|
21
|
+
const sectionNames = useSectionNames();
|
|
20
22
|
const datasetId = dataset;
|
|
21
23
|
|
|
22
24
|
const gameData = isDumpSection(section) && datasetId;
|
|
@@ -52,6 +54,8 @@ export function NavTitle({ className, ...props }: ComponentProps<'a'>) {
|
|
|
52
54
|
versions={versions}
|
|
53
55
|
onSelect={(next) => setVersionPreference(datasetId, next)}
|
|
54
56
|
/>
|
|
57
|
+
) : section ? (
|
|
58
|
+
sectionNames[section]?.title
|
|
55
59
|
) : undefined
|
|
56
60
|
}
|
|
57
61
|
className={className}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
|
|
4
|
+
import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
|
|
5
|
+
|
|
6
|
+
export function StaleBadge({ className }: { className?: string }) {
|
|
7
|
+
const t = useTranslations();
|
|
8
|
+
return (
|
|
9
|
+
<span
|
|
10
|
+
className={cn(
|
|
11
|
+
'pointer-events-none absolute end-3 top-3 z-20 rounded-full border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-[11px] font-medium text-amber-700 dark:text-amber-300',
|
|
12
|
+
className,
|
|
13
|
+
)}
|
|
14
|
+
>
|
|
15
|
+
{t('cached', { note: 'kit.stale_badge' })}
|
|
16
|
+
</span>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -37,6 +37,7 @@ import { LangLocalesScope } from '@terrafirmagreg-team/translate/provider';
|
|
|
37
37
|
import { localeItems } from '@terrafirmagreg-team/translate/create';
|
|
38
38
|
import { siteLinks } from '@terrafirmagreg-team/sites';
|
|
39
39
|
import { useCurrentDataset } from '@terrafirmagreg-team/kit/hooks/use-current-dataset';
|
|
40
|
+
import { useFailedDatasets } from '@terrafirmagreg-team/kit/hooks/use-dump-status';
|
|
40
41
|
import { useDatasetAccent } from '@terrafirmagreg-team/kit/hooks/use-dataset-accent';
|
|
41
42
|
import { useNavOptions } from '@terrafirmagreg-team/kit/hooks/use-nav-options';
|
|
42
43
|
import { pinTreeDataset } from '@terrafirmagreg-team/kit/lib/pin-tree';
|
|
@@ -57,6 +58,7 @@ export type LayoutProps = {
|
|
|
57
58
|
useTree?: (tree: PageTree.Root) => PageTree.Root;
|
|
58
59
|
sidebar?: Pick<LeftSidebarSlotProps, 'feature'>;
|
|
59
60
|
navTitle?: ComponentType<ComponentProps<'a'>>;
|
|
61
|
+
defaultOpenLevel?: number;
|
|
60
62
|
children?: ReactNode;
|
|
61
63
|
};
|
|
62
64
|
|
|
@@ -78,15 +80,19 @@ export function Layout({
|
|
|
78
80
|
useTree = identityTree,
|
|
79
81
|
sidebar,
|
|
80
82
|
navTitle,
|
|
83
|
+
defaultOpenLevel,
|
|
81
84
|
children,
|
|
82
85
|
}: LayoutProps) {
|
|
83
86
|
const t = useTranslations();
|
|
84
87
|
const { defaultLanguage } = useLocaleRegistry();
|
|
85
88
|
const lang: Lang = useResolvedLang();
|
|
86
|
-
const
|
|
89
|
+
const sectionDatasets = catalog[section] ?? [];
|
|
90
|
+
const current = useCurrentDataset(sectionDatasets);
|
|
87
91
|
const baseId = current ? current.id : undefined;
|
|
88
92
|
const pinned = current ? current.version : undefined;
|
|
89
|
-
const meta =
|
|
93
|
+
const meta = sectionDatasets.find((item) => item.id === baseId);
|
|
94
|
+
const failedDatasets = useFailedDatasets();
|
|
95
|
+
const featureHidden = baseId !== undefined && failedDatasets.includes(baseId);
|
|
90
96
|
useDatasetAccent(meta?.accent);
|
|
91
97
|
const nav = useNavOptions({ section, catalog });
|
|
92
98
|
const pathname = usePathname();
|
|
@@ -139,6 +145,7 @@ export function Layout({
|
|
|
139
145
|
<UiLayout
|
|
140
146
|
tree={pinnedTree}
|
|
141
147
|
navTitle={navTitle}
|
|
148
|
+
defaultOpenLevel={defaultOpenLevel}
|
|
142
149
|
sidebar={
|
|
143
150
|
<LeftSidebar
|
|
144
151
|
navKeys={NAV_KEYS}
|
|
@@ -170,7 +177,7 @@ export function Layout({
|
|
|
170
177
|
<LeftSidebarSearch />
|
|
171
178
|
<Switcher {...nav.section} />
|
|
172
179
|
<Switcher {...nav.dataset} />
|
|
173
|
-
{Feature ? (
|
|
180
|
+
{Feature && !featureHidden ? (
|
|
174
181
|
<>
|
|
175
182
|
<LeftSidebarSeparator />
|
|
176
183
|
<Feature />
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
2
3
|
import { FileQuestion } from 'lucide-react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
3
5
|
import { sectionUrl } from '@terrafirmagreg-team/sites/location';
|
|
4
6
|
import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
|
|
5
7
|
import { LangLink } from '@terrafirmagreg-team/ui/components/blocks/lang/link';
|
|
6
8
|
import { EmptyState } from '@terrafirmagreg-team/ui/components/blocks/page/empty-state';
|
|
9
|
+
import { buttonVariants } from '@terrafirmagreg-team/ui/components/ui/button';
|
|
10
|
+
import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
|
|
11
|
+
import { requestDumpRetry } from '@terrafirmagreg-team/data/lib/dump-status';
|
|
12
|
+
import { useDumpStatus } from '@terrafirmagreg-team/kit/hooks/use-dump-status';
|
|
7
13
|
import type { SectionDataset, SectionKey } from '@terrafirmagreg-team/kit/types';
|
|
8
14
|
|
|
9
15
|
export function SectionNotFound({
|
|
@@ -14,24 +20,53 @@ export function SectionNotFound({
|
|
|
14
20
|
datasets: readonly SectionDataset[];
|
|
15
21
|
}) {
|
|
16
22
|
const t = useTranslations();
|
|
23
|
+
const router = useRouter();
|
|
24
|
+
const { statuses } = useDumpStatus();
|
|
25
|
+
const hadFailure = useRef(false);
|
|
26
|
+
const anyFailed = Object.values(statuses).some(
|
|
27
|
+
(state) => state.status === 'failed',
|
|
28
|
+
);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (anyFailed) {
|
|
31
|
+
hadFailure.current = true;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (hadFailure.current && Object.keys(statuses).length) {
|
|
35
|
+
hadFailure.current = false;
|
|
36
|
+
router.refresh();
|
|
37
|
+
}
|
|
38
|
+
}, [anyFailed, statuses, router]);
|
|
39
|
+
|
|
17
40
|
if (!datasets.length)
|
|
18
41
|
return (
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
42
|
+
<div className="flex flex-col items-center gap-3 py-16">
|
|
43
|
+
<EmptyState variant="missing">
|
|
44
|
+
{t('Section data is temporarily unavailable. Try again later.', {
|
|
45
|
+
note: 'kit.section_unavailable',
|
|
46
|
+
})}
|
|
47
|
+
</EmptyState>
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
onClick={requestDumpRetry}
|
|
51
|
+
className={cn(buttonVariants({ variant: 'outline', size: 'xs' }))}
|
|
52
|
+
>
|
|
53
|
+
{t('Retry now', { note: 'kit.retry_now' })}
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
24
56
|
);
|
|
25
57
|
return (
|
|
26
|
-
<
|
|
58
|
+
<div className="flex flex-col items-center gap-3 py-16 text-muted-foreground">
|
|
27
59
|
<FileQuestion className="size-10" />
|
|
28
60
|
<p>{t('Page not found', { note: 'ui.not_found' })}</p>
|
|
29
61
|
<LangLink
|
|
30
62
|
href={sectionUrl(section, datasets[0].id)}
|
|
31
|
-
className=
|
|
63
|
+
className={cn(
|
|
64
|
+
buttonVariants({ variant: 'outline', size: 'xs' }),
|
|
65
|
+
'no-underline',
|
|
66
|
+
)}
|
|
32
67
|
>
|
|
33
68
|
{t('Back', { note: 'ui.back' })}
|
|
34
69
|
</LangLink>
|
|
35
|
-
</
|
|
70
|
+
</div>
|
|
36
71
|
);
|
|
37
72
|
}
|
|
@@ -193,8 +193,8 @@ export function createSectionRoutes<
|
|
|
193
193
|
render(
|
|
194
194
|
await source.getSource(),
|
|
195
195
|
slug,
|
|
196
|
-
await trees?.(),
|
|
197
|
-
await datasetCatalog(),
|
|
196
|
+
await trees?.().catch(() => undefined),
|
|
197
|
+
await datasetCatalog().catch((): DatasetCatalog => ({}) as DatasetCatalog),
|
|
198
198
|
page,
|
|
199
199
|
lang,
|
|
200
200
|
),
|
|
@@ -203,8 +203,10 @@ export function createSectionRoutes<
|
|
|
203
203
|
async function SectionLayout({ children }: { children?: ReactNode }) {
|
|
204
204
|
const { slots } = baseOptions();
|
|
205
205
|
const [resolvedTrees, catalog] = await Promise.all([
|
|
206
|
-
trees
|
|
207
|
-
|
|
206
|
+
trees
|
|
207
|
+
? trees().catch((): SectionTrees => ({}))
|
|
208
|
+
: Promise.resolve<SectionTrees>({}),
|
|
209
|
+
datasetCatalog().catch((): DatasetCatalog => ({}) as DatasetCatalog),
|
|
208
210
|
]);
|
|
209
211
|
return (
|
|
210
212
|
<LayoutView
|
|
@@ -237,8 +239,8 @@ export function createSectionRoutes<
|
|
|
237
239
|
}
|
|
238
240
|
|
|
239
241
|
const [resolvedTrees, catalog] = await Promise.all([
|
|
240
|
-
trees?.(),
|
|
241
|
-
datasetCatalog(),
|
|
242
|
+
trees?.().catch(() => undefined),
|
|
243
|
+
datasetCatalog().catch((): DatasetCatalog => ({}) as DatasetCatalog),
|
|
242
244
|
]);
|
|
243
245
|
const content = langContent(
|
|
244
246
|
slug,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { useTranslations } from '@terrafirmagreg-team/translate/hooks/use-translations';
|
|
5
|
+
import { requestDumpRetry } from '@terrafirmagreg-team/data/lib/dump-status';
|
|
6
|
+
import { EmptyState } from '@terrafirmagreg-team/ui/components/blocks/page/empty-state';
|
|
7
|
+
import { buttonVariants } from '@terrafirmagreg-team/ui/components/ui/button';
|
|
8
|
+
import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
|
|
9
|
+
|
|
10
|
+
export function DumpUnavailable({ children }: { children?: ReactNode }) {
|
|
11
|
+
const t = useTranslations();
|
|
12
|
+
return (
|
|
13
|
+
<div className="flex flex-col items-center gap-3 py-10">
|
|
14
|
+
<EmptyState variant="missing">
|
|
15
|
+
{children ??
|
|
16
|
+
t('Section data is temporarily unavailable. Try again later.', {
|
|
17
|
+
note: 'kit.section_unavailable',
|
|
18
|
+
})}
|
|
19
|
+
</EmptyState>
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
onClick={requestDumpRetry}
|
|
23
|
+
className={cn(buttonVariants({ variant: 'outline', size: 'xs' }))}
|
|
24
|
+
>
|
|
25
|
+
{t('Retry now', { note: 'kit.retry_now' })}
|
|
26
|
+
</button>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useSyncExternalStore } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
dumpStatusSnapshot,
|
|
6
|
+
subscribeDumpStatus,
|
|
7
|
+
type DumpStatusSnapshot,
|
|
8
|
+
} from '@terrafirmagreg-team/data/lib/dump-status';
|
|
9
|
+
|
|
10
|
+
const SERVER_SNAPSHOT: DumpStatusSnapshot = {
|
|
11
|
+
statuses: {},
|
|
12
|
+
retryNonce: 0,
|
|
13
|
+
dismissed: false,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function useDumpStatus(): DumpStatusSnapshot {
|
|
17
|
+
return useSyncExternalStore(
|
|
18
|
+
subscribeDumpStatus,
|
|
19
|
+
dumpStatusSnapshot,
|
|
20
|
+
() => SERVER_SNAPSHOT,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useDumpRetryNonce(): number {
|
|
25
|
+
return useDumpStatus().retryNonce;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useFailedDatasets(): string[] {
|
|
29
|
+
return Object.entries(useDumpStatus().statuses)
|
|
30
|
+
.filter(([, state]) => state.status === 'failed')
|
|
31
|
+
.map(([id]) => id);
|
|
32
|
+
}
|
|
@@ -17,6 +17,8 @@ import type {
|
|
|
17
17
|
SwitcherProps,
|
|
18
18
|
} from '@terrafirmagreg-team/ui/types/nav';
|
|
19
19
|
|
|
20
|
+
const EMPTY_DATASETS: never[] = [];
|
|
21
|
+
|
|
20
22
|
function datasetIcon(dataset: { icon?: IconName }): ReactNode {
|
|
21
23
|
if (!dataset.icon) return undefined;
|
|
22
24
|
return createElement(DynamicIcon, {
|
|
@@ -31,7 +33,8 @@ export function useNavOptions({ section, catalog }: LeftSidebarScope): {
|
|
|
31
33
|
} {
|
|
32
34
|
const t = useTranslations();
|
|
33
35
|
const sectionNames = useSectionNames();
|
|
34
|
-
const
|
|
36
|
+
const sectionDatasets = catalog[section] ?? EMPTY_DATASETS;
|
|
37
|
+
const current = useCurrentDataset(sectionDatasets);
|
|
35
38
|
const baseId = current ? current.id : undefined;
|
|
36
39
|
const pinned = current ? current.version : undefined;
|
|
37
40
|
const preferred = useVersionPreference(baseId);
|
|
@@ -41,7 +44,7 @@ export function useNavOptions({ section, catalog }: LeftSidebarScope): {
|
|
|
41
44
|
() =>
|
|
42
45
|
sectionTabs.map(({ key, icon: Icon }) => {
|
|
43
46
|
const meta = baseId
|
|
44
|
-
? catalog[key]
|
|
47
|
+
? catalog[key]?.find(({ id }) => id === baseId)
|
|
45
48
|
: undefined;
|
|
46
49
|
const pinned =
|
|
47
50
|
isDumpSection(key) && baseId
|
|
@@ -61,14 +64,14 @@ export function useNavOptions({ section, catalog }: LeftSidebarScope): {
|
|
|
61
64
|
|
|
62
65
|
const datasetOptions: SwitcherOption[] = useMemo(
|
|
63
66
|
() =>
|
|
64
|
-
|
|
67
|
+
sectionDatasets.map((datasetItem) => ({
|
|
65
68
|
value: datasetItem.id,
|
|
66
69
|
title: datasetItem.title ?? datasetItem.id,
|
|
67
70
|
description: datasetItem.description,
|
|
68
71
|
icon: datasetIcon(datasetItem),
|
|
69
72
|
url: sectionUrl(section, datasetItem.id),
|
|
70
73
|
})),
|
|
71
|
-
[section,
|
|
74
|
+
[section, sectionDatasets],
|
|
72
75
|
);
|
|
73
76
|
|
|
74
77
|
return {
|
package/src/lib/datasets.ts
CHANGED
|
@@ -158,7 +158,7 @@ async function dumpDatasets(
|
|
|
158
158
|
const usable = await Promise.all(
|
|
159
159
|
ids.map(async (ref) => {
|
|
160
160
|
const { id } = ref;
|
|
161
|
-
const index = await dump.loadDataset(ref);
|
|
161
|
+
const index = await dump.loadDataset(ref).catch(() => null);
|
|
162
162
|
if (!index || !hasContent(index)) return null;
|
|
163
163
|
return {
|
|
164
164
|
id,
|
|
@@ -191,9 +191,10 @@ export function sectionDatasets(
|
|
|
191
191
|
section: SectionKey,
|
|
192
192
|
): Promise<SectionDataset[]> {
|
|
193
193
|
const hasContent = sectionConfig().dump?.hasContent[section];
|
|
194
|
-
|
|
194
|
+
const promise = hasContent
|
|
195
195
|
? dumpSectionDatasets(section, hasContent)
|
|
196
196
|
: contentSectionDatasetsCached(section);
|
|
197
|
+
return promise.catch(() => []);
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
export async function defaultDataset(
|
|
@@ -228,7 +229,8 @@ const datasetCatalogCached = createAsyncCache(
|
|
|
228
229
|
async (): Promise<DatasetCatalog> =>
|
|
229
230
|
Promise.all(
|
|
230
231
|
sectionTabs.map(
|
|
231
|
-
async ({ key }) =>
|
|
232
|
+
async ({ key }) =>
|
|
233
|
+
[key, await sectionDatasets(key).catch(() => [])] as const,
|
|
232
234
|
),
|
|
233
235
|
).then((rows) => Object.fromEntries(rows) as DatasetCatalog),
|
|
234
236
|
() => 'catalog',
|
|
@@ -21,10 +21,14 @@ export type LocaleLoader = (
|
|
|
21
21
|
lang: Lang,
|
|
22
22
|
) => Promise<LocaleLoadResult>;
|
|
23
23
|
|
|
24
|
-
const
|
|
24
|
+
const store = globalThis as Record<symbol, unknown>;
|
|
25
|
+
const RENDERERS_KEY = Symbol.for('terrafirmagreg.locale.renderers');
|
|
26
|
+
const LOADER_KEY = Symbol.for('terrafirmagreg.locale.loader');
|
|
27
|
+
|
|
28
|
+
const renderers = (store[RENDERERS_KEY] ??= new Map<
|
|
25
29
|
string,
|
|
26
30
|
{ resolve: LocaleResolve; render: LocaleRender }
|
|
27
|
-
>();
|
|
31
|
+
>()) as Map<string, { resolve: LocaleResolve; render: LocaleRender }>;
|
|
28
32
|
|
|
29
33
|
export function registerLocaleRenderer(
|
|
30
34
|
section: string,
|
|
@@ -37,13 +41,11 @@ export function localeRenderer(section: string) {
|
|
|
37
41
|
return renderers.get(section);
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
let loader: LocaleLoader | undefined;
|
|
41
|
-
|
|
42
44
|
export function registerLocaleLoader(next: LocaleLoader): void {
|
|
43
|
-
|
|
45
|
+
store[LOADER_KEY] = next;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
export function localeLoader(): LocaleLoader | undefined {
|
|
47
|
-
return
|
|
49
|
+
return store[LOADER_KEY] as LocaleLoader | undefined;
|
|
48
50
|
}
|
|
49
51
|
|
package/src/lib/toc-title.ts
CHANGED
|
@@ -8,6 +8,27 @@ import {
|
|
|
8
8
|
type ReactNode,
|
|
9
9
|
} from 'react';
|
|
10
10
|
|
|
11
|
+
function isPresentation(element: ReactElement): boolean {
|
|
12
|
+
const props = element.props as Record<string, unknown>;
|
|
13
|
+
if (props['data-md'] !== undefined) return true;
|
|
14
|
+
const type = element.type as { mdPresentation?: boolean } | string;
|
|
15
|
+
return typeof type !== 'string' && type.mdPresentation === true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function textOf(node: ReactNode): string {
|
|
19
|
+
return Children.toArray(node)
|
|
20
|
+
.map((child) => {
|
|
21
|
+
if (typeof child === 'string' || typeof child === 'number')
|
|
22
|
+
return String(child);
|
|
23
|
+
if (isValidElement(child)) {
|
|
24
|
+
const element = child as ReactElement<{ children?: ReactNode }>;
|
|
25
|
+
return textOf(element.props.children);
|
|
26
|
+
}
|
|
27
|
+
return '';
|
|
28
|
+
})
|
|
29
|
+
.join('');
|
|
30
|
+
}
|
|
31
|
+
|
|
11
32
|
function unwrap(node: ReactNode): ReactNode {
|
|
12
33
|
return Children.map(node, (child) => {
|
|
13
34
|
if (!isValidElement(child)) return child;
|
|
@@ -15,6 +36,7 @@ function unwrap(node: ReactNode): ReactNode {
|
|
|
15
36
|
href?: unknown;
|
|
16
37
|
children?: ReactNode;
|
|
17
38
|
}>;
|
|
39
|
+
if (isPresentation(element)) return textOf(element.props.children);
|
|
18
40
|
if (element.props.href !== undefined) {
|
|
19
41
|
return unwrap(element.props.children);
|
|
20
42
|
}
|