@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myst-theme/site",
3
- "version": "0.3.8",
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.3.8",
20
- "@myst-theme/frontmatter": "^0.3.8",
21
- "@myst-theme/jupyter": "^0.3.8",
22
- "@myst-theme/providers": "^0.3.8",
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.1",
26
- "myst-config": "^1.0.0",
27
- "myst-demo": "^0.3.8",
28
- "myst-to-react": "^0.3.8",
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 { useParse, DEFAULT_RENDERERS } from 'myst-to-react';
2
- import { SourceFileKind } from 'myst-common';
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 leading-6">
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((a, b) => {
90
- if (a) return a;
91
- if (b.classList.contains('highlight')) return b.id;
92
- return null;
93
- }, null as string | null);
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 = DOC_OUTLINE_CLASS,
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 hidden',
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 leading-6 dark:text-slate-100">
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 '../types';
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 shadow-sm hover:shadow-lg dark:shadow-neutral-700"
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 '../../loaders';
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,5 +1,4 @@
1
1
  export * from './utils';
2
- export * from './types';
3
2
  export * from './loaders';
4
3
  export * from './components';
5
4
  export * from './pages';
@@ -1,7 +1,4 @@
1
- export enum ErrorStatus {
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
@@ -1,5 +1,3 @@
1
1
  export * from './errors.server';
2
- export * as cdn from './cdn.server';
3
- export * from './utils';
4
2
  export * from './links';
5
3
  export * from './theme.server';
@@ -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 '../types';
6
+ import type { PageLoader } from '@myst-theme/common';
7
7
  import type { GenericParent } from 'myst-common';
8
- import { SourceFileKind } from 'myst-common';
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 ({ article }: { article: PageLoader }) {
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 kind={article.kind} frontmatter={article.frontmatter} />
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
- {/* {canCompute && article.kind === SourceFileKind.Article && <NotebookToolbar />} */}
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 && <FooterLinksBlock links={article.footer} />}
59
+ {!hide_footer_links && !hide_all_footer_links && (
60
+ <FooterLinksBlock links={article.footer} />
61
+ )}
49
62
  </ExecuteScopeProvider>
50
63
  </BusyScopeProvider>
51
64
  </ReferencesProvider>
@@ -1,5 +1,5 @@
1
1
  import type { SiteManifest } from 'myst-config';
2
- import type { SiteLoader } from '../types';
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 { ContentReload, renderers } from '../components';
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 bg-white transition-colors duration-500 dark:bg-stone-900">
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}>
@@ -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, AppWithReload, Document, AppCatchBoundary } from './Root';
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
- }
@@ -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
- };