Skip to content

Commit

Permalink
🦶🎵 Add footnotes to bottom of page (#397)
Browse files Browse the repository at this point in the history
  • Loading branch information
rowanc1 authored Jun 5, 2024
1 parent d95786c commit e3ab7cd
Show file tree
Hide file tree
Showing 16 changed files with 188 additions and 73 deletions.
9 changes: 9 additions & 0 deletions .changeset/four-tips-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'myst-to-react': patch
'@myst-theme/article': patch
'@myst-theme/site': patch
'@myst-theme/book': patch
'@myst-theme/styles': patch
---

Add footnotes to bottom of page in addition to hover
2 changes: 1 addition & 1 deletion packages/myst-to-react/src/basic.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import type * as spec from 'myst-spec';
import { HashLink } from './heading.js';
import { HashLink } from './hashLink.js';
import type { NodeRenderer } from '@myst-theme/providers';
import classNames from 'classnames';
import { Tooltip } from './components/index.js';
Expand Down
18 changes: 2 additions & 16 deletions packages/myst-to-react/src/crossReference.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { default as useSWR } from 'swr';
import { HoverPopover } from './components/index.js';
import { MyST } from './MyST.js';
import { selectMdastNodes } from 'myst-common';
import { scrollToElement } from './hashLink.js';

const fetcher = (...args: Parameters<typeof fetch>) =>
fetch(...args).then((res) => {
Expand Down Expand Up @@ -40,14 +41,6 @@ function XrefChildren({ load, identifier }: { load?: boolean; identifier: string
return <MyST ast={data?.nodes} />;
}

function openDetails(el: HTMLElement | null) {
if (!el) return;
if (el.nodeName === 'DETAILS') {
(el as HTMLDetailsElement).open = true;
}
openDetails(el.parentElement);
}

function createRemoteBaseUrl(url?: string, remoteBaseUrl?: string): string {
if (remoteBaseUrl && url?.startsWith(remoteBaseUrl)) {
// The remoteBaseUrl is included in the url
Expand Down Expand Up @@ -144,14 +137,7 @@ export function CrossReferenceHover({
e.preventDefault();
if (!htmlId) return;
const el = document.getElementById(htmlId);
openDetails(el);
el?.scrollIntoView({ behavior: 'smooth' });
history.replaceState(undefined, '', `#${htmlId}`);
if (el) {
// Changes keyboard tab-index location
if (el.tabIndex === -1) el.tabIndex = -1;
el.focus({ preventScroll: true });
}
scrollToElement(el, { htmlId });
};
return (
<HoverPopover
Expand Down
2 changes: 1 addition & 1 deletion packages/myst-to-react/src/exercise.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';
import type { NodeRenderer } from '@myst-theme/providers';
import { ChevronRightIcon } from '@heroicons/react/24/solid';
import classNames from 'classnames';
import { HashLink } from './heading.js';
import { HashLink } from './hashLink.js';
import type { GenericNode } from 'myst-common';
import { MyST } from './MyST.js';

Expand Down
9 changes: 7 additions & 2 deletions packages/myst-to-react/src/footnotes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { XRefProvider, useReferences } from '@myst-theme/providers';
import { select } from 'unist-util-select';
import { HoverPopover } from './components/index.js';
import { MyST } from './MyST.js';
import { HashLink } from './hashLink.js';

function FootnoteDefinition({ identifier }: { identifier: string }) {
const references = useReferences();
Expand All @@ -24,8 +25,12 @@ export const FootnoteReference: NodeRenderer = ({ node }) => {
openDelay={0}
card={<FootnoteDefinition identifier={node.identifier as string} />}
>
<span>
<sup className="hover-link">[{node.number ?? node.identifier}]</sup>
<span id={`fnref-${node.key}`}>
<sup className="hover-link">
<HashLink id={`fn-${node.identifier}`} title="Link to Footnote" scrollBehavior="instant">
[{node.enumerator ?? node.number ?? node.identifier}]
</HashLink>
</sup>
</span>
</HoverPopover>
);
Expand Down
95 changes: 95 additions & 0 deletions packages/myst-to-react/src/hashLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { useXRefState } from '@myst-theme/providers';
import classNames from 'classnames';

export type HashLinkBehavior = {
/** When scrolling, is this `instant`, `auto` or `scroll`? */
scrollBehavior?: ScrollBehavior;
/** When updating the URL, do you push state or replace it? */
historyState?: 'replace' | 'push' | null;
/** Change the keyboard tab-index location to the new element */
focusTarget?: boolean;
};

function openDetails(el: HTMLElement | null) {
if (!el) return;
if (el.nodeName === 'DETAILS') {
(el as HTMLDetailsElement).open = true;
}
openDetails(el.parentElement);
}

export function scrollToElement(
el: HTMLElement | null,
{
htmlId = el?.id,
scrollBehavior = 'smooth',
historyState = 'replace',
focusTarget = true,
}: {
/** Update the URL fragment to this ID */
htmlId?: string;
} & HashLinkBehavior = {},
) {
if (!el) return;
openDetails(el);
el.scrollIntoView({ behavior: scrollBehavior });
if (historyState === 'push') {
history.pushState(undefined, '', `#${htmlId}`);
} else if (historyState === 'replace') {
history.replaceState(undefined, '', `#${htmlId}`);
}
if (focusTarget) {
// Changes keyboard tab-index location
if (el.tabIndex === -1) el.tabIndex = -1;
el.focus({ preventScroll: true });
}
}

export function HashLink({
id,
kind,
title = `Link to this ${kind}`,
children = '¶',
hover,
className = 'font-normal',
hideInPopup,
scrollBehavior,
historyState,
focusTarget,
}: {
id?: string;
kind?: string;
title?: string;
hover?: boolean;
children?: '#' | '¶' | React.ReactNode;
className?: string;
hideInPopup?: boolean;
} & HashLinkBehavior) {
const { inCrossRef } = useXRefState();
if (inCrossRef || !id) {
// If we are in a cross-reference pop-out, either hide hash link
// or return something that is **not** a link
return hideInPopup ? null : (
<span className={classNames('select-none', className)}>{children}</span>
);
}
const scroll: React.MouseEventHandler<HTMLAnchorElement> = (evt) => {
evt.preventDefault();
const el = document.getElementById(id);
scrollToElement(el, { scrollBehavior, historyState, focusTarget });
};
return (
<a
className={classNames('select-none no-underline text-inherit hover:text-inherit', className, {
'transition-opacity opacity-0 focus:opacity-100 group-hover:opacity-70': hover,
'hover:underline': !hover,
})}
onClick={scroll}
href={`#${id}`}
title={title}
aria-label={title}
>
{children}
</a>
);
}
50 changes: 1 addition & 49 deletions packages/myst-to-react/src/heading.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,8 @@
import { Heading } from 'myst-spec';
import type { NodeRenderer } from '@myst-theme/providers';
import { useXRefState } from '@myst-theme/providers';
import { createElement as e } from 'react';
import classNames from 'classnames';
import { MyST } from './MyST.js';

export function HashLink({
id,
kind,
title = `Link to this ${kind}`,
children = '¶',
hover,
className = 'font-normal',
hideInPopup,
}: {
id?: string;
kind?: string;
title?: string;
hover?: boolean;
children?: '#' | '¶' | React.ReactNode;
className?: string;
hideInPopup?: boolean;
}) {
const { inCrossRef } = useXRefState();
if (inCrossRef || !id) {
// If we are in a cross-reference pop-out, either hide hash link
// or return something that is **not** a link
return hideInPopup ? null : (
<span className={classNames('select-none', className)}>{children}</span>
);
}
const scroll: React.MouseEventHandler<HTMLAnchorElement> = (evt) => {
evt.preventDefault();
const el = document.getElementById(id);
el?.scrollIntoView({ behavior: 'smooth' });
history.replaceState(undefined, '', `#${id}`);
};
return (
<a
className={classNames('select-none no-underline text-inherit hover:text-inherit', className, {
'transition-opacity opacity-0 focus:opacity-100 group-hover:opacity-70': hover,
'hover:underline': !hover,
})}
onClick={scroll}
href={`#${id}`}
title={title}
aria-label={title}
>
{children}
</a>
);
}
import { HashLink } from './hashLink.js';

const Heading: NodeRenderer<Heading> = ({ node }) => {
const { enumerator, depth, key, identifier, html_id } = node;
Expand Down
2 changes: 1 addition & 1 deletion packages/myst-to-react/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import UNKNOWN_MYST_RENDERERS from './unknown.js';

export { CopyIcon, HoverPopover, Tooltip, LinkCard } from './components/index.js';
export { CodeBlock } from './code.js';
export { HashLink } from './heading.js';
export { HashLink, scrollToElement } from './hashLink.js';
export { Admonition, AdmonitionKind } from './admonitions.js';
export { Details } from './dropdown.js';
export { TabSet, TabItem } from './tabs.js';
Expand Down
2 changes: 1 addition & 1 deletion packages/myst-to-react/src/math.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ExclamationCircleIcon } from '@heroicons/react/24/outline';
import type { InlineMath, Math } from 'myst-spec';
import { InlineError } from './inlineError.js';
import { HashLink } from './heading.js';
import { HashLink } from './hashLink.js';
import type { NodeRenderer } from '@myst-theme/providers';

// function Math({ value, html }: { value: string; html: string }) {
Expand Down
2 changes: 1 addition & 1 deletion packages/myst-to-react/src/proof.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';
import type { NodeRenderer } from '@myst-theme/providers';
import { ChevronRightIcon } from '@heroicons/react/24/solid';
import classNames from 'classnames';
import { HashLink } from './heading.js';
import { HashLink } from './hashLink.js';
import type { GenericNode } from 'myst-common';
import { MyST } from './MyST.js';

Expand Down
54 changes: 54 additions & 0 deletions packages/site/src/components/Footnotes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useGridSystemProvider, useReferences } from '@myst-theme/providers';
import classNames from 'classnames';
import type { GenericNode } from 'myst-common';
import type { FootnoteDefinition, FootnoteReference } from 'myst-spec-ext';
import { HashLink, MyST } from 'myst-to-react';
import { selectAll } from 'unist-util-select';

export function Footnotes() {
const references = useReferences();
const grid = useGridSystemProvider();
const defs = selectAll('footnoteDefinition', references?.article) as FootnoteDefinition[];
const refs = selectAll('footnoteReference', references?.article) as FootnoteReference[];
if (defs.length === 0) return null;
return (
<section id="footnotes" className={classNames(grid, 'subgrid-gap col-screen')}>
<div>
<header className="text-lg font-semibold text-stone-900 dark:text-white group">
Footnotes
<HashLink id="footnotes" title="Link to Footnotes" hover className="ml-2" />
</header>
</div>
<div className="pl-3 mb-8 text-xs text-stone-500 dark:text-stone-300">
<ol>
{defs.map((fn) => {
return (
<li key={(fn as GenericNode).key} id={`fn-${fn.identifier}`} className="group">
<div className="flex flex-row">
<div className="break-words grow">
<MyST ast={fn.children} />
</div>
<div className="flex flex-col grow-0">
{refs
.filter((ref) => ref.identifier === fn.identifier)
.map((ref) => (
<HashLink
key={(ref as GenericNode).key}
id={`fnref-${(ref as GenericNode).key}`}
title="Link to Content"
hover
className="p-1"
children="↩"
scrollBehavior="instant"
/>
))}
</div>
</div>
</li>
);
})}
</ol>
</div>
</section>
);
}
1 change: 1 addition & 0 deletions packages/site/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { DocumentOutline, useOutlineHeight, SupportingDocuments } from './Docume
export { FooterLinksBlock } from './FooterLinksBlock.js';
export { ContentReload } from './ContentReload.js';
export { Bibliography } from './Bibliography.js';
export { Footnotes } from './Footnotes.js';
export { ArticleHeader } from './Headers.js';
export { FrontmatterParts, Abstract, Keywords } from './Abstract.js';
export { BackmatterParts, Backmatter } from './Backmatter.js';
Expand Down
1 change: 1 addition & 0 deletions styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@
@import './hover.css';
@import './proof.css';
@import './toc.css';
@import './backmatter.css';
9 changes: 9 additions & 0 deletions styles/backmatter.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
#footnotes p {
margin: 0.25rem;
}
}
2 changes: 2 additions & 0 deletions themes/article/app/components/Article.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
FrontmatterParts,
BackmatterParts,
extractKnownParts,
Footnotes,
} from '@myst-theme/site';
import { ErrorTray, NotebookToolbar, useComputeOptions } from '@myst-theme/jupyter';
import { FrontmatterBlock } from '@myst-theme/frontmatter';
Expand Down Expand Up @@ -59,6 +60,7 @@ export function Article({
<FrontmatterParts parts={parts} keywords={keywords} hideKeywords={hideKeywords} />
<ContentBlocks mdast={tree as GenericParent} />
<BackmatterParts parts={parts} />
<Footnotes />
<Bibliography />
<ConnectionStatusTray />
</ExecuteScopeProvider>
Expand Down
3 changes: 2 additions & 1 deletion themes/book/app/components/ArticlePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
BackmatterParts,
DocumentOutline,
extractKnownParts,
Footnotes,
} from '@myst-theme/site';
import type { SiteManifest } from 'myst-config';
import type { PageLoader } from '@myst-theme/common';
Expand Down Expand Up @@ -95,6 +96,7 @@ export const ArticlePage = React.memo(function ({
<FrontmatterParts parts={parts} keywords={keywords} hideKeywords={hideKeywords} />
<ContentBlocks pageKind={article.kind} mdast={tree as GenericParent} />
<BackmatterParts parts={parts} />
<Footnotes />
<Bibliography />
<ConnectionStatusTray />
{!hide_footer_links && !hide_all_footer_links && (
Expand All @@ -105,4 +107,3 @@ export const ArticlePage = React.memo(function ({
</ReferencesProvider>
);
});

0 comments on commit e3ab7cd

Please sign in to comment.