diff --git a/.changeset/brave-poems-walk.md b/.changeset/brave-poems-walk.md new file mode 100644 index 000000000..c4783a99b --- /dev/null +++ b/.changeset/brave-poems-walk.md @@ -0,0 +1,10 @@ +--- +'myst-to-react': minor +'myst-demo': minor +'@myst-theme/providers': minor +'@myst-theme/diagrams': minor +'@myst-theme/jupyter': minor +'@myst-theme/site': minor +--- + +Update to myst renderer as a react component diff --git a/docs/stories/0-Introduction.stories.mdx b/docs/stories/0-Introduction.stories.mdx index 9e1a1645c..1b94df8eb 100644 --- a/docs/stories/0-Introduction.stories.mdx +++ b/docs/stories/0-Introduction.stories.mdx @@ -1,6 +1,7 @@ -import { Meta } from '@storybook/addon-docs'; +import { Meta, Story, Canvas } from '@storybook/addon-docs'; import { FrontmatterBlock } from '@myst-theme/frontmatter'; -import { Admonition } from 'myst-to-react'; +import { ThemeProvider } from '@myst-theme/providers'; +import { MyST, Admonition, DEFAULT_RENDERERS } from 'myst-to-react'; @@ -30,3 +31,40 @@ The library also has a number of UI-providers that help coordinate interactions We can also use these components in this documentation, which is going to be great. 🚀 + +## Using `MyST` Component + +```tsx +import { ThemeProvider } from '@myst-theme/providers'; +import { MyST, DEFAULT_RENDERERS } from 'myst-to-react'; + +function MyComponent({ node }) { + return ( + + + + ); +} +``` + +export const { text, abbreviation } = DEFAULT_RENDERERS; + +export const Template = (args) => ( + + + +); + +This example creates an abbreviation with MyST, using a simple AST node. + + + + {Template.bind({})} + + diff --git a/packages/diagrams/src/index.tsx b/packages/diagrams/src/index.tsx index 4d82dbc34..6e04f1cf1 100644 --- a/packages/diagrams/src/index.tsx +++ b/packages/diagrams/src/index.tsx @@ -38,6 +38,6 @@ export function MermaidRenderer({ id, value }: { value: string; id: string }) { ); } -export const MermaidNodeRenderer: NodeRenderer = (node) => { - return ; +export const MermaidNodeRenderer: NodeRenderer = ({ node }) => { + return ; }; diff --git a/packages/jupyter/src/embed.tsx b/packages/jupyter/src/embed.tsx index 9703cafc5..dbaafef19 100644 --- a/packages/jupyter/src/embed.tsx +++ b/packages/jupyter/src/embed.tsx @@ -8,6 +8,7 @@ import { import { JupyterIcon } from '@scienceicons/react/24/solid'; import { select } from 'unist-util-select'; import { useLinkProvider, useBaseurl, withBaseurl } from '@myst-theme/providers'; +import { MyST } from 'myst-to-react'; function EmbedWithControls({ outputKey, @@ -73,17 +74,12 @@ function EmbedWithControls({ ); } -export function Embed(node: GenericNode, children: React.ReactNode) { +export function Embed({ node }: { node: GenericNode }) { const output = select('output', node) as GenericNode; - if (!output) return <>{children}; + if (!output) return ; return ( - - {children} + + ); } diff --git a/packages/jupyter/src/output.tsx b/packages/jupyter/src/output.tsx index 1de0b9214..f34f49868 100644 --- a/packages/jupyter/src/output.tsx +++ b/packages/jupyter/src/output.tsx @@ -80,11 +80,9 @@ function JupyterOutput({ ); } -export function Output(node: GenericNode) { - // Note, NodeRenderer's can't have hooks in it directly! +export function Output({ node }: { node: GenericNode }) { return ( (null); const [text, setText] = useState(value.trim()); const [references, setReferences] = useState({}); @@ -187,7 +185,6 @@ export function MySTRenderer({ const [jats, setJats] = useState('Loading...'); const [jatsWarnings, setJatsWarnings] = useState([]); const [warnings, setWarnings] = useState([]); - const [content, setContent] = useState(

