import { Code } from '@luke-ui/react/code'; import { Grid } from '@luke-ui/react/grid'; import type { HeadingLevel } from '@luke-ui/react/heading'; import { Heading, HeadingLevels } from '@luke-ui/react/heading'; import { IconButton } from '@luke-ui/react/icon-button'; import type { LinkProps } from '@luke-ui/react/link'; import { Link as LukeLink } from '@luke-ui/react/link'; import { ScrollFade } from '@luke-ui/react/scroll-fade'; import { Text } from '@luke-ui/react/text'; import { cx } from '@luke-ui/react/utils'; import { VisuallyHidden } from '@luke-ui/react/visually-hidden'; import type { ComponentPropsWithoutRef, ReactNode } from 'react'; import { createContext, use } from 'react'; import { useCopyButton } from '../../lib/use-copy-button.js'; import * as codeBlockStyles from '../code-block/code-block.css.js'; import type { CodeBlockProps } from '../code-block/code-block.js'; import { CodeBlock } from '../code-block/code-block.js'; import { DocsCard } from '../docs-card.js'; import { DocsLink } from '../docs-link.js'; import { LinkIcon } from '../link-icon.js'; import * as styles from './mdx-elements.css.js'; const EXTERNAL_HREF_PATTERN = /^(?:\w+:|\/\/)/; // MDX passes attributes, not handlers. React Aria Components' `Link` types its own event handlers. type AnchorProps = Omit< ComponentPropsWithoutRef<'a'>, 'dangerouslySetInnerHTML' | 'style' | `on${string}` >; const EXTERNAL_REL_TOKENS = ['noreferrer', 'noopener']; /** * Link in MDX prose. External URLs open in a new tab. Internal paths navigate client-side. Every * anchor attribute the author writes reaches the DOM. */ export function MdxLink({ children, href, ...props }: AnchorProps) { // Without an `href` the anchor is a placeholder: not interactive, but still a valid target for // `id` and a carrier for the author's other attributes. if (href === undefined) return {children}; // React Aria Components drops the anchor attributes it does not model, such as `title` and // `aria-expanded`. `render` puts the author's attributes under the ones it keeps, so it still // owns `href`, `rel`, `target`, handlers, and `ref`. Every branch below passes an `href`, so the // `span` fallback only satisfies the render type. const render: LinkProps['render'] = (domProps) => 'href' in domProps ? ( {domProps.children} ) : ( ); if (EXTERNAL_HREF_PATTERN.test(href)) { const target = props.target ?? '_blank'; return ( {children} ); } // A same-page anchor needs no router. The browser scrolls to it. if (href.startsWith('#')) { return ( {children} ); } const { hash, path } = splitHash(href); return ( {children} ); } /** Adds `noreferrer noopener` to the author's `rel` tokens when the link opens a new browsing context. */ function mergeRel(rel: string | undefined, target: string) { const tokens = rel?.split(/\s+/).filter(Boolean) ?? []; if (target === '_blank') { for (const token of EXTERNAL_REL_TOKENS) { if (!tokens.includes(token)) tokens.push(token); } } return tokens.length > 0 ? tokens.join(' ') : undefined; } /** Builds the heading renderer for one level, with a link and a copy button when it has an `id`. */ export function createMdxHeading(level: HeadingLevel) { return function MdxHeading({ children, id, }: Pick, 'children' | 'id'>) { if (id === undefined) { return ( {children} ); } // The copy button sits inside the heading to keep its spacing and alignment, so the heading // takes its name from the anchor alone. `:` cannot appear in a generated slug, so the anchor id // never collides with another heading's id. const titleId = `${id}:title`; return ( {children} ); }; } function CopyAnchorButton({ id }: { id: string }) { const [copied, onCopy] = useCopyButton(() => { const url = new URL(window.location.href); url.hash = id; return navigator.clipboard.writeText(url.toString()); }); return ( } onPress={onCopy} prominence="low" size="small" /> {copied ? 'Copied' : ''} ); } // MDX wraps text on its own line inside a JSX element in a paragraph. A component that sets its own // typography flags the paragraphs it receives so they inherit it. const InheritTypographyContext = createContext(false); /** * Makes the MDX paragraphs inside it inherit the surrounding typography, colour, and weight instead * of the body style. Wrap generated Markdown that a component sets in its own `Text`. */ export function InheritTypography({ children }: { children: ReactNode }) { return {children}; } export function MdxParagraph({ children }: Pick, 'children'>) { const shouldInheritFont = use(InheritTypographyContext); return ( {children} ); } // MDX resolves a fence's `` child through the same `code` mapping as inline code. The fence // provides this flag so `MdxCode` leaves its markup to the docs `CodeBlock`. const FenceContext = createContext(false); /** Fenced code block. The docs `CodeBlock` owns highlighting and copying. */ export function MdxFence(props: CodeBlockProps) { return ( ); } /** Inline code. Code inside a fence renders as plain ``. */ export function MdxCode(props: ComponentPropsWithoutRef<'code'>) { const isInFence = use(FenceContext); if (isInFence) return ; return ; } export function MdxImage({ alt, className, ...props }: ComponentPropsWithoutRef<'img'>) { return {alt}; } /** Table that scrolls on the inline axis inside its own bordered region. */ export function MdxTable({ className, ...props }: ComponentPropsWithoutRef<'table'>) { return ( ); } export function Cards({ className, ...props }: ComponentPropsWithoutRef<'div'>) { // Continue learning is an h2 in MDX; card titles step to h3 for the outline. return ( ); } interface CardProps { /** Description text. `description` wins when both are set. */ children?: ReactNode; description?: ReactNode; href: string; title: ReactNode; } /** Same card chrome as the components index: title, short description, whole-surface link. */ export function Card({ children, description, href, title }: CardProps) { return ( {description ?? children}} href={href} title={title} /> ); } /** Splits `/path#hash` into the path and the hash TanStack Router takes as a separate prop. */ function splitHash(href: string) { const hashIndex = href.indexOf('#'); if (hashIndex === -1) return { hash: undefined, path: href }; return { hash: href.slice(hashIndex + 1) || undefined, path: href.slice(0, hashIndex) }; }