Skip to content

Commit

A README no longer blinks while you read it: the Code page follows pushes and checks and revalidates as they arrive, and each time Markdown handed react-markdown a new set of components (a new function for every heading, link, picture and table), so React took the whole README down and put it back, reloading its pictures. Markdown now keeps the parsed text, the components (made once per link and picture base) and the rendered elements while those stay the same, so a refresh leaves the text untouched. The same holds wherever Markdown is drawn: issues, pull requests, comments and docs. Checked: the site's typecheck, its 901 tests and a production build.

syntaqxcommitted Parente40d21dBrowse files
1 file+97−880/1 viewed
+97−88
1010 } from "lucide-react";
1111 import type { Root } from "hast";
1212 import type { Components } from "react-markdown";
13−import { type ReactNode, isValidElement, useEffect, useRef, useState } from "react";
13+import { type ReactNode, isValidElement, useEffect, useMemo, useRef, useState } from "react";
1414 import { Link } from "react-router";
1515 import rehypeRaw from "rehype-raw";
1616 import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
204204 */
205205 rawBase?: string;
206206 }) {
207+ // The same text renders to the same elements with the same components, so
208+ // a page that revalidates (a live refresh) leaves the text as it is
209+ // rather than mounting it again and reloading its pictures.
210+ const tree = useMemo(() => treeOf(source, repo), [source, repo?.namespace, repo?.name]);
211+ const components = useMemo(() => markdownComponents(base, rawBase), [base, rawBase]);
212+ const rendered = useMemo(() => renderMarkdownTree(tree, components), [tree, components]);
207213 return (
208214 // A long word or address breaks rather than widening the page.
209− <div className="prose wrap-break-word">
210− {renderMarkdownTree(treeOf(source, repo), {
211− h1: ({ children }) => <Heading level={1}>{children}</Heading>,
212− h2: ({ children }) => <Heading level={2}>{children}</Heading>,
213− h3: ({ children }) => <Heading level={3}>{children}</Heading>,
214− h4: ({ children }) => <Heading level={4}>{children}</Heading>,
215− a({ href = "", children, node }) {
216− const ref = (node?.properties as { dataRef?: string } | undefined)?.dataRef;
217− if (ref === "mention") {
218− // `@name` may be a person (with a card) or a workspace (none).
219− const name = href === G1T_MENTION_HREF ? "g1t" : href.replace(/^\//, "");
220− return (
221− <UserCard username={name}>
222− <Link to={href} prefetch="intent" className="font-medium">
223− {children}
224− </Link>
225− </UserCard>
226− );
227− }
228− if (ref) {
229− return (
230− <Link
231− to={href}
232− prefetch="intent"
233− className={ref === "commit" ? "font-mono text-[0.9em]" : ref === "mention" ? "font-medium" : ""}
234− >
235− {children}
236− </Link>
237− );
238− }
239− if (href.startsWith("#")) return <a href={href}>{children}</a>;
240− if (isExternal(href)) {
241− return (
242− <a href={href} rel="noreferrer nofollow ugc" target="_blank">
243− {children}
244− </a>
245− );
246− }
247− // A link within the site, or relative to the document's folder.
248− const to = href.startsWith("/") || !base ? href : `${base}/${href.replace(/^\.\//, "")}`;
249− return <Link to={to}>{children}</Link>;
250− },
251− blockquote({ children, node }) {
252− const kind = (node?.properties as { dataAlert?: AlertKind } | undefined)?.dataAlert;
253− if (!kind || !ALERT[kind]) return <blockquote>{children}</blockquote>;
254− const alert = ALERT[kind];
255− return (
256− <div className={`markdown-alert border-l-2 py-1 pl-4 ${alert.tone}`}>
257− <p className="alert-title flex items-center gap-2 text-sm font-medium">
258− {alert.icon}
259− {alert.title}
260− </p>
261− <div className="mt-1 [&>*+*]:mt-3">{children}</div>
262− </div>
263− );
264− },
265− pre({ children }) {
266− const code = Array.isArray(children) ? children[0] : children;
267− if (isValidElement<{ className?: string; children?: ReactNode }>(code)) {
268− const language = /language-([\w+-]+)/.exec(code.props.className ?? "")?.[1] ?? null;
269− return <CodeBlock language={language} code={textOf(code.props.children).replace(/\n$/, "")} />;
270− }
271− return <pre>{children}</pre>;
272− },
273− // A wide table scrolls on its own, inside the text, not the page.
274− table: ({ children }) => (
275− <div className="overflow-x-auto">
276− <table className="max-sm:[&_td]:min-w-32 max-sm:[&_th]:whitespace-nowrap">{children}</table>
277− </div>
278− ),
279− input({ type, checked, disabled }) {
280− // Task list boxes: shown, not editable.
281− return type === "checkbox" ? (
282− <Checkbox
283− checked={checked === true}
284− disabled={disabled !== false}
285− aria-label={checked ? "Done" : "Not done"}
286− className="mr-1.5 inline-flex translate-y-0.5 disabled:cursor-default disabled:opacity-100"
287− />
288− ) : null;
289− },
290− img({ src, alt }) {
291− const at = typeof src === "string" ? imageSource(src, rawBase) : undefined;
292− return <img src={at} alt={alt ?? ""} loading="lazy" className="inline max-w-full rounded" />;
293− },
294− } satisfies Components)}
295− </div>
215+ <div className="prose wrap-break-word">{rendered}</div>
296216 );
297217 }
218+
219+/** How each element is drawn: links relative to `base`, pictures under `rawBase`. */
220+function markdownComponents(base: string | undefined, rawBase: string | undefined): Components {
221+ return {
222+ h1: ({ children }) => <Heading level={1}>{children}</Heading>,
223+ h2: ({ children }) => <Heading level={2}>{children}</Heading>,
224+ h3: ({ children }) => <Heading level={3}>{children}</Heading>,
225+ h4: ({ children }) => <Heading level={4}>{children}</Heading>,
226+ a({ href = "", children, node }) {
227+ const ref = (node?.properties as { dataRef?: string } | undefined)?.dataRef;
228+ if (ref === "mention") {
229+ // `@name` may be a person (with a card) or a workspace (none).
230+ const name = href === G1T_MENTION_HREF ? "g1t" : href.replace(/^\//, "");
231+ return (
232+ <UserCard username={name}>
233+ <Link to={href} prefetch="intent" className="font-medium">
234+ {children}
235+ </Link>
236+ </UserCard>
237+ );
238+ }
239+ if (ref) {
240+ return (
241+ <Link
242+ to={href}
243+ prefetch="intent"
244+ className={ref === "commit" ? "font-mono text-[0.9em]" : ref === "mention" ? "font-medium" : ""}
245+ >
246+ {children}
247+ </Link>
248+ );
249+ }
250+ if (href.startsWith("#")) return <a href={href}>{children}</a>;
251+ if (isExternal(href)) {
252+ return (
253+ <a href={href} rel="noreferrer nofollow ugc" target="_blank">
254+ {children}
255+ </a>
256+ );
257+ }
258+ // A link within the site, or relative to the document's folder.
259+ const to = href.startsWith("/") || !base ? href : `${base}/${href.replace(/^\.\//, "")}`;
260+ return <Link to={to}>{children}</Link>;
261+ },
262+ blockquote({ children, node }) {
263+ const kind = (node?.properties as { dataAlert?: AlertKind } | undefined)?.dataAlert;
264+ if (!kind || !ALERT[kind]) return <blockquote>{children}</blockquote>;
265+ const alert = ALERT[kind];
266+ return (
267+ <div className={`markdown-alert border-l-2 py-1 pl-4 ${alert.tone}`}>
268+ <p className="alert-title flex items-center gap-2 text-sm font-medium">
269+ {alert.icon}
270+ {alert.title}
271+ </p>
272+ <div className="mt-1 [&>*+*]:mt-3">{children}</div>
273+ </div>
274+ );
275+ },
276+ pre({ children }) {
277+ const code = Array.isArray(children) ? children[0] : children;
278+ if (isValidElement<{ className?: string; children?: ReactNode }>(code)) {
279+ const language = /language-([\w+-]+)/.exec(code.props.className ?? "")?.[1] ?? null;
280+ return <CodeBlock language={language} code={textOf(code.props.children).replace(/\n$/, "")} />;
281+ }
282+ return <pre>{children}</pre>;
283+ },
284+ // A wide table scrolls on its own, inside the text, not the page.
285+ table: ({ children }) => (
286+ <div className="overflow-x-auto">
287+ <table className="max-sm:[&_td]:min-w-32 max-sm:[&_th]:whitespace-nowrap">{children}</table>
288+ </div>
289+ ),
290+ input({ type, checked, disabled }) {
291+ // Task list boxes: shown, not editable.
292+ return type === "checkbox" ? (
293+ <Checkbox
294+ checked={checked === true}
295+ disabled={disabled !== false}
296+ aria-label={checked ? "Done" : "Not done"}
297+ className="mr-1.5 inline-flex translate-y-0.5 disabled:cursor-default disabled:opacity-100"
298+ />
299+ ) : null;
300+ },
301+ img({ src, alt }) {
302+ const at = typeof src === "string" ? imageSource(src, rawBase) : undefined;
303+ return <img src={at} alt={alt ?? ""} loading="lazy" className="inline max-w-full rounded" />;
304+ },
305+ } satisfies Components;
306+}