{value}

); const [previewType, setPreviewType] = useState('DEMO'); useEffect(() => { @@ -195,7 +192,7 @@ export function MySTRenderer({ parse( text, { numbering }, - { renderers, removeHeading: !!TitleBlock, jats: { fullArticle: !!TitleBlock } }, + { removeHeading: !!TitleBlock, jats: { fullArticle: !!TitleBlock } }, ).then((result) => { if (!ref.current) return; setFrontmatter(result.frontmatter); @@ -206,13 +203,12 @@ export function MySTRenderer({ setTexWarnings(result.texWarnings); setJats(result.jats); setJatsWarnings(result.jatsWarnings); - setContent(result.content); setWarnings(result.warnings); }); return () => { ref.current = false; }; - }, [text, renderers]); + }, [text]); useEffect(() => { if (!area.current) return; @@ -315,7 +311,7 @@ export function MySTRenderer({ <> {TitleBlock && } - {content} + )} @@ -359,6 +355,6 @@ export function MySTRenderer({ ); } -export const MystDemoRenderer: NodeRenderer = (node) => { - return ; +export const MystDemoRenderer: NodeRenderer = ({ node }) => { + return ; }; diff --git a/packages/myst-to-react/docs/index.md b/packages/myst-to-react/docs/index.md index 0c9012380..881988605 100644 --- a/packages/myst-to-react/docs/index.md +++ b/packages/myst-to-react/docs/index.md @@ -26,11 +26,15 @@ npm install myst-to-react ## Simple example -```typescript -import { useParse, DEFAULT_RENDERERS } from 'myst-to-react'; - -function MyComponent({ node, renderers = DEFAULT_RENDERERS }) { - const children = useParse(node, renderers); - return
{children}
; +```tsx +import { ThemeProvider } from '@myst-theme/providers'; +import { MyST, DEFAULT_RENDERERS } from 'myst-to-react'; + +function MyComponent({ node }) { + return ( + + + + ); } ``` diff --git a/packages/myst-to-react/src/MyST.tsx b/packages/myst-to-react/src/MyST.tsx new file mode 100644 index 000000000..475291005 --- /dev/null +++ b/packages/myst-to-react/src/MyST.tsx @@ -0,0 +1,28 @@ +import { useNodeRenderers } from '@myst-theme/providers'; +import type { GenericNode } from 'myst-common'; + +function DefaultComponent({ node }: { node: GenericNode }) { + if (!node.children) return {node.value}; + return ( +
+ +
+ ); +} + +export function MyST({ ast }: { ast?: GenericNode | GenericNode[] }) { + const renderers = useNodeRenderers(); + if (!ast || ast.length === 0) return null; + if (!Array.isArray(ast)) { + const Component = renderers[ast.type] ?? DefaultComponent; + return ; + } + return ( + <> + {ast?.map((node) => { + const Component = renderers[node.type] ?? DefaultComponent; + return ; + })} + + ); +} diff --git a/packages/myst-to-react/src/admonitions.tsx b/packages/myst-to-react/src/admonitions.tsx index 710270ef8..9df51d9f5 100644 --- a/packages/myst-to-react/src/admonitions.tsx +++ b/packages/myst-to-react/src/admonitions.tsx @@ -16,6 +16,8 @@ import XCircleIcon from '@heroicons/react/24/solid/XCircleIcon'; import BoltIcon from '@heroicons/react/24/solid/BoltIcon'; import ChevronRightIcon from '@heroicons/react/24/solid/ChevronRightIcon'; import classNames from 'classnames'; +import { MyST } from './MyST'; +import type { GenericNode } from 'myst-common'; // import { AdmonitionKind } from 'myst-common'; // TODO: get this from myst-spec? @@ -105,8 +107,8 @@ function AdmonitionIcon({ kind, className }: { kind: AdmonitionKind; className?: return ; } -export const AdmonitionTitle: NodeRenderer = (node, children) => { - return children; +export const AdmonitionTitle: NodeRenderer = ({ node }) => { + return ; }; const WrapperElement = ({ @@ -220,28 +222,26 @@ export function Admonition({ ); } -export const AdmonitionRenderer: NodeRenderer = (node, children) => { - const [title, ...rest] = children as any[]; +export const AdmonitionRenderer: NodeRenderer = ({ node }) => { + const [title, ...rest] = node.children as GenericNode[]; const classes = getClasses(node.class); const { kind, color } = getFirstKind({ kind: node.kind, classes }); const isDropdown = classes.includes('dropdown'); const isSimple = classes.includes('simple'); - const hideIcon = (node as any).icon === false; + const hideIcon = node.icon === false; - const useTitle = node.children?.[0].type === 'admonitionTitle'; + const useTitle = title?.type === 'admonitionTitle'; return ( : undefined} kind={kind} color={color} dropdown={isDropdown} simple={isSimple} hideIcon={hideIcon} > - {!useTitle && title} - {rest} + {useTitle ? : } ); }; diff --git a/packages/myst-to-react/src/basic.tsx b/packages/myst-to-react/src/basic.tsx index 67da4a750..30a98b500 100644 --- a/packages/myst-to-react/src/basic.tsx +++ b/packages/myst-to-react/src/basic.tsx @@ -3,6 +3,7 @@ import { HashLink } from './heading'; import type { NodeRenderer } from '@myst-theme/providers'; import classNames from 'classnames'; import { Tooltip } from './components'; +import { MyST } from './MyST'; type TableExts = { rowspan?: number; @@ -40,6 +41,7 @@ type CaptionNumber = { }; type BasicNodeRenderers = { + text: NodeRenderer; strong: NodeRenderer; emphasis: NodeRenderer; link: NodeRenderer; @@ -75,157 +77,189 @@ type BasicNodeRenderers = { }; const BASIC_RENDERERS: BasicNodeRenderers = { - delete(node, children) { - return {children}; + text({ node }) { + return node.value; }, - strong(node, children) { - return {children}; + delete({ node }) { + return ( + + + + ); + }, + strong({ node }) { + return ( + + + + ); }, - emphasis(node, children) { - return {children}; + emphasis({ node }) { + return ( + + + + ); }, - underline(node, children) { + underline({ node }) { return ( - - {children} + + ); }, - smallcaps(node, children) { + smallcaps({ node }) { return ( - - {children} + + ); }, - link(node, children) { + link({ node }) { return ( - - {children} + + ); }, - paragraph(node, children) { + paragraph({ node }) { return ( -

- {children} +

+

); }, - break(node) { - return
; + break() { + return
; }, - inlineMath(node) { - return {node.value}; + inlineMath({ node }) { + return {node.value}; }, - math(node) { - return {node.value}; + math({ node }) { + return {node.value}; }, - list(node, children) { + list({ node }) { if (node.ordered) { return ( -
    - {children} +
      +
    ); } return ( -
      - {children} +
        +
      ); }, - listItem(node, children) { + listItem({ node }) { if (node.checked == null) { - return
    • {children}
    • ; + return ( +
    • + +
    • + ); } return ( -
    • +
    • - {children} +
    • ); }, - container(node, children) { + container({ node }) { return (
      - {children} +
      ); }, - caption(node, children) { + caption({ node }) { return ( -
      - {children} +
      +
      ); }, - blockquote(node, children) { + blockquote({ node }) { return ( -
      - {children} +
      +
      ); }, - thematicBreak(node) { - return
      ; + thematicBreak() { + return
      ; }, - captionNumber(node, children) { - function backwardsCompatibleLabel(value: string, kind?: string) { - const capital = kind?.slice(0, 1).toUpperCase() ?? 'F'; - const body = kind?.slice(1) ?? 'igure'; - return `${capital}${body}: ${children}`; - } - const label = - typeof children === 'string' ? backwardsCompatibleLabel(children, node.kind) : children; + captionNumber({ node }) { const id = node.html_id || node.identifier || node.key; return ( - {label} + ); }, - table(node, children) { + table({ node }) { // TODO: actually render the tbody on the server if it isn't included here. return ( - - {children} +
      + + +
      ); }, - tableRow(node, children) { - return {children}; + tableRow({ node }) { + return ( + + + + ); }, - tableCell(node, children) { + tableCell({ node }) { const ifGreaterThanOne = (num?: number) => (num === 1 ? undefined : num); const attrs = { - key: node.key, rowSpan: ifGreaterThanOne(node.rowspan), colSpan: ifGreaterThanOne(node.colspan), }; - if (node.header) return {children}; - return {children}; + if (node.header) + return ( + + + + ); + return ( + + + + ); }, - subscript(node, children) { - return {children}; + subscript({ node }) { + return ( + + + + ); }, - superscript(node, children) { - return {children}; + superscript({ node }) { + return ( + + + + ); }, - abbreviation(node, children) { + abbreviation({ node }) { return ( - + - {children} + ); @@ -236,33 +270,36 @@ const BASIC_RENDERERS: BasicNodeRenderers = { comment() { return null; }, - definitionList(node, children) { + definitionList({ node }) { return ( -
      - {children} +
      +
      ); }, - definitionTerm(node, children) { - let strongChildren: React.ReactNode = children; - if (Array.isArray(children)) { - const allowedStrongTypes = new Set(['emphasis']); - strongChildren = children.map((child, i) => { - if (typeof child === 'string') return {child}; - if (allowedStrongTypes.has(child?.type)) return {child}; - return child; - }); - } else if (typeof children === 'string') { - strongChildren = {children}; - } + definitionTerm({ node }) { + const allowedStrongTypes = new Set(['text', 'emphasis']); + const makeStrong = + node.children?.reduce((allowed, n) => allowed && allowedStrongTypes.has(n.type), true) ?? + false; return ( -
      - {strongChildren} +
      + {makeStrong ? ( + + + + ) : ( + + )}
      ); }, - definitionDescription(node, children) { - return
      {children}
      ; + definitionDescription({ node }) { + return ( +
      + +
      + ); }, }; diff --git a/packages/myst-to-react/src/card.tsx b/packages/myst-to-react/src/card.tsx index 0098d2664..cd5d9dcd5 100644 --- a/packages/myst-to-react/src/card.tsx +++ b/packages/myst-to-react/src/card.tsx @@ -2,6 +2,8 @@ import React from 'react'; import type { NodeRenderer } from '@myst-theme/providers'; import classNames from 'classnames'; import { useLinkProvider, useBaseurl, withBaseurl } from '@myst-theme/providers'; +import { MyST } from './MyST'; +import type { GenericNode } from 'myst-common'; type CardSpec = { type: 'card'; @@ -18,43 +20,37 @@ type FooterSpec = { type: 'footer'; }; -export const Header: NodeRenderer = (node, children) => { +export const Header: NodeRenderer = ({ node }) => { return ( -
      - {children} +
      +
      ); }; -export const Footer: NodeRenderer = (node, children) => { +export const Footer: NodeRenderer = ({ node }) => { return ( -