@xyd-js/plugin-docs 0.0.0-build

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.
@@ -0,0 +1,463 @@
1
+ import path from "node:path";
2
+
3
+ import * as React from "react";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { useMemo, useContext, ReactElement, SVGProps, useEffect } from "react";
6
+ import { redirect, ScrollRestoration, useLocation, useNavigation } from "react-router";
7
+
8
+ import { MetadataMap, Metadata, Settings } from "@xyd-js/core"
9
+ import { ContentFS } from "@xyd-js/content"
10
+ import { markdownPlugins } from "@xyd-js/content/md"
11
+ import { pageMetaLayout } from "@xyd-js/framework";
12
+ import { mapSettingsToProps } from "@xyd-js/framework/hydration";
13
+ import { FrameworkPage, type FwSidebarItemProps } from "@xyd-js/framework/react";
14
+ import type { IBreadcrumb, INavLinks } from "@xyd-js/ui";
15
+ import { UXNode } from "openux-js";
16
+
17
+ // @ts-ignore
18
+ import virtualSettings from "virtual:xyd-settings";
19
+ // @ts-ignore
20
+ const { settings } = virtualSettings as Settings
21
+
22
+ import { PageContext } from "./context";
23
+ import { SUPPORTED_META_TAGS } from "./metatags";
24
+ import { useAnalytics } from "@xyd-js/analytics";
25
+
26
+ function getPathname(url: string) {
27
+ const parsedUrl = new URL(url);
28
+ return parsedUrl.pathname.replace(/^\//, '');
29
+ }
30
+
31
+ interface loaderData {
32
+ sidebarGroups: FwSidebarItemProps[]
33
+ breadcrumbs: IBreadcrumb[],
34
+ toc: MetadataMap,
35
+ slug: string
36
+ code: string
37
+ metadata: Metadata | null
38
+ rawPage: string // TODO: in the future routing like /docs/quickstart.md but some issues with react-router like *.md in `route` config
39
+ navlinks?: INavLinks,
40
+ }
41
+
42
+ class timedebugLoader {
43
+ static get total() {
44
+ console.time('loader:total')
45
+
46
+ return
47
+ }
48
+
49
+ static get totalEnd() {
50
+ console.timeEnd('loader:total')
51
+
52
+ return
53
+ }
54
+
55
+ static get compile() {
56
+ console.time('loader:compile')
57
+
58
+ return
59
+ }
60
+
61
+ static get compileEnd() {
62
+ console.timeEnd('loader:compile')
63
+
64
+ return
65
+ }
66
+
67
+ static get mapSettingsToProps() {
68
+ console.time('loader:mapSettingsToProps')
69
+
70
+ return
71
+ }
72
+
73
+ static get mapSettingsToPropsEnd() {
74
+ console.timeEnd('loader:mapSettingsToProps')
75
+
76
+ return
77
+ }
78
+ }
79
+
80
+ export async function loader({ request }: { request: any }) {
81
+ if (!globalThis.__xydPagePathMapping) {
82
+ throw new Error("PagePathMapping not found")
83
+ }
84
+
85
+ const timedebug = timedebugLoader
86
+
87
+ timedebug.total
88
+
89
+ const slug = getPathname(request.url || "index") || "index"
90
+ if (path.extname(slug)) {
91
+ console.log(`(loader): currently not supporting file extension in slug: ${slug}`);
92
+ timedebug.totalEnd
93
+ return {}
94
+ }
95
+
96
+ timedebug.mapSettingsToProps
97
+
98
+ const {
99
+ groups: sidebarGroups,
100
+ breadcrumbs,
101
+ navlinks,
102
+ hiddenPages,
103
+ metadata
104
+ } = await mapSettingsToProps( // TOOD: we use mapSettingsToProps twice (in layout) - refactor
105
+ settings || globalThis.__xydSettings,
106
+ globalThis.__xydPagePathMapping,
107
+ slug,
108
+ )
109
+ timedebug.mapSettingsToPropsEnd
110
+
111
+ function redirectFallback() {
112
+ const fallbackUrl = findFallbackUrl(sidebarGroups, slug)
113
+
114
+ return redirect(fallbackUrl)
115
+ }
116
+
117
+ if (hiddenPages?.[slug]) {
118
+ const resp = redirectFallback()
119
+ if (resp) {
120
+ timedebug.totalEnd
121
+ return resp
122
+ }
123
+ }
124
+
125
+ let code = ""
126
+ let rawPage = ""
127
+
128
+ const mdPlugins = await markdownPlugins({
129
+ maxDepth: metadata?.maxTocDepth || settings?.theme?.writer?.maxTocDepth || 2,
130
+ }, settings)
131
+
132
+ const contentFs = new ContentFS(settings, mdPlugins.remarkPlugins, mdPlugins.rehypePlugins, mdPlugins.recmaPlugins)
133
+
134
+ const pagePath = globalThis.__xydPagePathMapping[slug]
135
+ if (pagePath) {
136
+ timedebug.compile
137
+ code = await contentFs.compile(pagePath)
138
+ rawPage = await contentFs.readRaw(pagePath)
139
+ timedebug.compileEnd
140
+ } else {
141
+ const resp = redirectFallback()
142
+ if (resp) {
143
+ timedebug.totalEnd
144
+ return resp
145
+ }
146
+ }
147
+
148
+ timedebug.totalEnd
149
+
150
+ if (metadata) {
151
+ metadata.layout = pageMetaLayout(metadata)
152
+ }
153
+
154
+ return {
155
+ sidebarGroups,
156
+ breadcrumbs,
157
+ navlinks,
158
+ slug,
159
+ code,
160
+ metadata,
161
+ rawPage,
162
+ } as loaderData
163
+ }
164
+
165
+ interface MetaTag {
166
+ title?: string
167
+
168
+ name?: string
169
+
170
+ property?: string
171
+
172
+ content?: string
173
+ }
174
+
175
+ // TODO: better SEO (use https://github.com/unjs/unhead?)
176
+ export function meta(props: any) {
177
+ const {
178
+ title = "",
179
+ description = "",
180
+ } = props?.data?.metadata || {}
181
+
182
+ const metaTags: MetaTag[] = [
183
+ { title: title },
184
+ ]
185
+
186
+ if (description) {
187
+ metaTags.push({
188
+ name: "description",
189
+ content: description,
190
+ })
191
+ }
192
+
193
+ const metaTagsMap: { [key: string]: MetaTag } = {}
194
+
195
+ for (const key in settings?.seo?.metatags) {
196
+ const metaType = SUPPORTED_META_TAGS[key]
197
+ if (!metaType) {
198
+ continue
199
+ }
200
+
201
+ if (description && key === "description") {
202
+ continue
203
+ }
204
+
205
+ metaTagsMap[key] = {
206
+ [metaType]: key,
207
+ content: settings?.seo?.metatags[key],
208
+ }
209
+ }
210
+
211
+ // TOOD: filter?
212
+ for (const key in props?.data?.metadata) {
213
+ const metaType = SUPPORTED_META_TAGS[key]
214
+ if (!metaType) {
215
+ continue
216
+ }
217
+
218
+ if (description && key === "description") {
219
+ continue
220
+ }
221
+
222
+ metaTagsMap[key] = {
223
+ [metaType]: key,
224
+ content: props?.data?.metadata[key],
225
+ }
226
+ }
227
+
228
+ if (props?.data?.metadata?.noindex) {
229
+ metaTagsMap["robots"] = {
230
+ name: "robots",
231
+ content: "noindex",
232
+ }
233
+ }
234
+
235
+ for (const key in metaTagsMap) {
236
+ metaTags.push(metaTagsMap[key])
237
+ }
238
+
239
+ return metaTags
240
+ }
241
+
242
+ function findFirstUrl(items: any = []): string {
243
+ const queue = [...items];
244
+
245
+ while (queue.length > 0) {
246
+ const item = queue.shift();
247
+
248
+ if (item.href) {
249
+ return item.href;
250
+ }
251
+
252
+ if (item.items) {
253
+ queue.push(...item.items);
254
+ }
255
+ }
256
+
257
+ return "";
258
+ }
259
+
260
+ function findFallbackUrl(sidebarGroups: FwSidebarItemProps[], currentSlug: string): string {
261
+ if (!sidebarGroups || sidebarGroups.length === 0) {
262
+ throw new Error("No sidebar groups available for fallback redirect")
263
+ }
264
+
265
+ // Iterate through all sidebar groups to find the first valid URL
266
+ for (const group of sidebarGroups) {
267
+ if (!group.items || group.items.length === 0) {
268
+ continue
269
+ }
270
+
271
+ const firstItem = findFirstUrl(group.items)
272
+
273
+ if (!firstItem) {
274
+ continue
275
+ }
276
+
277
+ // Avoid infinite redirects by checking if the found URL is the same as current slug
278
+ if (sanitizeUrl(firstItem) === sanitizeUrl(currentSlug)) {
279
+ console.log("Avoiding infinite redirect: found URL matches current slug", firstItem, currentSlug)
280
+ continue
281
+ }
282
+
283
+ return firstItem
284
+ }
285
+
286
+ // If we get here, no valid URL was found in any sidebar group
287
+ throw new Error(`No valid fallback URL found for slug: ${currentSlug}.`)
288
+ }
289
+
290
+ const createElementWithKeys = (type: any, props: any) => {
291
+ // Process children to add keys to all elements
292
+ const processChildren = (childrenArray: any[]): any[] => {
293
+ return childrenArray.map((child, index) => {
294
+ // If the child is a React element and doesn't have a key, add one
295
+ if (React.isValidElement(child) && !child.key) {
296
+ return React.cloneElement(child, { key: `mdx-${index}` });
297
+ }
298
+ // If the child is an array, process it recursively
299
+ if (Array.isArray(child)) {
300
+ return processChildren(child);
301
+ }
302
+ return child;
303
+ });
304
+ };
305
+
306
+ // Handle both cases: children as separate args or as props.children
307
+ let processedChildren;
308
+
309
+ if (props && props.children) {
310
+ if (Array.isArray(props.children)) {
311
+ processedChildren = processChildren(props.children);
312
+ } else if (React.isValidElement(props.children) && !props.children.key) {
313
+ // Single child without key
314
+ processedChildren = React.cloneElement(props.children, { key: 'mdx-child' });
315
+ } else {
316
+ // Single child with key or non-React element
317
+ processedChildren = props.children;
318
+ }
319
+ } else {
320
+ processedChildren = [];
321
+ }
322
+
323
+ // Create the element with processed children
324
+ return React.createElement(type, {
325
+ ...props,
326
+ children: processedChildren
327
+ });
328
+ };
329
+
330
+ // TODO: move to content?
331
+ function mdxExport(code: string, themeContentComponents: any, themeFileComponents: any, globalAPI: any) {
332
+ // Create a wrapper around React.createElement that adds keys to elements in lists
333
+ const scope = {
334
+ Fragment: React.Fragment,
335
+ jsxs: createElementWithKeys,
336
+ jsx: createElementWithKeys,
337
+ jsxDEV: createElementWithKeys,
338
+ }
339
+
340
+ const global = {
341
+ ...themeContentComponents,
342
+ React,
343
+ }
344
+
345
+ const fn = new Function("_$scope", ...Object.keys(global), "fileComponents", ...Object.keys(globalAPI || {}), code);
346
+
347
+ return fn(scope, ...Object.values(global), themeFileComponents, ...Object.values(globalAPI));
348
+ }
349
+
350
+ // // TODO: move to content?
351
+ function mdxContent(code: string, themeContentComponents: any, themeFileComponents: any, globalAPI: any) {
352
+ const content = mdxExport(code, themeContentComponents, themeFileComponents, globalAPI) // TODO: fix any
353
+ if (!mdxExport) {
354
+ return {}
355
+ }
356
+
357
+ // TODO: IN THE FUTURE BETTER API
358
+ const layout = pageMetaLayout(content?.frontmatter)
359
+ if (content?.frontmatter && layout) {
360
+ content.frontmatter.layout = layout
361
+ }
362
+
363
+ return {
364
+ component: content?.default,
365
+ toc: content?.toc,
366
+ metadata: content?.frontmatter,
367
+ themeSettings: content?.themeSettings || {},
368
+ page: content?.page || false,
369
+ }
370
+ }
371
+
372
+ export function MemoMDXComponent(codeComponent: any) {
373
+ return useMemo(
374
+ () => codeComponent ? codeComponent : null,
375
+ [codeComponent]
376
+ )
377
+ }
378
+
379
+ export default function DocsPage({ loaderData }: { loaderData: loaderData }) {
380
+ const location = useLocation()
381
+ const navigation = useNavigation()
382
+
383
+ const analytics = useAnalytics()
384
+
385
+ const { theme } = useContext(PageContext)
386
+ if (!theme) {
387
+ throw new Error("BaseTheme not found")
388
+ }
389
+
390
+ // Dispatch custom event when location changes
391
+ useEffect(() => {
392
+ const event = new CustomEvent('xyd::pathnameChange', {
393
+ detail: {
394
+ pathname: location.pathname,
395
+ }
396
+ });
397
+
398
+ window.dispatchEvent(event);
399
+ }, [location.pathname]);
400
+
401
+ const themeContentComponents = theme.reactContentComponents()
402
+ const themeFileComponents = theme.reactFileComponents()
403
+ const globalAPI = {
404
+ analytics,
405
+ }
406
+
407
+ const createContent = (fileComponents) => {
408
+ return mdxContent(loaderData.code, themeContentComponents, fileComponents ? themeFileComponents : undefined, globalAPI)
409
+ }
410
+
411
+ const content = createContent(true)
412
+ const Content = MemoMDXComponent(content.component)
413
+
414
+ const contentOriginal = createContent(false)
415
+ const ContentOriginal = MemoMDXComponent(contentOriginal.component)
416
+
417
+ const { Page } = theme
418
+
419
+ return <>
420
+ <UXNode
421
+ name="Framework"
422
+ props={{
423
+ location: location.pathname + location.search + location.hash,
424
+ }}
425
+ >
426
+ <FrameworkPage
427
+ key={location.pathname}
428
+ metadata={content.metadata}
429
+ breadcrumbs={loaderData.breadcrumbs}
430
+ rawPage={loaderData.rawPage}
431
+ toc={content.toc || []}
432
+ navlinks={loaderData.navlinks}
433
+ ContentComponent={Content}
434
+ ContentOriginal={ContentOriginal}
435
+ >
436
+ <Page>
437
+ <ContentOriginal components={{
438
+ ...themeContentComponents,
439
+ wrapper: (props) => {
440
+ // TODO: in the future delete uxnod
441
+ return <UXNode
442
+ name=".ContentPage"
443
+ props={{}}
444
+ >
445
+ {props.children}
446
+ </UXNode>
447
+ }
448
+ }} />
449
+ <ScrollRestoration />
450
+ </Page>
451
+ </FrameworkPage>
452
+ </UXNode>
453
+ </>
454
+ }
455
+
456
+
457
+ function sanitizeUrl(url: string) {
458
+ if (url.startsWith("/")) {
459
+ return url
460
+ }
461
+
462
+ return `/${url}`
463
+ }