@myst-theme/site 0.3.8 → 0.4.1
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 +11 -11
- package/src/components/ContentBlocks.tsx +4 -7
- package/src/components/DocumentOutline.tsx +13 -13
- package/src/components/FooterLinksBlock.tsx +2 -2
- package/src/components/Navigation/TableOfContents.tsx +1 -2
- package/src/components/renderers.ts +2 -1
- package/src/index.ts +0 -1
- package/src/loaders/errors.server.ts +1 -4
- package/src/loaders/index.ts +0 -2
- package/src/pages/Article.tsx +19 -6
- package/src/pages/Root.tsx +3 -18
- package/src/pages/index.ts +1 -1
- package/src/loaders/cdn.server.ts +0 -172
- package/src/loaders/utils.ts +0 -152
- package/src/types.ts +0 -51
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myst-theme/site",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"main": "./src/index.ts",
|
|
5
5
|
"types": "./src/index.ts",
|
|
6
6
|
"files": [
|
|
@@ -16,16 +16,18 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@headlessui/react": "^1.7.15",
|
|
18
18
|
"@heroicons/react": "^2.0.18",
|
|
19
|
-
"@myst-theme/diagrams": "^0.
|
|
20
|
-
"@myst-theme/frontmatter": "^0.
|
|
21
|
-
"@myst-theme/jupyter": "^0.
|
|
22
|
-
"@myst-theme/
|
|
19
|
+
"@myst-theme/diagrams": "^0.4.1",
|
|
20
|
+
"@myst-theme/frontmatter": "^0.4.1",
|
|
21
|
+
"@myst-theme/jupyter": "^0.4.1",
|
|
22
|
+
"@myst-theme/common": "^0.4.1",
|
|
23
|
+
"@myst-theme/providers": "^0.4.1",
|
|
23
24
|
"classnames": "^2.3.2",
|
|
24
25
|
"lodash.throttle": "^4.1.1",
|
|
25
|
-
"myst-common": "^1.0
|
|
26
|
-
"myst-
|
|
27
|
-
"myst-
|
|
28
|
-
"myst-
|
|
26
|
+
"myst-common": "^1.1.0",
|
|
27
|
+
"myst-spec-ext": "^1.1.0",
|
|
28
|
+
"myst-config": "^1.1.0",
|
|
29
|
+
"myst-demo": "^0.4.1",
|
|
30
|
+
"myst-to-react": "^0.4.1",
|
|
29
31
|
"nbtx": "^0.2.3",
|
|
30
32
|
"node-cache": "^5.1.2",
|
|
31
33
|
"node-fetch": "^2.6.11",
|
|
@@ -33,8 +35,6 @@
|
|
|
33
35
|
"unist-util-select": "^4.0.1"
|
|
34
36
|
},
|
|
35
37
|
"peerDependencies": {
|
|
36
|
-
"@remix-run/node": "^1.17.0",
|
|
37
|
-
"@remix-run/react": "^1.17.0",
|
|
38
38
|
"@types/react": "^16.8 || ^17.0 || ^18.0",
|
|
39
39
|
"@types/react-dom": "^16.8 || ^17.0 || ^18.0",
|
|
40
40
|
"react": "^16.8 || ^17.0 || ^18.0",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { SourceFileKind } from 'myst-
|
|
1
|
+
import { MyST } from 'myst-to-react';
|
|
2
|
+
import { SourceFileKind } from 'myst-spec-ext';
|
|
3
3
|
import type { GenericParent } from 'myst-common';
|
|
4
|
-
import { useNodeRenderers } from '@myst-theme/providers';
|
|
5
4
|
import classNames from 'classnames';
|
|
6
5
|
import {
|
|
7
6
|
NotebookClearCell,
|
|
@@ -31,8 +30,6 @@ function Block({
|
|
|
31
30
|
node: GenericParent;
|
|
32
31
|
className?: string;
|
|
33
32
|
}) {
|
|
34
|
-
const renderers = useNodeRenderers() ?? DEFAULT_RENDERERS;
|
|
35
|
-
const children = useParse(node, renderers);
|
|
36
33
|
const subGrid = 'article-grid article-subgrid-gap col-screen';
|
|
37
34
|
const dataClassName = typeof node.data?.class === 'string' ? node.data?.class : undefined;
|
|
38
35
|
// Hide the subgrid if either the dataClass or the className exists and includes `col-`
|
|
@@ -40,7 +37,7 @@ function Block({
|
|
|
40
37
|
(dataClassName && dataClassName.includes('col-')) || (className && className.includes('col-'));
|
|
41
38
|
return (
|
|
42
39
|
<div
|
|
43
|
-
key={id}
|
|
40
|
+
key={`block-${id}`}
|
|
44
41
|
id={id}
|
|
45
42
|
className={classNames('relative group/block', className, dataClassName, {
|
|
46
43
|
[subGrid]: !noSubGrid,
|
|
@@ -61,7 +58,7 @@ function Block({
|
|
|
61
58
|
</div>
|
|
62
59
|
</>
|
|
63
60
|
)}
|
|
64
|
-
{children}
|
|
61
|
+
<MyST ast={node.children} />
|
|
65
62
|
</div>
|
|
66
63
|
);
|
|
67
64
|
}
|
|
@@ -26,7 +26,7 @@ type Props = {
|
|
|
26
26
|
* scrollIntoView is used to ensure that when a user clicks on an item, it will smoothly scroll.
|
|
27
27
|
*/
|
|
28
28
|
const Headings = ({ headings, activeId, highlight, selector }: Props) => (
|
|
29
|
-
<ul className="text-sm text-slate-400
|
|
29
|
+
<ul className="text-sm leading-6 text-slate-400">
|
|
30
30
|
{headings.map((heading) => (
|
|
31
31
|
<li
|
|
32
32
|
key={heading.id}
|
|
@@ -43,7 +43,7 @@ const Headings = ({ headings, activeId, highlight, selector }: Props) => (
|
|
|
43
43
|
'text-slate-500 dark:text-slate-300': heading.level >= 3 && heading.id !== activeId,
|
|
44
44
|
'text-blue-600 dark:text-white font-bold': heading.id === activeId,
|
|
45
45
|
'pr-2': heading.id !== activeId, // Allows for bold to change length
|
|
46
|
-
'pl-2': heading.level === 2,
|
|
46
|
+
'pl-2': heading.level === 1 || heading.level === 2,
|
|
47
47
|
'pl-4': heading.level === 3,
|
|
48
48
|
'pl-8 text-xs': heading.level === 4,
|
|
49
49
|
'pl-10 text-xs font-light': heading.level === 5,
|
|
@@ -86,11 +86,14 @@ function useHeaders(selector: string) {
|
|
|
86
86
|
|
|
87
87
|
const highlight = useCallback(() => {
|
|
88
88
|
const current = [...onScreen.current];
|
|
89
|
-
const highlighted = current.reduce(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
const highlighted = current.reduce(
|
|
90
|
+
(a, b) => {
|
|
91
|
+
if (a) return a;
|
|
92
|
+
if (b.classList.contains('highlight')) return b.id;
|
|
93
|
+
return null;
|
|
94
|
+
},
|
|
95
|
+
null as string | null,
|
|
96
|
+
);
|
|
94
97
|
const active = [...onScreen.current].sort((a, b) => a.offsetTop - b.offsetTop)[0];
|
|
95
98
|
if (highlighted || active) setActiveId(highlighted || active.id);
|
|
96
99
|
}, []);
|
|
@@ -156,9 +159,6 @@ const useIntersectionObserver = (highlight: () => void, onScreen: Set<HTMLHeadin
|
|
|
156
159
|
return { observer };
|
|
157
160
|
};
|
|
158
161
|
|
|
159
|
-
const DOC_OUTLINE_CLASS =
|
|
160
|
-
'fixed bottom-0 right-[max(0px,calc(50%-45rem))] w-[14rem] lg:w-[18rem] py-10 px-4 lg:px-8 overflow-y-auto hidden lg:block';
|
|
161
|
-
|
|
162
162
|
export function useOutlineHeight<T extends HTMLElement = HTMLElement>() {
|
|
163
163
|
const container = useRef<T>(null);
|
|
164
164
|
const outline = useRef<T>(null);
|
|
@@ -186,7 +186,7 @@ export function useOutlineHeight<T extends HTMLElement = HTMLElement>() {
|
|
|
186
186
|
export const DocumentOutline = ({
|
|
187
187
|
outlineRef,
|
|
188
188
|
top,
|
|
189
|
-
className
|
|
189
|
+
className,
|
|
190
190
|
selector = SELECTOR,
|
|
191
191
|
}: {
|
|
192
192
|
outlineRef?: React.RefObject<HTMLElement>;
|
|
@@ -204,7 +204,7 @@ export const DocumentOutline = ({
|
|
|
204
204
|
ref={outlineRef}
|
|
205
205
|
aria-label="Document Outline"
|
|
206
206
|
className={classNames(
|
|
207
|
-
'not-prose overflow-y-auto
|
|
207
|
+
'not-prose overflow-y-auto',
|
|
208
208
|
'transition-opacity duration-700', // Animation on load
|
|
209
209
|
className,
|
|
210
210
|
)}
|
|
@@ -213,7 +213,7 @@ export const DocumentOutline = ({
|
|
|
213
213
|
maxHeight: `calc(100vh - ${(top ?? 0) + 20}px)`,
|
|
214
214
|
}}
|
|
215
215
|
>
|
|
216
|
-
<div className="mb-4 text-sm uppercase text-slate-900
|
|
216
|
+
<div className="mb-4 text-sm leading-6 uppercase text-slate-900 dark:text-slate-100">
|
|
217
217
|
In this article
|
|
218
218
|
</div>
|
|
219
219
|
<Headings headings={headings} activeId={activeId} highlight={highlight} selector={selector} />
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import classNames from 'classnames';
|
|
2
2
|
import ArrowLeftIcon from '@heroicons/react/24/outline/ArrowLeftIcon';
|
|
3
3
|
import ArrowRightIcon from '@heroicons/react/24/outline/ArrowRightIcon';
|
|
4
|
-
import type { FooterLinks, NavigationLink } from '
|
|
4
|
+
import type { FooterLinks, NavigationLink } from '@myst-theme/common';
|
|
5
5
|
import { useLinkProvider, useBaseurl, withBaseurl } from '@myst-theme/providers';
|
|
6
6
|
|
|
7
7
|
const FooterLink = ({
|
|
@@ -16,7 +16,7 @@ const FooterLink = ({
|
|
|
16
16
|
return (
|
|
17
17
|
<Link
|
|
18
18
|
prefetch="intent"
|
|
19
|
-
className="flex-1 block p-4 font-normal text-gray-600 no-underline border border-gray-200 rounded group hover:border-blue-600 dark:hover:border-blue-400 hover:text-blue-600 dark:hover:text-blue-400 dark:text-gray-100 dark:border-gray-500
|
|
19
|
+
className="flex-1 block p-4 font-normal text-gray-600 no-underline border border-gray-200 rounded shadow-sm group hover:border-blue-600 dark:hover:border-blue-400 hover:text-blue-600 dark:hover:text-blue-400 dark:text-gray-100 dark:border-gray-500 hover:shadow-lg dark:shadow-neutral-700"
|
|
20
20
|
to={withBaseurl(url, baseurl)}
|
|
21
21
|
>
|
|
22
22
|
<div className="flex h-full align-middle">
|
|
@@ -9,8 +9,7 @@ import {
|
|
|
9
9
|
useBaseurl,
|
|
10
10
|
withBaseurl,
|
|
11
11
|
} from '@myst-theme/providers';
|
|
12
|
-
import { getProjectHeadings } from '
|
|
13
|
-
import type { Heading } from '../../types';
|
|
12
|
+
import { getProjectHeadings, type Heading } from '@myst-theme/common';
|
|
14
13
|
|
|
15
14
|
type Props = {
|
|
16
15
|
folder?: string;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import type { NodeRenderer } from '@myst-theme/providers';
|
|
1
2
|
import { DEFAULT_RENDERERS } from 'myst-to-react';
|
|
2
3
|
import { MystDemoRenderer } from 'myst-demo';
|
|
3
4
|
import { MermaidNodeRenderer } from '@myst-theme/diagrams';
|
|
4
5
|
import OUTPUT_RENDERERS from '@myst-theme/jupyter';
|
|
5
6
|
|
|
6
|
-
export const renderers = {
|
|
7
|
+
export const renderers: Record<string, NodeRenderer> = {
|
|
7
8
|
...DEFAULT_RENDERERS,
|
|
8
9
|
myst: MystDemoRenderer,
|
|
9
10
|
mermaid: MermaidNodeRenderer,
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
noSite = 'Site was not found',
|
|
3
|
-
noArticle = 'Article was not found',
|
|
4
|
-
}
|
|
1
|
+
import { ErrorStatus } from '@myst-theme/common';
|
|
5
2
|
|
|
6
3
|
export function responseNoSite(): Response {
|
|
7
4
|
// note: error boundary logic is dependent on the string sent here
|
package/src/loaders/index.ts
CHANGED
package/src/pages/Article.tsx
CHANGED
|
@@ -3,9 +3,9 @@ import { ReferencesProvider } from '@myst-theme/providers';
|
|
|
3
3
|
import { Bibliography, ContentBlocks, FooterLinksBlock } from '../components';
|
|
4
4
|
import { ErrorDocumentNotFound } from './ErrorDocumentNotFound';
|
|
5
5
|
import { ErrorProjectNotFound } from './ErrorProjectNotFound';
|
|
6
|
-
import type { PageLoader } from '
|
|
6
|
+
import type { PageLoader } from '@myst-theme/common';
|
|
7
7
|
import type { GenericParent } from 'myst-common';
|
|
8
|
-
import { SourceFileKind } from 'myst-
|
|
8
|
+
import { SourceFileKind } from 'myst-spec-ext';
|
|
9
9
|
import {
|
|
10
10
|
useComputeOptions,
|
|
11
11
|
ExecuteScopeProvider,
|
|
@@ -14,10 +14,17 @@ import {
|
|
|
14
14
|
ConnectionStatusTray,
|
|
15
15
|
BinderBadge,
|
|
16
16
|
useCanCompute,
|
|
17
|
+
ErrorTray,
|
|
17
18
|
} from '@myst-theme/jupyter';
|
|
18
19
|
import { FrontmatterBlock } from '@myst-theme/frontmatter';
|
|
19
20
|
|
|
20
|
-
export const ArticlePage = React.memo(function ({
|
|
21
|
+
export const ArticlePage = React.memo(function ({
|
|
22
|
+
article,
|
|
23
|
+
hide_all_footer_links,
|
|
24
|
+
}: {
|
|
25
|
+
article: PageLoader;
|
|
26
|
+
hide_all_footer_links?: boolean;
|
|
27
|
+
}) {
|
|
21
28
|
const canCompute = useCanCompute(article);
|
|
22
29
|
const { binderBadgeUrl } = useComputeOptions();
|
|
23
30
|
const { hide_title_block, hide_footer_links } = (article.frontmatter as any)?.design ?? {};
|
|
@@ -33,7 +40,11 @@ export const ArticlePage = React.memo(function ({ article }: { article: PageLoad
|
|
|
33
40
|
<BusyScopeProvider>
|
|
34
41
|
<ExecuteScopeProvider contents={article}>
|
|
35
42
|
{!hide_title_block && (
|
|
36
|
-
<FrontmatterBlock
|
|
43
|
+
<FrontmatterBlock
|
|
44
|
+
kind={article.kind}
|
|
45
|
+
frontmatter={article.frontmatter}
|
|
46
|
+
className="mb-8"
|
|
47
|
+
/>
|
|
37
48
|
)}
|
|
38
49
|
{binderUrl && !canCompute && (
|
|
39
50
|
<div className="flex justify-end">
|
|
@@ -41,11 +52,13 @@ export const ArticlePage = React.memo(function ({ article }: { article: PageLoad
|
|
|
41
52
|
</div>
|
|
42
53
|
)}
|
|
43
54
|
{canCompute && article.kind === SourceFileKind.Notebook && <NotebookToolbar showLaunch />}
|
|
44
|
-
|
|
55
|
+
<ErrorTray pageSlug={article.slug} />
|
|
45
56
|
<ContentBlocks pageKind={article.kind} mdast={article.mdast as GenericParent} />
|
|
46
57
|
<Bibliography />
|
|
47
58
|
<ConnectionStatusTray />
|
|
48
|
-
{!hide_footer_links &&
|
|
59
|
+
{!hide_footer_links && !hide_all_footer_links && (
|
|
60
|
+
<FooterLinksBlock links={article.footer} />
|
|
61
|
+
)}
|
|
49
62
|
</ExecuteScopeProvider>
|
|
50
63
|
</BusyScopeProvider>
|
|
51
64
|
</ReferencesProvider>
|
package/src/pages/Root.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SiteManifest } from 'myst-config';
|
|
2
|
-
import type { SiteLoader } from '
|
|
2
|
+
import type { SiteLoader } from '@myst-theme/common';
|
|
3
3
|
import { BaseUrlProvider, SiteProvider, Theme, ThemeProvider } from '@myst-theme/providers';
|
|
4
4
|
import {
|
|
5
5
|
Links,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
Link,
|
|
13
13
|
NavLink,
|
|
14
14
|
} from '@remix-run/react';
|
|
15
|
-
import {
|
|
15
|
+
import { renderers } from '../components';
|
|
16
16
|
import { Analytics } from '../seo';
|
|
17
17
|
import { Error404 } from './Error404';
|
|
18
18
|
import classNames from 'classnames';
|
|
@@ -60,7 +60,7 @@ export function Document({
|
|
|
60
60
|
analytics_plausible={config?.analytics_plausible}
|
|
61
61
|
/>
|
|
62
62
|
</head>
|
|
63
|
-
<body className="m-0
|
|
63
|
+
<body className="m-0 transition-colors duration-500 bg-white dark:bg-stone-900">
|
|
64
64
|
<ThemeProvider theme={theme} renderers={renderers} {...links}>
|
|
65
65
|
<BaseUrlProvider baseurl={baseurl}>
|
|
66
66
|
<ThebeBundleLoaderProvider loadThebeLite publicPath={baseurl}>
|
|
@@ -88,21 +88,6 @@ export function App() {
|
|
|
88
88
|
);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
export function AppWithReload() {
|
|
92
|
-
const { theme, config, CONTENT_CDN_PORT, MODE, BASE_URL } = useLoaderData<SiteLoader>();
|
|
93
|
-
return (
|
|
94
|
-
<Document
|
|
95
|
-
theme={theme}
|
|
96
|
-
config={config}
|
|
97
|
-
scripts={MODE === 'static' ? undefined : <ContentReload port={CONTENT_CDN_PORT} />}
|
|
98
|
-
staticBuild={MODE === 'static'}
|
|
99
|
-
baseurl={BASE_URL}
|
|
100
|
-
>
|
|
101
|
-
<Outlet />
|
|
102
|
-
</Document>
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
91
|
export function AppCatchBoundary() {
|
|
107
92
|
return (
|
|
108
93
|
<Document theme={Theme.light}>
|
package/src/pages/index.ts
CHANGED
|
@@ -2,4 +2,4 @@ export { ErrorProjectNotFound } from './ErrorProjectNotFound';
|
|
|
2
2
|
export { ErrorDocumentNotFound } from './ErrorDocumentNotFound';
|
|
3
3
|
export { Error404 } from './Error404';
|
|
4
4
|
export { ArticlePage, ArticlePageCatchBoundary, ProjectPageCatchBoundary } from './Article';
|
|
5
|
-
export { App,
|
|
5
|
+
export { App, Document, AppCatchBoundary } from './Root';
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import fetch from 'node-fetch';
|
|
2
|
-
import NodeCache from 'node-cache';
|
|
3
|
-
import type { SiteManifest } from 'myst-config';
|
|
4
|
-
import { responseNoArticle, responseNoSite } from './errors.server';
|
|
5
|
-
import {
|
|
6
|
-
getFooterLinks,
|
|
7
|
-
getProject,
|
|
8
|
-
updatePageStaticLinksInplace,
|
|
9
|
-
updateSiteManifestStaticLinksInplace,
|
|
10
|
-
} from './utils';
|
|
11
|
-
import { redirect } from '@remix-run/node';
|
|
12
|
-
import type { PageLoader } from '../types';
|
|
13
|
-
|
|
14
|
-
interface CdnRouter {
|
|
15
|
-
cdn?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
declare global {
|
|
19
|
-
// Disable multiple caches when this file is rebuilt
|
|
20
|
-
// eslint-disable-next-line
|
|
21
|
-
var cdnRouterCache: NodeCache | undefined, configCache: NodeCache | undefined;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export type Host = string | { CDN: string; id: string };
|
|
25
|
-
|
|
26
|
-
const DEFAULT_CDN = 'https://cdn.curvenote.com/';
|
|
27
|
-
|
|
28
|
-
function getCdnRouterCache() {
|
|
29
|
-
if (global.cdnRouterCache) return global.cdnRouterCache;
|
|
30
|
-
console.log('Creating cdnRouterCache');
|
|
31
|
-
// The router should update every minute
|
|
32
|
-
global.cdnRouterCache = new NodeCache({ stdTTL: 30 });
|
|
33
|
-
return global.cdnRouterCache;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function getConfigCache() {
|
|
37
|
-
if (global.configCache) return global.configCache;
|
|
38
|
-
console.log('Creating configCache');
|
|
39
|
-
// The config can be long lived as it is static (0 == ∞)
|
|
40
|
-
global.configCache = new NodeCache({ stdTTL: 0 });
|
|
41
|
-
return global.configCache;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function getCdnPath(hostname: string): Promise<string | undefined> {
|
|
45
|
-
const cached = getCdnRouterCache().get<CdnRouter>(hostname);
|
|
46
|
-
if (cached) return cached.cdn;
|
|
47
|
-
const response = await fetch(`https://api.curvenote.com/routers/${hostname}`);
|
|
48
|
-
if (response.status === 404) {
|
|
49
|
-
// Always hit the API again if it is not found!
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
const data = (await response.json()) as CdnRouter;
|
|
53
|
-
getCdnRouterCache().set<CdnRouter>(hostname, data);
|
|
54
|
-
return data.cdn;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function withPublicFolderUrl(baseUrl: string, url: string): string {
|
|
58
|
-
return withBaseUrl(baseUrl, `public${url}`);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export async function getCdnLocation(host: Host) {
|
|
62
|
-
if (typeof host === 'string') {
|
|
63
|
-
const id = await getCdnPath(host);
|
|
64
|
-
if (!id) throw responseNoSite();
|
|
65
|
-
return { CDN: DEFAULT_CDN, id };
|
|
66
|
-
}
|
|
67
|
-
return host;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export async function getCdnBaseUrl(host: Host): Promise<string> {
|
|
71
|
-
const { CDN, id } = await getCdnLocation(host);
|
|
72
|
-
return `${CDN}${id}/`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function withBaseUrl<T extends string | undefined>(baseUrl: string, url: T): T {
|
|
76
|
-
if (!url) return url;
|
|
77
|
-
return `${baseUrl}${url}` as T;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Basic comparison for checking that the title and (possible) slug are the same
|
|
82
|
-
*/
|
|
83
|
-
function foldTitleString(title?: string): string | undefined {
|
|
84
|
-
return title?.replace(/[-\s_]/g, '').toLowerCase();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* If the site title and the first nav item are the same, remove it.
|
|
89
|
-
*/
|
|
90
|
-
function removeSingleNavItems(config: SiteManifest) {
|
|
91
|
-
if (
|
|
92
|
-
config?.nav?.length === 1 &&
|
|
93
|
-
foldTitleString(config.nav[0].title) === foldTitleString(config.title)
|
|
94
|
-
) {
|
|
95
|
-
config.nav = [];
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export async function getConfig(host: Host): Promise<SiteManifest> {
|
|
100
|
-
const location = await getCdnLocation(host);
|
|
101
|
-
const baseUrl = await getCdnBaseUrl(location);
|
|
102
|
-
const { id } = location;
|
|
103
|
-
if (!id) throw responseNoSite();
|
|
104
|
-
const cached = getConfigCache().get<SiteManifest>(id);
|
|
105
|
-
// Load the data from an in memory cache.
|
|
106
|
-
if (cached) return cached;
|
|
107
|
-
const response = await fetch(withBaseUrl(baseUrl, 'config.json'));
|
|
108
|
-
if (response.status === 404) throw responseNoSite();
|
|
109
|
-
const data = (await response.json()) as SiteManifest;
|
|
110
|
-
data.id = id;
|
|
111
|
-
removeSingleNavItems(data);
|
|
112
|
-
updateSiteManifestStaticLinksInplace(data, (url) => withPublicFolderUrl(baseUrl, url));
|
|
113
|
-
getConfigCache().set<SiteManifest>(id, data);
|
|
114
|
-
return data;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export async function getObjectsInv(host: Host): Promise<Buffer | undefined> {
|
|
118
|
-
const baseUrl = await getCdnBaseUrl(host);
|
|
119
|
-
if (!baseUrl) return;
|
|
120
|
-
const url = `${baseUrl}objects.inv`;
|
|
121
|
-
const response = await fetch(url);
|
|
122
|
-
if (response.status === 404) return;
|
|
123
|
-
const buffer = await response.buffer();
|
|
124
|
-
return buffer;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function getData(
|
|
128
|
-
baseUrl: string,
|
|
129
|
-
config?: SiteManifest,
|
|
130
|
-
project?: string,
|
|
131
|
-
slug?: string,
|
|
132
|
-
): Promise<PageLoader | null> {
|
|
133
|
-
if (!slug || !config) throw responseNoArticle();
|
|
134
|
-
const { id } = config;
|
|
135
|
-
if (!id) throw responseNoSite();
|
|
136
|
-
const projectPart = project ? `${project}/` : '';
|
|
137
|
-
const response = await fetch(withBaseUrl(baseUrl, `content/${projectPart}${slug}.json`));
|
|
138
|
-
if (response.status === 404) throw responseNoArticle();
|
|
139
|
-
const data = (await response.json()) as PageLoader;
|
|
140
|
-
return updatePageStaticLinksInplace(data, (url) => withPublicFolderUrl(baseUrl, url));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export async function getPage(
|
|
144
|
-
host: Host,
|
|
145
|
-
opts: {
|
|
146
|
-
domain?: string;
|
|
147
|
-
project?: string;
|
|
148
|
-
loadIndexPage?: boolean;
|
|
149
|
-
slug?: string;
|
|
150
|
-
redirect?: boolean | string;
|
|
151
|
-
},
|
|
152
|
-
): Promise<PageLoader | Response | null> {
|
|
153
|
-
const projectName = opts.project;
|
|
154
|
-
const baseUrl = await getCdnBaseUrl(host);
|
|
155
|
-
const config = await getConfig(host);
|
|
156
|
-
if (!config) throw responseNoSite();
|
|
157
|
-
const project = getProject(config, projectName);
|
|
158
|
-
if (!project) throw responseNoArticle();
|
|
159
|
-
if (opts.slug === project.index && opts.redirect) {
|
|
160
|
-
return redirect(
|
|
161
|
-
`${typeof opts.redirect === 'string' ? opts.redirect : '/'}${projectName ?? ''}`,
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
const slug = opts.loadIndexPage || opts.slug == null ? project.index : opts.slug;
|
|
165
|
-
const loader = await getData(baseUrl, config, projectName, slug).catch((e) => {
|
|
166
|
-
console.error(e);
|
|
167
|
-
return null;
|
|
168
|
-
});
|
|
169
|
-
if (!loader) throw responseNoArticle();
|
|
170
|
-
const footer = getFooterLinks(config, projectName, slug);
|
|
171
|
-
return { ...loader, footer, domain: opts.domain as string, project: projectName as string };
|
|
172
|
-
}
|
package/src/loaders/utils.ts
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import type { MinifiedOutput } from 'nbtx';
|
|
2
|
-
import { walkOutputs } from 'nbtx';
|
|
3
|
-
import type { SiteManifest } from 'myst-config';
|
|
4
|
-
import { selectAll } from 'unist-util-select';
|
|
5
|
-
import type { Image as ImageSpec, Link as LinkSpec } from 'myst-spec';
|
|
6
|
-
import type { FooterLinks, Heading, NavigationLink, PageLoader } from '../types';
|
|
7
|
-
|
|
8
|
-
type Image = ImageSpec & { urlOptimized?: string };
|
|
9
|
-
type Link = LinkSpec & { static?: boolean };
|
|
10
|
-
type Output = { data?: MinifiedOutput[] };
|
|
11
|
-
|
|
12
|
-
type ManifestProject = Required<SiteManifest>['projects'][0];
|
|
13
|
-
type ManifestProjectItem = ManifestProject['pages'][0];
|
|
14
|
-
|
|
15
|
-
export function getProject(
|
|
16
|
-
config?: SiteManifest,
|
|
17
|
-
projectSlug?: string,
|
|
18
|
-
): ManifestProject | undefined {
|
|
19
|
-
if (!config) return undefined;
|
|
20
|
-
if (!projectSlug) return config.projects?.[0];
|
|
21
|
-
const project = config.projects?.find((p) => p.slug === projectSlug);
|
|
22
|
-
return project;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function getProjectHeadings(
|
|
26
|
-
config: SiteManifest,
|
|
27
|
-
projectSlug?: string,
|
|
28
|
-
opts: { addGroups: boolean } = { addGroups: false },
|
|
29
|
-
): Heading[] | undefined {
|
|
30
|
-
const project = getProject(config, projectSlug);
|
|
31
|
-
if (!project) return undefined;
|
|
32
|
-
const headings: Heading[] = [
|
|
33
|
-
{
|
|
34
|
-
title: project.title,
|
|
35
|
-
short_title: project.short_title,
|
|
36
|
-
slug: project.index,
|
|
37
|
-
path: project.slug ? `/${project.slug}` : '/',
|
|
38
|
-
level: 'index',
|
|
39
|
-
},
|
|
40
|
-
...project.pages.map((p) => {
|
|
41
|
-
if (!('slug' in p)) return p;
|
|
42
|
-
return { ...p, path: projectSlug ? `/${project.slug}/${p.slug}` : `/${p.slug}` };
|
|
43
|
-
}),
|
|
44
|
-
];
|
|
45
|
-
if (opts.addGroups) {
|
|
46
|
-
let lastTitle = project.short_title || project.title;
|
|
47
|
-
return headings.map((heading) => {
|
|
48
|
-
if (!heading.slug || heading.level === 'index') {
|
|
49
|
-
lastTitle = heading.short_title || heading.title;
|
|
50
|
-
}
|
|
51
|
-
return { ...heading, group: lastTitle };
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
return headings;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function getHeadingLink(currentSlug: string, headings?: Heading[]): NavigationLink | undefined {
|
|
58
|
-
if (!headings) return undefined;
|
|
59
|
-
const linkIndex = headings.findIndex(({ slug }) => !!slug && slug !== currentSlug);
|
|
60
|
-
const link = headings[linkIndex];
|
|
61
|
-
if (!link?.path) return undefined;
|
|
62
|
-
return {
|
|
63
|
-
title: link.title,
|
|
64
|
-
short_title: link.short_title,
|
|
65
|
-
url: link.path,
|
|
66
|
-
group: link.group,
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function getFooterLinks(
|
|
71
|
-
config?: SiteManifest,
|
|
72
|
-
projectSlug?: string,
|
|
73
|
-
slug?: string,
|
|
74
|
-
): FooterLinks {
|
|
75
|
-
if (!slug || !config) return {};
|
|
76
|
-
const pages = getProjectHeadings(config, projectSlug, {
|
|
77
|
-
addGroups: true,
|
|
78
|
-
});
|
|
79
|
-
const found = pages?.findIndex(({ slug: s }) => s === slug) ?? -1;
|
|
80
|
-
if (found === -1) return {};
|
|
81
|
-
const prev = getHeadingLink(slug, pages?.slice(0, found).reverse());
|
|
82
|
-
const next = getHeadingLink(slug, pages?.slice(found + 1));
|
|
83
|
-
const footer: FooterLinks = {
|
|
84
|
-
navigation: { prev, next },
|
|
85
|
-
};
|
|
86
|
-
return footer;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
type UpdateUrl = (url: string) => string;
|
|
90
|
-
|
|
91
|
-
export function updateSiteManifestStaticLinksInplace(
|
|
92
|
-
data: SiteManifest,
|
|
93
|
-
updateUrl: UpdateUrl,
|
|
94
|
-
): SiteManifest {
|
|
95
|
-
data.actions?.forEach((action) => {
|
|
96
|
-
if (!action.static) return;
|
|
97
|
-
action.url = updateUrl(action.url);
|
|
98
|
-
});
|
|
99
|
-
// TODO: this needs to be based on the template.yml in the future
|
|
100
|
-
if (data.logo) data.logo = updateUrl(data.logo);
|
|
101
|
-
if (data.logo_dark) data.logo_dark = updateUrl(data.logo_dark);
|
|
102
|
-
// Update the thumbnails to point at the CDN
|
|
103
|
-
data.projects?.forEach((project) => {
|
|
104
|
-
project.pages
|
|
105
|
-
.filter((page): page is ManifestProjectItem => 'slug' in page)
|
|
106
|
-
.forEach((page) => {
|
|
107
|
-
if (page.thumbnail) page.thumbnail = updateUrl(page.thumbnail);
|
|
108
|
-
if (page.thumbnailOptimized) page.thumbnailOptimized = updateUrl(page.thumbnailOptimized);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
return data;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function updatePageStaticLinksInplace(data: PageLoader, updateUrl: UpdateUrl): PageLoader {
|
|
115
|
-
if (data?.frontmatter?.thumbnail) {
|
|
116
|
-
data.frontmatter.thumbnail = updateUrl(data.frontmatter.thumbnail);
|
|
117
|
-
}
|
|
118
|
-
if (data?.frontmatter?.thumbnailOptimized) {
|
|
119
|
-
data.frontmatter.thumbnailOptimized = updateUrl(data.frontmatter.thumbnailOptimized);
|
|
120
|
-
}
|
|
121
|
-
if (data?.frontmatter?.exports) {
|
|
122
|
-
data.frontmatter.exports = data.frontmatter.exports.map((exp) => {
|
|
123
|
-
if (!exp.url) return exp;
|
|
124
|
-
return { ...exp, url: updateUrl(exp.url) };
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
// Fix all of the images to point to the CDN
|
|
128
|
-
const images = selectAll('image', data.mdast) as Image[];
|
|
129
|
-
images.forEach((node) => {
|
|
130
|
-
node.url = updateUrl(node.url);
|
|
131
|
-
if (node.urlOptimized) {
|
|
132
|
-
node.urlOptimized = updateUrl(node.urlOptimized);
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
const links = selectAll('link,linkBlock,card', data.mdast) as Link[];
|
|
136
|
-
const staticLinks = links.filter((node) => node.static);
|
|
137
|
-
staticLinks.forEach((node) => {
|
|
138
|
-
// These are static links to thinks like PDFs or other referenced files
|
|
139
|
-
node.url = updateUrl(node.url);
|
|
140
|
-
});
|
|
141
|
-
const outputs = selectAll('output', data.mdast) as Output[];
|
|
142
|
-
outputs.forEach((node) => {
|
|
143
|
-
if (!node.data) return;
|
|
144
|
-
walkOutputs(node.data, (obj) => {
|
|
145
|
-
// The path will be defined from output of myst-cli
|
|
146
|
-
// Here we are re-asigning it to the current domain
|
|
147
|
-
if (!obj.path) return;
|
|
148
|
-
obj.path = updateUrl(obj.path);
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
return data;
|
|
152
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import type { Root } from 'mdast';
|
|
2
|
-
import type { References, Dependency, SourceFileKind } from 'myst-common';
|
|
3
|
-
import type { SiteManifest } from 'myst-config';
|
|
4
|
-
import type { PageFrontmatter } from 'myst-frontmatter';
|
|
5
|
-
import type { Theme } from '@myst-theme/providers';
|
|
6
|
-
|
|
7
|
-
export type Heading = {
|
|
8
|
-
slug?: string;
|
|
9
|
-
path?: string;
|
|
10
|
-
title: string;
|
|
11
|
-
short_title?: string;
|
|
12
|
-
level: number | 'index';
|
|
13
|
-
group?: string;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export type SiteLoader = {
|
|
17
|
-
theme: Theme;
|
|
18
|
-
config?: SiteManifest;
|
|
19
|
-
CONTENT_CDN_PORT?: string | number;
|
|
20
|
-
MODE?: 'app' | 'static';
|
|
21
|
-
BASE_URL?: string;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export type NavigationLink = {
|
|
25
|
-
group?: string;
|
|
26
|
-
title: string;
|
|
27
|
-
url: string;
|
|
28
|
-
short_title?: string;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export type FooterLinks = {
|
|
32
|
-
navigation?: {
|
|
33
|
-
prev?: NavigationLink;
|
|
34
|
-
next?: NavigationLink;
|
|
35
|
-
};
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export type PageLoader = {
|
|
39
|
-
kind: SourceFileKind;
|
|
40
|
-
file: string;
|
|
41
|
-
sha256: string;
|
|
42
|
-
slug: string;
|
|
43
|
-
domain: string; // This is written in at render time in the site
|
|
44
|
-
project: string; // This is written in at render time in the site
|
|
45
|
-
frontmatter: PageFrontmatter;
|
|
46
|
-
mdast: Root;
|
|
47
|
-
references: References;
|
|
48
|
-
footer?: FooterLinks;
|
|
49
|
-
// This may not be defined
|
|
50
|
-
dependencies?: Dependency[];
|
|
51
|
-
};
|