Skip to content

Commit

Permalink
🔘 Add support for buttons (#521)
Browse files Browse the repository at this point in the history
* Ref: 2i2c-org/infrastructure#5346

Add LinkOrButton renderer

Co-authored-by: Angus Hollands <[email protected]>

* Updated links and add initial styling

* fix: style names

* lint: prettier

* Remove unused class from import

* Revert "Remove unused class from import"

This reverts commit da3d945.

* Revert "lint: prettier"

This reverts commit efb41a5.

* refactor: use class only

* wip: add support for xrefs

* chore: run linter

* chore: add changeset

* fix: use tighter check for buttons

* fix: revert previous rel

* Update packages/myst-to-react/src/links/index.tsx

* Add buttonRole to parser

* Add buttonRole

* Add myst-ext-button package

---------

Co-authored-by: Angus Hollands <[email protected]>
Co-authored-by: Angus Hollands <[email protected]>
Co-authored-by: Rowan Cockett <[email protected]>
  • Loading branch information
4 people authored Jan 27, 2025
1 parent de028cd commit c2db8fb
Show file tree
Hide file tree
Showing 12 changed files with 111 additions and 33 deletions.
6 changes: 6 additions & 0 deletions .changeset/serious-grapes-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'myst-to-react': patch
'@myst-theme/styles': patch
---

Render links with `button` class as buttons
1 change: 1 addition & 0 deletions packages/myst-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"myst-common": "^1.7.3",
"myst-config": "^1.7.3",
"myst-directives": "^1.5.7",
"myst-ext-button": "^0.0.0",
"myst-ext-card": "^1.0.9",
"myst-ext-exercise": "^1.0.8",
"myst-ext-grid": "^1.0.8",
Expand Down
15 changes: 8 additions & 7 deletions packages/myst-demo/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ async function parse(
const { default: mystToTypst } = await import('myst-to-typst');
const { default: mystToJats } = await import('myst-to-jats').catch(() => ({ default: null }));
const { mystToHtml } = await import('myst-to-html');
const { buttonRole } = await import('myst-ext-button');
const { cardDirective } = await import('myst-ext-card');
const { gridDirective } = await import('myst-ext-grid');
const { tabDirectives } = await import('myst-ext-tabs');
Expand All @@ -131,7 +132,7 @@ async function parse(
proofDirective,
...exerciseDirectives,
],
// roles: [reactiveRole],
roles: [buttonRole],
vfile,
});
const mdast = parseMyst(text);
Expand Down Expand Up @@ -397,27 +398,27 @@ export function MySTRenderer({
'relative',
{
'grid grid-cols-2 gap-0 grid-rows-[3rem_1fr]': column,
'shadow-lg rounded': !fullscreen,
'rounded shadow-lg': !fullscreen,
'm-0': fullscreen,
},
className,
)}
>
{column && (
<div className="flex flex-row items-stretch h-full col-span-2 px-2 border dark:border-slate-600">
<div className="flex flex-row col-span-2 items-stretch px-2 h-full border dark:border-slate-600">
<div className="flex-grow"></div>
{demoMenu}
</div>
)}
<div className={classnames('myst relative', { 'overflow-auto': column })}>
<div className={classnames('relative myst', { 'overflow-auto': column })}>
<CopyIcon text={text} className="absolute right-0 p-1" />
<label>
<span className="sr-only">Edit the MyST Markdown text</span>
<textarea
ref={area}
value={text}
className={classnames(
'block p-6 shadow-inner resize-none w-full font-mono bg-slate-50/50 dark:bg-slate-800/50 outline-none',
'block p-6 w-full font-mono shadow-inner outline-none resize-none bg-slate-50/50 dark:bg-slate-800/50',
{ 'text-sm': !column },
{ 'h-full': column },
)}
Expand All @@ -427,7 +428,7 @@ export function MySTRenderer({
</div>
{/* The `exclude-from-outline` class is excluded from the document outline */}
<div
className={classnames('exclude-from-outline relative min-h-1 dark:bg-slate-900', {
className={classnames('relative exclude-from-outline min-h-1 dark:bg-slate-900', {
'overflow-auto': column,
})}
>
Expand Down Expand Up @@ -464,7 +465,7 @@ export function MySTRenderer({
{previewType === 'DOCX' && (
<div>
<button
className="p-3 border rounded"
className="p-3 rounded border"
onClick={() => saveDocxFile('demo.docx', references.article)}
title={`Download Micorsoft Word`}
aria-label={`Download Micorsoft Word`}
Expand Down
2 changes: 1 addition & 1 deletion packages/myst-to-react/src/basic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ const BASIC_RENDERERS: BasicNodeRenderers = {
},
link({ node }) {
return (
<a target="_blank" href={node.url} rel="noreferrer">
<a target="_blank" href={node.url} className={node.class} rel="noreferrer">
<MyST ast={node.children} />
</a>
);
Expand Down
27 changes: 22 additions & 5 deletions packages/myst-to-react/src/crossReference.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { MyST } from './MyST.js';
import type { GenericNode, GenericParent } from 'myst-common';
import { selectMdastNodes } from 'myst-common';
import { scrollToElement } from './hashLink.js';
import classNames from 'classnames';

const fetcher = (...args: Parameters<typeof fetch>) =>
fetch(...args).then((res) => {
Expand Down Expand Up @@ -126,6 +127,7 @@ export function CrossReferenceHover({
remoteBaseUrl: remoteBaseUrlIn,
children,
identifier,
className,
htmlId = '',
}: {
remote?: boolean;
Expand All @@ -134,6 +136,7 @@ export function CrossReferenceHover({
remoteBaseUrl?: string;
identifier: string;
htmlId?: string;
className?: string;
children: React.ReactNode;
}) {
const Link = useLinkProvider();
Expand All @@ -150,6 +153,7 @@ export function CrossReferenceHover({
const el = document.getElementById(htmlId);
scrollToElement(el, { htmlId });
};
const isButtonLike = (className ?? '').split(' ').includes('button');
return (
<HoverPopover
card={({ load }) => (
Expand All @@ -159,7 +163,7 @@ export function CrossReferenceHover({
<div className="w-full px-3 py-1 text-xs border-b bg-gray-50">
<strong className="text-gray-700">Source: </strong>
<a
className="text-gray-700"
className={classNames('text-gray-700', className)}
href={`${createRemoteBaseUrl(url, remoteBaseUrl)}${htmlId ? `#${htmlId}` : ''}`}
target="_blank"
>
Expand All @@ -179,7 +183,7 @@ export function CrossReferenceHover({
<a
href={`${createRemoteBaseUrl(url, remoteBaseUrl)}${htmlId ? `#${htmlId}` : ''}`}
target="_blank"
className="hover-link"
className={classNames({ 'hover-link': !isButtonLike }, className)}
>
{children}
</a>
Expand All @@ -188,13 +192,17 @@ export function CrossReferenceHover({
<Link
to={`${withBaseurl(url, baseurl)}${htmlId ? `#${htmlId}` : ''}`}
prefetch="intent"
className="hover-link"
className={classNames({ 'hover-link': !isButtonLike }, className)}
>
{children}
</Link>
)}
{!remote && (
<a href={`#${htmlId}`} onClick={scroll} className="hover-link">
<a
href={`#${htmlId}`}
onClick={scroll}
className={classNames({ 'hover-link': !isButtonLike }, className)}
>
{children}
</a>
)}
Expand All @@ -212,7 +220,15 @@ export const CrossReferenceNode: NodeRenderer<CrossReference> = ({ node }) => {
/>
);
}
const { remote, url, dataUrl, remoteBaseUrl, identifier, html_id } = node as any;
const {
remote,
url,
dataUrl,
remoteBaseUrl,
identifier,
html_id,
class: className,
} = node as any;
return (
<CrossReferenceHover
identifier={identifier}
Expand All @@ -221,6 +237,7 @@ export const CrossReferenceNode: NodeRenderer<CrossReference> = ({ node }) => {
url={url}
dataUrl={dataUrl}
remoteBaseUrl={remoteBaseUrl}
className={className}
>
{node.prefix && <>{node.prefix} </>}
<MyST ast={node.children} />
Expand Down
14 changes: 11 additions & 3 deletions packages/myst-to-react/src/links/github.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ function GithubFilePreview({
from,
to,
open,
className,
}: {
url: string;
raw: string;
Expand All @@ -60,6 +61,7 @@ function GithubFilePreview({
from?: number;
to?: number;
open: boolean;
className?: string;
}) {
const { data, error } = useLoadWhenOpen(open, raw, fetcher);
let code = data;
Expand All @@ -68,7 +70,7 @@ function GithubFilePreview({
<div className="hover-document article w-[500px] sm:max-w-[500px]">
<a
href={url}
className="block text-inherit hover:text-inherit"
className={classNames('block text-inherit hover:text-inherit', className)}
target="_blank"
rel="noreferrer"
>
Expand Down Expand Up @@ -138,12 +140,14 @@ function GithubIssuePreview({
repo,
issue_number,
open,
className,
}: {
url: string;
org: string;
repo: string;
issue_number?: string | number;
open: boolean;
className?: string;
}) {
const { data, error } = useLoadWhenOpen(
open,
Expand All @@ -163,7 +167,7 @@ function GithubIssuePreview({
<div className="hover-document article">
<a
href={url}
className="block text-inherit hover:text-inherit"
className={classNames('block text-inherit hover:text-inherit', className)}
target="_blank"
rel="noreferrer"
>
Expand Down Expand Up @@ -238,6 +242,7 @@ export function GithubLink({
from,
to,
issue_number,
className,
}: {
children: React.ReactNode;
kind: 'file' | 'issue';
Expand All @@ -249,6 +254,7 @@ export function GithubLink({
from?: number;
issue_number?: string | number;
to?: number;
className?: string;
}) {
return (
<HoverPopover
Expand All @@ -264,6 +270,7 @@ export function GithubLink({
open={load}
org={org}
repo={repo}
className={className}
/>
);
}
Expand All @@ -275,12 +282,13 @@ export function GithubLink({
org={org}
issue_number={issue_number}
repo={repo}
className={className}
/>
);
}
}}
>
<a href={url} className="italic" target="_blank" rel="noreferrer">
<a href={url} className={classNames('italic', className)} target="_blank" rel="noreferrer">
{children}
</a>
</HoverPopover>
Expand Down
38 changes: 26 additions & 12 deletions packages/myst-to-react/src/links/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,23 @@ function getPageInfo(site: SiteManifest | undefined, path: string) {
return project.pages.find((p) => p.slug === (pageSlug || projectSlug));
}

function InternalLink({ url, children }: { url: string; children: React.ReactNode }) {
function InternalLink({
url,
children,
className,
}: {
url: string;
children: React.ReactNode;
className?: string;
}) {
const Link = useLinkProvider();
const site = useSiteManifest();
const page = getPageInfo(site, url);
const baseurl = useBaseurl();
const skipPreview = !page || (!page.description && !page.thumbnail);
if (!page || skipPreview) {
return (
<Link to={withBaseurl(url, baseurl)} prefetch="intent">
<Link to={withBaseurl(url, baseurl)} prefetch="intent" className={className}>
{children}
</Link>
);
Expand All @@ -48,7 +56,7 @@ function InternalLink({ url, children }: { url: string; children: React.ReactNod
/>
}
>
<Link to={withBaseurl(url, baseurl)} prefetch="intent">
<Link to={withBaseurl(url, baseurl)} prefetch="intent" className={className}>
{children}
</Link>
</HoverPopover>
Expand All @@ -57,7 +65,12 @@ function InternalLink({ url, children }: { url: string; children: React.ReactNod

export const WikiLinkRenderer: NodeRenderer<TransformedLink> = ({ node }) => {
return (
<WikiLink url={node.url} page={node.data?.page as string} wiki={node.data?.wiki as string}>
<WikiLink
url={node.url}
page={node.data?.page as string}
wiki={node.data?.wiki as string}
className={node.class}
>
<MyST ast={node.children} />
</WikiLink>
);
Expand All @@ -75,31 +88,32 @@ export const GithubLinkRenderer: NodeRenderer<TransformedLink> = ({ node }) => {
from={node.data?.from as number | undefined}
to={node.data?.to as number | undefined}
issue_number={node.data?.issue_number as number | undefined}
className={node.class}
>
<MyST ast={node.children} />
</GithubLink>
);
};

export const RRIDLinkRenderer: NodeRenderer<TransformedLink> = ({ node }) => (
<RRIDLink rrid={node.data?.rrid as string} />
);
export const RRIDLinkRenderer: NodeRenderer<TransformedLink> = ({ node }) => {
return <RRIDLink rrid={node.data?.rrid as string} className={node.class} />;
};

export const RORLinkRenderer: NodeRenderer<TransformedLink> = ({ node }) => (
<RORLink node={node} ror={node.data?.ror as string} />
);
export const RORLinkRenderer: NodeRenderer<TransformedLink> = ({ node }) => {
return <RORLink node={node} ror={node.data?.ror as string} className={node.class} />;
};

export const SimpleLink: NodeRenderer<TransformedLink> = ({ node }) => {
const internal = node.internal ?? false;
if (internal) {
return (
<InternalLink url={node.url}>
<InternalLink url={node.url} className={node.class}>
<MyST ast={node.children} />
</InternalLink>
);
}
return (
<a target="_blank" href={node.url} rel="noreferrer">
<a target="_blank" rel="noreferrer" href={node.url} className={node.class}>
<MyST ast={node.children} />
</a>
);
Expand Down
17 changes: 15 additions & 2 deletions packages/myst-to-react/src/links/ror.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,23 @@ function RORChild({ ror }: { ror: string }) {
);
}

export function RORLink({ node, ror }: { node: GenericNode; ror: string }) {
export function RORLink({
node,
ror,
className,
}: {
node: GenericNode;
ror: string;
className?: string;
}) {
return (
<HoverPopover card={<RORChild ror={ror} />}>
<a href={`https://ror.org/${ror}`} target="_blank" rel="noopener noreferrer">
<a
href={`https://ror.org/${ror}`}
target="_blank"
rel="noopener noreferrer"
className={className}
>
<MyST ast={node.children} />
</a>
</HoverPopover>
Expand Down
Loading

0 comments on commit c2db8fb

Please sign in to comment.