diff --git a/.changeset/itchy-doors-leave.md b/.changeset/itchy-doors-leave.md new file mode 100644 index 000000000..72137166c --- /dev/null +++ b/.changeset/itchy-doors-leave.md @@ -0,0 +1,12 @@ +--- +'myst-to-react': patch +'@myst-theme/frontmatter': patch +'myst-demo': patch +'@myst-theme/providers': patch +'@myst-theme/common': patch +'@myst-theme/article': patch +'@myst-theme/site': patch +'@myst-theme/book': patch +--- + +Support parts mdast trees in page frontmatter diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index ff6fa68ec..bb235b60c 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -45,7 +45,8 @@ export type FooterLinks = { }; }; -type PageFrontmatterWithDownloads = Omit & { +type PageFrontmatterWithDownloads = Omit & { + parts?: Record; downloads?: SiteAction[]; exports?: SiteExport[]; }; diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts index f9994d377..c2bf336c4 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -188,28 +188,31 @@ export function updatePageStaticLinksInplace(data: PageLoader, updateUrl: Update return { ...exp, url: updateUrl(exp.url) }; }); } - // Fix all of the images to point to the CDN - const images = selectAll('image', data.mdast) as Image[]; - images.forEach((node) => { - node.url = updateUrl(node.url); - if (node.urlOptimized) { - node.urlOptimized = updateUrl(node.urlOptimized); - } - }); - const links = selectAll('link,linkBlock,card', data.mdast) as Link[]; - const staticLinks = links.filter((node) => node.static); - staticLinks.forEach((node) => { - // These are static links to thinks like PDFs or other referenced files - node.url = updateUrl(node.url); - }); - const outputs = selectAll('output', data.mdast) as Output[]; - outputs.forEach((node) => { - if (!node.data) return; - walkOutputs(node.data, (obj) => { - // The path will be defined from output of myst - // Here we are re-assigning it to the current domain - if (!obj.path) return; - obj.path = updateUrl(obj.path); + const allMdastTrees = [data, ...Object.values(data.frontmatter?.parts ?? {})]; + allMdastTrees.forEach(({ mdast }) => { + // Fix all of the images to point to the CDN + const images = selectAll('image', mdast) as Image[]; + images.forEach((node) => { + node.url = updateUrl(node.url); + if (node.urlOptimized) { + node.urlOptimized = updateUrl(node.urlOptimized); + } + }); + const links = selectAll('link,linkBlock,card', mdast) as Link[]; + const staticLinks = links?.filter((node) => node.static); + staticLinks.forEach((node) => { + // These are static links to thinks like PDFs or other referenced files + node.url = updateUrl(node.url); + }); + const outputs = selectAll('output', mdast) as Output[]; + outputs.forEach((node) => { + if (!node.data) return; + walkOutputs(node.data, (obj) => { + // The path will be defined from output of myst + // Here we are re-assigning it to the current domain + if (!obj.path) return; + obj.path = updateUrl(obj.path); + }); }); }); return data; diff --git a/packages/frontmatter/src/FrontmatterBlock.tsx b/packages/frontmatter/src/FrontmatterBlock.tsx index d311cf643..36c66ad22 100644 --- a/packages/frontmatter/src/FrontmatterBlock.tsx +++ b/packages/frontmatter/src/FrontmatterBlock.tsx @@ -194,7 +194,7 @@ export function FrontmatterBlock({ hideExports, className, }: { - frontmatter: PageFrontmatter; + frontmatter: Omit; kind?: SourceFileKind; authorStyle?: 'block' | 'list'; hideBadges?: boolean; diff --git a/packages/myst-demo/src/index.tsx b/packages/myst-demo/src/index.tsx index 3fbcebcbc..6e7ef4f56 100644 --- a/packages/myst-demo/src/index.tsx +++ b/packages/myst-demo/src/index.tsx @@ -386,6 +386,7 @@ export function MySTRenderer({ ); const mdastStage = astStage === 'pre' ? mdastPre : mdastPost; + const { downloads, exports, parts, ...reducedFrontmatter } = frontmatter; return (
{previewType === 'DEMO' && ( <> - + {TitleBlock && } diff --git a/packages/myst-to-react/src/crossReference.tsx b/packages/myst-to-react/src/crossReference.tsx index cbddf7f0c..a7a8c1602 100644 --- a/packages/myst-to-react/src/crossReference.tsx +++ b/packages/myst-to-react/src/crossReference.tsx @@ -7,11 +7,13 @@ import { XRefProvider, useXRefState, type NodeRenderer, + useFrontmatter, } from '@myst-theme/providers'; import { InlineError } from './inlineError.js'; import { default as useSWR } from 'swr'; import { HoverPopover } from './components/index.js'; import { MyST } from './MyST.js'; +import type { GenericNode, GenericParent } from 'myst-common'; import { selectMdastNodes } from 'myst-common'; import { scrollToElement } from './hashLink.js'; @@ -100,11 +102,20 @@ export function useFetchMdast({ function useSelectNodes({ load, identifier }: { load?: boolean; identifier: string }) { const references = useReferences(); + const frontmatter = useFrontmatter(); const { remote, url, remoteBaseUrl, dataUrl } = useXRefState(); if (!load) return; const { data, error } = useFetchMdast({ remote, url, remoteBaseUrl, dataUrl }); - const mdast = data?.mdast ?? references?.article; - const { nodes, htmlId } = selectMdastNodes(mdast, identifier, 3); + const mdast = data ? (data.mdast as GenericParent) : references?.article; + const parts = data ? (data.frontmatter?.parts as { mdast: GenericParent }) : frontmatter?.parts; + let nodes: GenericNode[] = []; + let htmlId: string | undefined; + [{ mdast }, ...Object.values(parts ?? {})].forEach(({ mdast: tree }) => { + if (!tree || nodes.length > 0) return; + const selected = selectMdastNodes(tree, identifier, 3); + nodes = selected.nodes; + htmlId = selected.htmlId; + }); return { htmlId, nodes, loading: remote && !data, error: remote && error }; } diff --git a/packages/providers/src/references.tsx b/packages/providers/src/references.tsx index 3397d73b2..392ce4881 100644 --- a/packages/providers/src/references.tsx +++ b/packages/providers/src/references.tsx @@ -1,9 +1,9 @@ import React, { useContext } from 'react'; import type { References } from 'myst-common'; -import type { PageFrontmatter } from 'myst-frontmatter'; +import type { PageLoader } from '@myst-theme/common'; const ReferencesContext = React.createContext<{ - frontmatter?: PageFrontmatter; + frontmatter?: PageLoader['frontmatter']; references?: References; }>({}); @@ -12,7 +12,7 @@ export function ReferencesProvider({ frontmatter, children, }: { - frontmatter?: PageFrontmatter; + frontmatter?: PageLoader['frontmatter']; references?: References; children: React.ReactNode; }) { diff --git a/packages/site/src/pages/Article.tsx b/packages/site/src/pages/Article.tsx index 50e17e417..af0319469 100644 --- a/packages/site/src/pages/Article.tsx +++ b/packages/site/src/pages/Article.tsx @@ -42,7 +42,7 @@ export const ArticlePage = React.memo(function ({ const downloads = combineDownloads(manifest?.downloads, article.frontmatter); const tree = copyNode(article.mdast); const keywords = article.frontmatter?.keywords ?? []; - const parts = extractKnownParts(tree); + const parts = extractKnownParts(tree, article.frontmatter?.parts); return (

Unexpected Error Occurred

Status: {error.status}

-

{error.data.message}

+

{error.data?.message ?? ''}

); } diff --git a/packages/site/src/utils.ts b/packages/site/src/utils.ts index 17f069050..de834d280 100644 --- a/packages/site/src/utils.ts +++ b/packages/site/src/utils.ts @@ -18,13 +18,21 @@ export type KnownParts = { acknowledgments?: GenericParent; }; -export function extractKnownParts(tree: GenericParent): KnownParts { +export function extractKnownParts( + tree: GenericParent, + parts?: Record, +): KnownParts { const abstract = extractPart(tree, 'abstract'); const summary = extractPart(tree, 'summary', { requireExplicitPart: true }); const keypoints = extractPart(tree, ['keypoints'], { requireExplicitPart: true }); const data_availability = extractPart(tree, ['data_availability', 'data availability']); const acknowledgments = extractPart(tree, ['acknowledgments', 'acknowledgements']); - return { abstract, summary, keypoints, data_availability, acknowledgments }; + const otherParts = Object.fromEntries( + Object.entries(parts ?? {}).map(([k, v]) => { + return [k, v.mdast]; + }), + ); + return { abstract, summary, keypoints, data_availability, acknowledgments, ...otherParts }; } /** diff --git a/themes/article/app/components/Article.tsx b/themes/article/app/components/Article.tsx index 04acab57a..b4c2eeb14 100644 --- a/themes/article/app/components/Article.tsx +++ b/themes/article/app/components/Article.tsx @@ -34,7 +34,7 @@ export function Article({ }) { const keywords = article.frontmatter?.keywords ?? []; const tree = copyNode(article.mdast); - const parts = extractKnownParts(tree); + const parts = extractKnownParts(tree, article.frontmatter?.parts); const { title, subtitle } = article.frontmatter; const compute = useComputeOptions(); const top = useThemeTop(); diff --git a/themes/book/app/components/ArticlePage.tsx b/themes/book/app/components/ArticlePage.tsx index 63e29bccc..2700b956f 100644 --- a/themes/book/app/components/ArticlePage.tsx +++ b/themes/book/app/components/ArticlePage.tsx @@ -74,7 +74,7 @@ export const ArticlePage = React.memo(function ({ const downloads = combineDownloads(manifest?.downloads, article.frontmatter); const tree = copyNode(article.mdast); const keywords = article.frontmatter?.keywords ?? []; - const parts = extractKnownParts(tree); + const parts = extractKnownParts(tree, article.frontmatter?.parts); const isOutlineMargin = useMediaQuery('(min-width: 1024px)'); return (