@haruhimemoe/ui 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -1
- package/README.md +246 -11
- package/dist/components/basics/Disclosure.d.ts +1 -1
- package/dist/components/basics/Disclosure.js +2 -2
- package/dist/components/basics/Prose.d.ts +9 -6
- package/dist/components/basics/Prose.js +4 -4
- package/dist/components/forms/Checkbox.d.ts +1 -1
- package/dist/components/forms/Checkbox.js +3 -1
- package/dist/components/forms/RadioGroup.js +3 -1
- package/dist/components/mdx/Callout.d.ts +24 -0
- package/dist/components/mdx/Callout.js +22 -0
- package/dist/components/mdx/CodeBlock.d.ts +26 -0
- package/dist/components/mdx/CodeBlock.js +48 -0
- package/dist/components/mdx/CodeCopyButton.d.ts +21 -0
- package/dist/components/mdx/CodeCopyButton.js +34 -0
- package/dist/components/mdx/MdxBlockquote.d.ts +23 -0
- package/dist/components/mdx/MdxBlockquote.js +17 -0
- package/dist/components/mdx/MdxHeading.d.ts +26 -0
- package/dist/components/mdx/MdxHeading.js +23 -0
- package/dist/components/mdx/MdxLink.d.ts +29 -0
- package/dist/components/mdx/MdxLink.js +26 -0
- package/dist/components/mdx/MdxPre.d.ts +25 -0
- package/dist/components/mdx/MdxPre.js +39 -0
- package/dist/components/mdx/MdxTable.d.ts +20 -0
- package/dist/components/mdx/MdxTable.js +23 -0
- package/dist/components/mdx/highlighter.d.ts +37 -0
- package/dist/components/mdx/highlighter.js +66 -0
- package/dist/components/mdx/mdxComponents.d.ts +21 -0
- package/dist/components/mdx/mdxComponents.js +22 -0
- package/dist/components/mdx/parseCodeMeta.d.ts +21 -0
- package/dist/components/mdx/parseCodeMeta.js +40 -0
- package/dist/components/mdx/textOf.d.ts +17 -0
- package/dist/components/mdx/textOf.js +28 -0
- package/dist/components/palette/CommandPalette.d.ts +21 -0
- package/dist/components/palette/CommandPalette.js +315 -0
- package/dist/components/palette/CommandPaletteButton.d.ts +21 -0
- package/dist/components/palette/CommandPaletteButton.js +26 -0
- package/dist/components/palette/PaletteFooter.d.ts +21 -0
- package/dist/components/palette/PaletteFooter.js +10 -0
- package/dist/components/palette/PaletteInput.d.ts +30 -0
- package/dist/components/palette/PaletteInput.js +23 -0
- package/dist/components/palette/PaletteList.d.ts +29 -0
- package/dist/components/palette/PaletteList.js +44 -0
- package/dist/components/palette/PaletteRow.d.ts +30 -0
- package/dist/components/palette/PaletteRow.js +33 -0
- package/dist/components/palette/calc.d.ts +28 -0
- package/dist/components/palette/calc.js +226 -0
- package/dist/components/palette/fuzzy.d.ts +51 -0
- package/dist/components/palette/fuzzy.js +138 -0
- package/dist/components/palette/hotkeys.d.ts +57 -0
- package/dist/components/palette/hotkeys.js +111 -0
- package/dist/components/palette/paletteEvents.d.ts +22 -0
- package/dist/components/palette/paletteEvents.js +22 -0
- package/dist/components/palette/platform.d.ts +13 -0
- package/dist/components/palette/platform.js +21 -0
- package/dist/components/palette/recents.d.ts +48 -0
- package/dist/components/palette/recents.js +85 -0
- package/dist/components/palette/rows.d.ts +63 -0
- package/dist/components/palette/rows.js +136 -0
- package/dist/components/palette/siteCommands.d.ts +38 -0
- package/dist/components/palette/siteCommands.js +152 -0
- package/dist/components/palette/store.d.ts +94 -0
- package/dist/components/palette/store.js +135 -0
- package/dist/components/palette/types.d.ts +102 -0
- package/dist/components/palette/types.js +11 -0
- package/dist/components/palette/useProviderSearch.d.ts +28 -0
- package/dist/components/palette/useProviderSearch.js +68 -0
- package/dist/components/shell/HeaderMenu.d.ts +1 -1
- package/dist/components/shell/HeaderMenu.js +2 -2
- package/dist/index.d.ts +8 -1
- package/dist/index.js +8 -1
- package/dist/mdx.d.ts +17 -0
- package/dist/mdx.js +17 -0
- package/dist/remark/callouts.d.ts +20 -0
- package/dist/remark/callouts.js +58 -0
- package/dist/remark/codeMeta.d.ts +17 -0
- package/dist/remark/codeMeta.js +22 -0
- package/dist/remark/headingIds.d.ts +16 -0
- package/dist/remark/headingIds.js +28 -0
- package/dist/remark/index.d.ts +29 -0
- package/dist/remark/index.js +33 -0
- package/dist/remark/mdast.d.ts +42 -0
- package/dist/remark/mdast.js +37 -0
- package/dist/remark/slugify.d.ts +23 -0
- package/dist/remark/slugify.js +40 -0
- package/dist/shiki.d.ts +13 -0
- package/dist/shiki.js +38 -0
- package/dist/theme.css +18 -0
- package/package.json +34 -4
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cx } from "../../utils/cx.js";
|
|
3
|
+
import { CodeCopyButton } from "./CodeCopyButton.js";
|
|
4
|
+
import { getHighlighter, THEME } from "./highlighter.js";
|
|
5
|
+
const BASE = "block min-h-6 border-l-2 px-3";
|
|
6
|
+
const LINE = `${BASE} border-transparent`;
|
|
7
|
+
// Never both border colors on one element: Tailwind emits border-h1 before border-transparent.
|
|
8
|
+
const MARKED = `${BASE} border-h1 bg-b4 forced-colors:border-[Highlight]`;
|
|
9
|
+
/** Shiki's tokens per line, or null to render plain text. */
|
|
10
|
+
const tokenize = async (code, lang) => {
|
|
11
|
+
if (!lang)
|
|
12
|
+
return null;
|
|
13
|
+
const highlighter = await getHighlighter();
|
|
14
|
+
if (!highlighter?.getLoadedLanguages().includes(lang.toLowerCase()))
|
|
15
|
+
return null;
|
|
16
|
+
try {
|
|
17
|
+
return highlighter.codeToTokens(code, { lang: lang.toLowerCase(), theme: THEME }).tokens;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const tokenStyle = (token) => ({
|
|
24
|
+
color: token.color,
|
|
25
|
+
fontStyle: (token.fontStyle ?? 0) & 1 ? "italic" : undefined,
|
|
26
|
+
fontWeight: (token.fontStyle ?? 0) & 2 ? "bold" : undefined,
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
29
|
+
* @function CodeBlock
|
|
30
|
+
* @param props {CodeBlockProps} the code, optional language, title, highlighted line numbers and
|
|
31
|
+
* wrapper class
|
|
32
|
+
* @returns {Promise<JSX.Element>} a header (name and copy button) over a highlighted `<pre>`
|
|
33
|
+
*/
|
|
34
|
+
export async function CodeBlock({ code, lang, title, highlight = [], className, ...rest }) {
|
|
35
|
+
const text = code.replace(/\r\n?/g, "\n").replace(/\n$/, "");
|
|
36
|
+
const tokens = await tokenize(text, lang);
|
|
37
|
+
const lines = tokens ?? text.split("\n").map((content) => [{ content, offset: 0 }]);
|
|
38
|
+
const marked = new Set(highlight);
|
|
39
|
+
const name = title ?? lang;
|
|
40
|
+
return (_jsxs("div", { className: cx("mt-3 overflow-hidden rounded-md border border-b3 bg-b6", className), ...rest, children: [_jsxs("div", { className: "flex min-h-9 items-center justify-between gap-3 border-b3 border-b px-3 py-1 text-c3 text-sm", children: [_jsx("span", { className: "truncate", children: name ?? "Code" }), _jsx(CodeCopyButton, { code: text, label: title ? `Copy ${title}` : "Copy code" })] }), _jsx("pre", { role: "group",
|
|
41
|
+
// biome-ignore lint/a11y/noNoninteractiveTabindex: a scrollable region needs keyboard focus
|
|
42
|
+
tabIndex: 0, "aria-label": name ? `Code: ${name}` : "Code", className: "overflow-x-auto py-3 text-c2 text-sm leading-6", children: _jsx("code", { children: lines.flatMap((line, index) => {
|
|
43
|
+
const span = (_jsx("span", { className: marked.has(index + 1) ? MARKED : LINE, "data-highlighted": marked.has(index + 1) ? "" : undefined, children: line.map((token, i) => tokens ? (_jsx("span", { style: tokenStyle(token), children: token.content }, i)) : (token.content)) }, index));
|
|
44
|
+
// A plain "\n" text node between line spans (no key needed on a bare string): kept
|
|
45
|
+
// out of the last line so there's no trailing phantom newline.
|
|
46
|
+
return index < lines.length - 1 ? [span, "\n"] : [span];
|
|
47
|
+
}) }) })] }));
|
|
48
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/CodeCopyButton.tsx
|
|
3
|
+
* @desc The copy button CodeBlock renders beside a code fence's header: copies the plain code
|
|
4
|
+
* text and reports "Copied" or "Copy failed" in a polite live region, like CopyButton. A
|
|
5
|
+
* server component (CodeBlock) renders this client file, so it takes a finished class
|
|
6
|
+
* string and never imports cx, keeping tailwind-merge out of the client bundle.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Sat Oct 3, 2026
|
|
9
|
+
* @modified Sat Oct 3, 2026
|
|
10
|
+
*/
|
|
11
|
+
/** The code to copy, and the button's accessible name (e.g. "Copy x.ts" or "Copy code"). */
|
|
12
|
+
export type CodeCopyButtonProps = {
|
|
13
|
+
code: string;
|
|
14
|
+
label: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* @function CodeCopyButton
|
|
18
|
+
* @param props {CodeCopyButtonProps} the code to copy and the button's accessible name
|
|
19
|
+
* @returns {JSX.Element} the copy button and an `<output>` announcing "Copied" or "Copy failed"
|
|
20
|
+
*/
|
|
21
|
+
export declare function CodeCopyButton({ code, label }: CodeCopyButtonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/CodeCopyButton.tsx
|
|
3
|
+
* @desc The copy button CodeBlock renders beside a code fence's header: copies the plain code
|
|
4
|
+
* text and reports "Copied" or "Copy failed" in a polite live region, like CopyButton. A
|
|
5
|
+
* server component (CodeBlock) renders this client file, so it takes a finished class
|
|
6
|
+
* string and never imports cx, keeping tailwind-merge out of the client bundle.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Sat Oct 3, 2026
|
|
9
|
+
* @modified Sat Oct 3, 2026
|
|
10
|
+
*/
|
|
11
|
+
"use client";
|
|
12
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
|
+
import { StatusOutput } from "../actions/StatusOutput.js";
|
|
14
|
+
import { useLatestStatus } from "../actions/useLatestStatus.js";
|
|
15
|
+
const BUTTON = "inline-flex min-h-6 min-w-6 items-center justify-center rounded px-2 py-0.5 text-c2 text-sm hover:bg-b4 hover:text-c1";
|
|
16
|
+
/**
|
|
17
|
+
* @function CodeCopyButton
|
|
18
|
+
* @param props {CodeCopyButtonProps} the code to copy and the button's accessible name
|
|
19
|
+
* @returns {JSX.Element} the copy button and an `<output>` announcing "Copied" or "Copy failed"
|
|
20
|
+
*/
|
|
21
|
+
export function CodeCopyButton({ code, label }) {
|
|
22
|
+
const { status, start, settle } = useLatestStatus();
|
|
23
|
+
const copy = async () => {
|
|
24
|
+
const run = start();
|
|
25
|
+
try {
|
|
26
|
+
await navigator.clipboard.writeText(code);
|
|
27
|
+
settle(run, "copied");
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
settle(run, "failed");
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
return (_jsxs("span", { className: "flex items-center gap-2", children: [_jsx(StatusOutput, { run: status?.run ?? null, children: status?.result === "copied" ? "Copied" : "Copy failed" }), _jsx("button", { type: "button", "aria-label": label, className: BUTTON, onClick: copy, children: "Copy" })] }));
|
|
34
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/MdxBlockquote.tsx
|
|
3
|
+
* @desc react-markdown/MDX's `blockquote` override: a blockquote carrying `data-callout` (set by
|
|
4
|
+
* the `remarkCallouts` plugin on a GitHub-style `> [!NOTE]` blockquote) renders as a
|
|
5
|
+
* Callout of that type, with every other prop (className, id, …) forwarded to it; any other
|
|
6
|
+
* blockquote renders plainly. Drops the `node` prop react-markdown passes to every
|
|
7
|
+
* component.
|
|
8
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
9
|
+
* @created Sat Oct 3, 2026
|
|
10
|
+
* @modified Sat Oct 3, 2026
|
|
11
|
+
*/
|
|
12
|
+
import type { ComponentProps } from "react";
|
|
13
|
+
/** Every native `<blockquote>` prop, plus `node` (dropped) and the callout plugin's marker. */
|
|
14
|
+
export type MdxBlockquoteProps = ComponentProps<"blockquote"> & {
|
|
15
|
+
node?: unknown;
|
|
16
|
+
"data-callout"?: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* @function MdxBlockquote
|
|
20
|
+
* @param props {MdxBlockquoteProps} native blockquote props and an optional `data-callout` type
|
|
21
|
+
* @returns {JSX.Element} a Callout for a known `data-callout` type, otherwise a plain blockquote
|
|
22
|
+
*/
|
|
23
|
+
export declare function MdxBlockquote({ node: _node, "data-callout": callout, children, ...props }: MdxBlockquoteProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Callout } from "./Callout.js";
|
|
3
|
+
const TYPES = ["note", "tip", "warning"];
|
|
4
|
+
/**
|
|
5
|
+
* @function MdxBlockquote
|
|
6
|
+
* @param props {MdxBlockquoteProps} native blockquote props and an optional `data-callout` type
|
|
7
|
+
* @returns {JSX.Element} a Callout for a known `data-callout` type, otherwise a plain blockquote
|
|
8
|
+
*/
|
|
9
|
+
export function MdxBlockquote({ node: _node, "data-callout": callout, children, ...props }) {
|
|
10
|
+
if (callout && TYPES.includes(callout)) {
|
|
11
|
+
// Callout's props are a div's (ComponentProps<"blockquote"> isn't literally
|
|
12
|
+
// ComponentProps<"div">, e.g. `ref`), but every field react-markdown passes here (className,
|
|
13
|
+
// id, aria-*, data-*) is one Callout already forwards straight onto its own div.
|
|
14
|
+
return (_jsx(Callout, { type: callout, ...props, children: children }));
|
|
15
|
+
}
|
|
16
|
+
return _jsx("blockquote", { ...props, children: children });
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/MdxHeading.tsx
|
|
3
|
+
* @desc react-markdown/MDX's `h2`/`h3` override: gives the heading a stable id (the given `id`,
|
|
4
|
+
* from remark's heading-id plugin, or a slug of its own text) and a hover-revealed anchor
|
|
5
|
+
* link beside it. Drops the `node` prop react-markdown passes to every component.
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Sat Oct 3, 2026
|
|
8
|
+
* @modified Sat Oct 3, 2026
|
|
9
|
+
*/
|
|
10
|
+
import type { ComponentProps } from "react";
|
|
11
|
+
/** Every native heading prop, plus the `node` react-markdown passes (dropped). */
|
|
12
|
+
export type MdxHeadingProps = ComponentProps<"h2"> & {
|
|
13
|
+
node?: unknown;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* @function MdxH2
|
|
17
|
+
* @param props {MdxHeadingProps} native h2 props and an optional id
|
|
18
|
+
* @returns {JSX.Element} an `<h2>` with a slug id and an anchor link
|
|
19
|
+
*/
|
|
20
|
+
export declare const MdxH2: ({ node: _node, id, children, ...props }: MdxHeadingProps) => import("react").JSX.Element;
|
|
21
|
+
/**
|
|
22
|
+
* @function MdxH3
|
|
23
|
+
* @param props {MdxHeadingProps} native h3 props and an optional id
|
|
24
|
+
* @returns {JSX.Element} an `<h3>` with a slug id and an anchor link
|
|
25
|
+
*/
|
|
26
|
+
export declare const MdxH3: ({ node: _node, id, children, ...props }: MdxHeadingProps) => import("react").JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { slugify } from "../../remark/slugify.js";
|
|
3
|
+
import { textOf } from "./textOf.js";
|
|
4
|
+
const WRAP = "group flex items-baseline gap-2 [&:first-child>:first-child]:mt-0";
|
|
5
|
+
// The ! wins over Prose's [&_a]:text-h1 and [&_a]:underline, which outrank plain utilities.
|
|
6
|
+
const ANCHOR = "inline-flex min-h-6 min-w-6 items-center justify-center rounded text-c4! no-underline! hover:text-h1!";
|
|
7
|
+
const heading = (Tag) => function MdxHeading({ node: _node, id, children, ...props }) {
|
|
8
|
+
const text = textOf(children);
|
|
9
|
+
const slug = id ?? slugify(text);
|
|
10
|
+
return (_jsxs("div", { className: WRAP, children: [_jsx(Tag, { id: slug || undefined, ...props, children: children }), slug ? (_jsx("a", { href: `#${slug}`, "aria-label": `Link to section: ${text}`, className: ANCHOR, children: "#" })) : null] }));
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* @function MdxH2
|
|
14
|
+
* @param props {MdxHeadingProps} native h2 props and an optional id
|
|
15
|
+
* @returns {JSX.Element} an `<h2>` with a slug id and an anchor link
|
|
16
|
+
*/
|
|
17
|
+
export const MdxH2 = heading("h2");
|
|
18
|
+
/**
|
|
19
|
+
* @function MdxH3
|
|
20
|
+
* @param props {MdxHeadingProps} native h3 props and an optional id
|
|
21
|
+
* @returns {JSX.Element} an `<h3>` with a slug id and an anchor link
|
|
22
|
+
*/
|
|
23
|
+
export const MdxH3 = heading("h3");
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/MdxLink.tsx
|
|
3
|
+
* @desc react-markdown/MDX's `a` override: an external `http(s)` href opens in a new tab with a
|
|
4
|
+
* safe rel (full props spread, like the `<a>` AutoLink itself renders for an off-app href);
|
|
5
|
+
* a same-page hash link is a plain anchor, since AutoLink's own `isExternalHref` check
|
|
6
|
+
* doesn't treat `#usage` as external and would otherwise send it through next/link, which
|
|
7
|
+
* has no reason to handle a same-page jump; everything else (internal paths, `mailto:` and
|
|
8
|
+
* other schemes) goes through AutoLink, which already picks `next/link` or a plain `<a>` by
|
|
9
|
+
* href and spreads every other prop onto whichever element it renders. No `href` at all (a
|
|
10
|
+
* README `<a name>`/`<a id>` anchor via rehype-raw) renders a plain `<a>` with no `href`
|
|
11
|
+
* attribute, not a focusable empty link. Drops the `node` prop react-markdown passes to
|
|
12
|
+
* every component, so it never reaches the DOM.
|
|
13
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
14
|
+
* @created Sat Oct 3, 2026
|
|
15
|
+
* @modified Sat Oct 3, 2026
|
|
16
|
+
*/
|
|
17
|
+
import type { ComponentProps } from "react";
|
|
18
|
+
/** Every native `<a>` prop, plus the `node` react-markdown passes (dropped, never rendered). */
|
|
19
|
+
export type MdxLinkProps = ComponentProps<"a"> & {
|
|
20
|
+
node?: unknown;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* @function MdxLink
|
|
24
|
+
* @param props {MdxLinkProps} the link's href and native anchor props
|
|
25
|
+
* @returns {JSX.Element} an external link (new tab, `rel="noopener noreferrer"`), a plain anchor
|
|
26
|
+
* for a same-page hash, or an AutoLink (next/link or a plain anchor, by href) otherwise,
|
|
27
|
+
* every other prop forwarded in full
|
|
28
|
+
*/
|
|
29
|
+
export declare function MdxLink({ node: _node, href, children, ...props }: MdxLinkProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { AutoLink } from "../basics/AutoLink.js";
|
|
3
|
+
/**
|
|
4
|
+
* @function MdxLink
|
|
5
|
+
* @param props {MdxLinkProps} the link's href and native anchor props
|
|
6
|
+
* @returns {JSX.Element} an external link (new tab, `rel="noopener noreferrer"`), a plain anchor
|
|
7
|
+
* for a same-page hash, or an AutoLink (next/link or a plain anchor, by href) otherwise,
|
|
8
|
+
* every other prop forwarded in full
|
|
9
|
+
*/
|
|
10
|
+
export function MdxLink({ node: _node, href, children, ...props }) {
|
|
11
|
+
if (href === undefined) {
|
|
12
|
+
return _jsx("a", { ...props, children: children });
|
|
13
|
+
}
|
|
14
|
+
if (/^https?:\/\//i.test(href)) {
|
|
15
|
+
return (_jsx("a", { href: href, target: "_blank", rel: "noopener noreferrer", ...props, children: children }));
|
|
16
|
+
}
|
|
17
|
+
if (href.startsWith("#")) {
|
|
18
|
+
return (_jsx("a", { href: href, ...props, children: children }));
|
|
19
|
+
}
|
|
20
|
+
// AutoLink's props are next/link's (anchor attributes plus next/link-only props like
|
|
21
|
+
// `prefetch`); every prop MdxLink accepts from react-markdown is a native anchor attribute,
|
|
22
|
+
// which AutoLink spreads straight onto whichever element it renders. Narrow cast, like
|
|
23
|
+
// AutoLink's own `href` handling: ComponentProps<"a"> isn't literally ComponentProps<Link>,
|
|
24
|
+
// but every field here is one AutoLink already knows how to forward.
|
|
25
|
+
return (_jsx(AutoLink, { href: href, ...props, children: children }));
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/MdxPre.tsx
|
|
3
|
+
* @desc react-markdown/MDX's `pre` override: a fenced code block (a `<pre>` wrapping a single
|
|
4
|
+
* child with a `language-*` class, a `data-meta` prop, or the literal `code` element type)
|
|
5
|
+
* is handed to CodeBlock with its parsed meta; detecting the fence by the child's props
|
|
6
|
+
* rather than only `child.type === "code"` means an app that overrides the `code` component
|
|
7
|
+
* (through `mdxComponents`' own map or MDX's `useMDXComponents`) still gets highlighting.
|
|
8
|
+
* Anything else renders as a plain, focusable `<pre>`. Drops the `node` prop react-markdown
|
|
9
|
+
* passes to every component.
|
|
10
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
11
|
+
* @created Sat Oct 3, 2026
|
|
12
|
+
* @modified Sat Oct 3, 2026
|
|
13
|
+
*/
|
|
14
|
+
import { type ComponentProps } from "react";
|
|
15
|
+
/** Every native `<pre>` prop, plus the `node` react-markdown passes (dropped). */
|
|
16
|
+
export type MdxPreProps = ComponentProps<"pre"> & {
|
|
17
|
+
node?: unknown;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* @function MdxPre
|
|
21
|
+
* @param props {MdxPreProps} native pre props; a single `code` child carries the language class
|
|
22
|
+
* and the fence's meta string in `data-meta`
|
|
23
|
+
* @returns {JSX.Element} a CodeBlock for a fenced `code` child, otherwise a plain, focusable pre
|
|
24
|
+
*/
|
|
25
|
+
export declare function MdxPre({ node: _node, children, ...props }: MdxPreProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @file src/components/mdx/MdxPre.tsx
|
|
4
|
+
* @desc react-markdown/MDX's `pre` override: a fenced code block (a `<pre>` wrapping a single
|
|
5
|
+
* child with a `language-*` class, a `data-meta` prop, or the literal `code` element type)
|
|
6
|
+
* is handed to CodeBlock with its parsed meta; detecting the fence by the child's props
|
|
7
|
+
* rather than only `child.type === "code"` means an app that overrides the `code` component
|
|
8
|
+
* (through `mdxComponents`' own map or MDX's `useMDXComponents`) still gets highlighting.
|
|
9
|
+
* Anything else renders as a plain, focusable `<pre>`. Drops the `node` prop react-markdown
|
|
10
|
+
* passes to every component.
|
|
11
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
12
|
+
* @created Sat Oct 3, 2026
|
|
13
|
+
* @modified Sat Oct 3, 2026
|
|
14
|
+
*/
|
|
15
|
+
import { Children, isValidElement } from "react";
|
|
16
|
+
import { CodeBlock } from "./CodeBlock.js";
|
|
17
|
+
import { parseCodeMeta } from "./parseCodeMeta.js";
|
|
18
|
+
import { textOf } from "./textOf.js";
|
|
19
|
+
/**
|
|
20
|
+
* @function MdxPre
|
|
21
|
+
* @param props {MdxPreProps} native pre props; a single `code` child carries the language class
|
|
22
|
+
* and the fence's meta string in `data-meta`
|
|
23
|
+
* @returns {JSX.Element} a CodeBlock for a fenced `code` child, otherwise a plain, focusable pre
|
|
24
|
+
*/
|
|
25
|
+
export function MdxPre({ node: _node, children, ...props }) {
|
|
26
|
+
const [child] = Children.toArray(children);
|
|
27
|
+
const isFence = isValidElement(child) &&
|
|
28
|
+
(child.type === "code" ||
|
|
29
|
+
/(?:^|\s)language-/.test(child.props.className ?? "") ||
|
|
30
|
+
child.props["data-meta"] !== undefined);
|
|
31
|
+
if (!isFence || !isValidElement(child)) {
|
|
32
|
+
return (_jsx("pre", {
|
|
33
|
+
// biome-ignore lint/a11y/noNoninteractiveTabindex: a scrollable region needs keyboard focus
|
|
34
|
+
tabIndex: 0, ...props, children: children }));
|
|
35
|
+
}
|
|
36
|
+
const lang = /(?:^|\s)language-(\S+)/.exec(child.props.className ?? "")?.[1];
|
|
37
|
+
const { title, highlight } = parseCodeMeta(child.props["data-meta"]);
|
|
38
|
+
return (_jsx(CodeBlock, { code: textOf(child.props.children), lang: lang, title: title, highlight: highlight }));
|
|
39
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/MdxTable.tsx
|
|
3
|
+
* @desc react-markdown/MDX's `table` override: wraps the table in a named, focusable scroll
|
|
4
|
+
* region (a table can overflow its column on narrow screens), named by its caption when
|
|
5
|
+
* one is given. Drops the `node` prop react-markdown passes to every component.
|
|
6
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
7
|
+
* @created Sat Oct 3, 2026
|
|
8
|
+
* @modified Sat Oct 3, 2026
|
|
9
|
+
*/
|
|
10
|
+
import { type ComponentProps } from "react";
|
|
11
|
+
/** Every native `<table>` prop, plus the `node` react-markdown passes (dropped). */
|
|
12
|
+
export type MdxTableProps = ComponentProps<"table"> & {
|
|
13
|
+
node?: unknown;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* @function MdxTable
|
|
17
|
+
* @param props {MdxTableProps} native table props; a `caption` child names the scroll region
|
|
18
|
+
* @returns {JSX.Element} a horizontally scrollable, focusable, labelled region wrapping the table
|
|
19
|
+
*/
|
|
20
|
+
export declare function MdxTable({ node: _node, children, ...props }: MdxTableProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* @file src/components/mdx/MdxTable.tsx
|
|
4
|
+
* @desc react-markdown/MDX's `table` override: wraps the table in a named, focusable scroll
|
|
5
|
+
* region (a table can overflow its column on narrow screens), named by its caption when
|
|
6
|
+
* one is given. Drops the `node` prop react-markdown passes to every component.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Sat Oct 3, 2026
|
|
9
|
+
* @modified Sat Oct 3, 2026
|
|
10
|
+
*/
|
|
11
|
+
import { Children, isValidElement } from "react";
|
|
12
|
+
import { textOf } from "./textOf.js";
|
|
13
|
+
/**
|
|
14
|
+
* @function MdxTable
|
|
15
|
+
* @param props {MdxTableProps} native table props; a `caption` child names the scroll region
|
|
16
|
+
* @returns {JSX.Element} a horizontally scrollable, focusable, labelled region wrapping the table
|
|
17
|
+
*/
|
|
18
|
+
export function MdxTable({ node: _node, children, ...props }) {
|
|
19
|
+
const caption = Children.toArray(children).find((child) => isValidElement(child) && child.type === "caption");
|
|
20
|
+
return (_jsx("div", { role: "group", "aria-label": textOf(caption) || "Table",
|
|
21
|
+
// biome-ignore lint/a11y/noNoninteractiveTabindex: a scrollable region needs keyboard focus
|
|
22
|
+
tabIndex: 0, className: "overflow-x-auto", children: _jsx("table", { ...props, children: children }) }));
|
|
23
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/highlighter.ts
|
|
3
|
+
* @desc The highlighter CodeBlock asks for. Nothing here names Shiki outside `import type`, so
|
|
4
|
+
* an app without Shiki builds: `@haruhimemoe/ui/shiki` (src/shiki.ts) registers the real
|
|
5
|
+
* loader when an app imports it. The loader lives in a `globalThis` slot keyed by
|
|
6
|
+
* `Symbol.for`, so it survives duplicate copies of this module. The first highlighter is
|
|
7
|
+
* cached; with no loader registered, or a loader that rejects, code renders plain (warned
|
|
8
|
+
* once, outside production) rather than throwing.
|
|
9
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
10
|
+
* @created Sat Oct 3, 2026
|
|
11
|
+
* @modified Sat Oct 3, 2026
|
|
12
|
+
*/
|
|
13
|
+
import type { HighlighterCore } from "shiki/core";
|
|
14
|
+
/** The Shiki CSS-variables theme name CodeBlock renders with. */
|
|
15
|
+
export declare const THEME = "haruhime";
|
|
16
|
+
/** Loads a Shiki highlighter: what `@haruhimemoe/ui/shiki` registers. */
|
|
17
|
+
export type HighlighterLoader = () => Promise<HighlighterCore>;
|
|
18
|
+
/**
|
|
19
|
+
* @function setHighlighterLoader
|
|
20
|
+
* @param loader {HighlighterLoader} loads the Shiki highlighter; replaces any earlier one
|
|
21
|
+
* @returns {void} registers the loader in the global slot and drops the cached highlighter
|
|
22
|
+
*/
|
|
23
|
+
export declare const setHighlighterLoader: (loader: HighlighterLoader) => void;
|
|
24
|
+
/**
|
|
25
|
+
* @function getHighlighter
|
|
26
|
+
* @param loader {HighlighterLoader | undefined} loads the highlighter; defaults to the registered
|
|
27
|
+
* one, overridable in tests
|
|
28
|
+
* @returns {Promise<HighlighterCore | null>} the cached highlighter, or null when no loader is
|
|
29
|
+
* registered or it failed to load
|
|
30
|
+
*/
|
|
31
|
+
export declare const getHighlighter: (loader?: HighlighterLoader | undefined) => Promise<HighlighterCore | null>;
|
|
32
|
+
/**
|
|
33
|
+
* @function resetHighlighter
|
|
34
|
+
* @returns {void} clears the registered loader, the cached highlighter and the warned flag
|
|
35
|
+
* (tests only)
|
|
36
|
+
*/
|
|
37
|
+
export declare const resetHighlighter: () => void;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/highlighter.ts
|
|
3
|
+
* @desc The highlighter CodeBlock asks for. Nothing here names Shiki outside `import type`, so
|
|
4
|
+
* an app without Shiki builds: `@haruhimemoe/ui/shiki` (src/shiki.ts) registers the real
|
|
5
|
+
* loader when an app imports it. The loader lives in a `globalThis` slot keyed by
|
|
6
|
+
* `Symbol.for`, so it survives duplicate copies of this module. The first highlighter is
|
|
7
|
+
* cached; with no loader registered, or a loader that rejects, code renders plain (warned
|
|
8
|
+
* once, outside production) rather than throwing.
|
|
9
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
10
|
+
* @created Sat Oct 3, 2026
|
|
11
|
+
* @modified Sat Oct 3, 2026
|
|
12
|
+
*/
|
|
13
|
+
/** The Shiki CSS-variables theme name CodeBlock renders with. */
|
|
14
|
+
export const THEME = "haruhime";
|
|
15
|
+
const SLOT = Symbol.for("@haruhimemoe/ui/highlighter");
|
|
16
|
+
// No node types in the build (tsconfig.build.json "types": []), so read process through globalThis.
|
|
17
|
+
const isProduction = () => globalThis.process?.env
|
|
18
|
+
?.NODE_ENV === "production";
|
|
19
|
+
let cached;
|
|
20
|
+
let warned = false;
|
|
21
|
+
const warnOnce = (message, error) => {
|
|
22
|
+
if (warned || isProduction())
|
|
23
|
+
return;
|
|
24
|
+
warned = true;
|
|
25
|
+
if (error === undefined)
|
|
26
|
+
console.warn(message);
|
|
27
|
+
else
|
|
28
|
+
console.warn(message, error);
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* @function setHighlighterLoader
|
|
32
|
+
* @param loader {HighlighterLoader} loads the Shiki highlighter; replaces any earlier one
|
|
33
|
+
* @returns {void} registers the loader in the global slot and drops the cached highlighter
|
|
34
|
+
*/
|
|
35
|
+
export const setHighlighterLoader = (loader) => {
|
|
36
|
+
globalThis[SLOT] = loader;
|
|
37
|
+
cached = undefined;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* @function getHighlighter
|
|
41
|
+
* @param loader {HighlighterLoader | undefined} loads the highlighter; defaults to the registered
|
|
42
|
+
* one, overridable in tests
|
|
43
|
+
* @returns {Promise<HighlighterCore | null>} the cached highlighter, or null when no loader is
|
|
44
|
+
* registered or it failed to load
|
|
45
|
+
*/
|
|
46
|
+
export const getHighlighter = (loader = globalThis[SLOT]) => {
|
|
47
|
+
if (!loader) {
|
|
48
|
+
warnOnce('@haruhimemoe/ui: code blocks aren\'t highlighted. Install shiki and add `import "@haruhimemoe/ui/shiki";` once, e.g. in mdx-components.tsx.');
|
|
49
|
+
return Promise.resolve(null);
|
|
50
|
+
}
|
|
51
|
+
cached ??= loader().catch((error) => {
|
|
52
|
+
warnOnce("@haruhimemoe/ui: Shiki didn't load, so code blocks aren't highlighted.", error);
|
|
53
|
+
return null;
|
|
54
|
+
});
|
|
55
|
+
return cached;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* @function resetHighlighter
|
|
59
|
+
* @returns {void} clears the registered loader, the cached highlighter and the warned flag
|
|
60
|
+
* (tests only)
|
|
61
|
+
*/
|
|
62
|
+
export const resetHighlighter = () => {
|
|
63
|
+
globalThis[SLOT] = undefined;
|
|
64
|
+
cached = undefined;
|
|
65
|
+
warned = false;
|
|
66
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/mdxComponents.ts
|
|
3
|
+
* @desc The element override map for @next/mdx's `useMDXComponents` and react-markdown's
|
|
4
|
+
* `components` prop: links, headings (h2/h3), fenced code, tables and callout blockquotes.
|
|
5
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
6
|
+
* @created Sat Oct 3, 2026
|
|
7
|
+
* @modified Sat Oct 3, 2026
|
|
8
|
+
*/
|
|
9
|
+
import { MdxBlockquote } from "./MdxBlockquote.js";
|
|
10
|
+
import { MdxLink } from "./MdxLink.js";
|
|
11
|
+
import { MdxPre } from "./MdxPre.js";
|
|
12
|
+
import { MdxTable } from "./MdxTable.js";
|
|
13
|
+
/** The element overrides for @next/mdx's useMDXComponents and react-markdown's `components`. */
|
|
14
|
+
export declare const mdxComponents: {
|
|
15
|
+
a: typeof MdxLink;
|
|
16
|
+
blockquote: typeof MdxBlockquote;
|
|
17
|
+
h2: ({ node: _node, id, children, ...props }: import("./MdxHeading.js").MdxHeadingProps) => import("react").JSX.Element;
|
|
18
|
+
h3: ({ node: _node, id, children, ...props }: import("./MdxHeading.js").MdxHeadingProps) => import("react").JSX.Element;
|
|
19
|
+
pre: typeof MdxPre;
|
|
20
|
+
table: typeof MdxTable;
|
|
21
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/mdxComponents.ts
|
|
3
|
+
* @desc The element override map for @next/mdx's `useMDXComponents` and react-markdown's
|
|
4
|
+
* `components` prop: links, headings (h2/h3), fenced code, tables and callout blockquotes.
|
|
5
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
6
|
+
* @created Sat Oct 3, 2026
|
|
7
|
+
* @modified Sat Oct 3, 2026
|
|
8
|
+
*/
|
|
9
|
+
import { MdxBlockquote } from "./MdxBlockquote.js";
|
|
10
|
+
import { MdxH2, MdxH3 } from "./MdxHeading.js";
|
|
11
|
+
import { MdxLink } from "./MdxLink.js";
|
|
12
|
+
import { MdxPre } from "./MdxPre.js";
|
|
13
|
+
import { MdxTable } from "./MdxTable.js";
|
|
14
|
+
/** The element overrides for @next/mdx's useMDXComponents and react-markdown's `components`. */
|
|
15
|
+
export const mdxComponents = {
|
|
16
|
+
a: MdxLink,
|
|
17
|
+
blockquote: MdxBlockquote,
|
|
18
|
+
h2: MdxH2,
|
|
19
|
+
h3: MdxH3,
|
|
20
|
+
pre: MdxPre,
|
|
21
|
+
table: MdxTable,
|
|
22
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/parseCodeMeta.ts
|
|
3
|
+
* @desc Parses a fenced code block's meta string (the text after the language on ```ts fences)
|
|
4
|
+
* into a title (`title="..."` or `title='...'`) and a sorted, deduped list of highlighted
|
|
5
|
+
* line numbers (`{1,3-5}`), capped at 10000 lines so a huge or malicious range can't hang
|
|
6
|
+
* the render.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Sat Oct 3, 2026
|
|
9
|
+
* @modified Sat Oct 3, 2026
|
|
10
|
+
*/
|
|
11
|
+
/** A parsed code fence meta string. */
|
|
12
|
+
export type CodeMeta = {
|
|
13
|
+
title?: string;
|
|
14
|
+
highlight: number[];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* @function parseCodeMeta
|
|
18
|
+
* @param meta {string | null | undefined} the code fence's meta string, if any
|
|
19
|
+
* @returns {CodeMeta} the title (when present) and the sorted, deduped, capped highlight lines
|
|
20
|
+
*/
|
|
21
|
+
export declare const parseCodeMeta: (meta?: string | null) => CodeMeta;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/parseCodeMeta.ts
|
|
3
|
+
* @desc Parses a fenced code block's meta string (the text after the language on ```ts fences)
|
|
4
|
+
* into a title (`title="..."` or `title='...'`) and a sorted, deduped list of highlighted
|
|
5
|
+
* line numbers (`{1,3-5}`), capped at 10000 lines so a huge or malicious range can't hang
|
|
6
|
+
* the render.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Sat Oct 3, 2026
|
|
9
|
+
* @modified Sat Oct 3, 2026
|
|
10
|
+
*/
|
|
11
|
+
const TITLE = /\btitle=(?:"([^"]*)"|'([^']*)')/;
|
|
12
|
+
const RANGES = /\{([^}]*)\}/;
|
|
13
|
+
const MAX_LINES = 10000;
|
|
14
|
+
/**
|
|
15
|
+
* @function parseCodeMeta
|
|
16
|
+
* @param meta {string | null | undefined} the code fence's meta string, if any
|
|
17
|
+
* @returns {CodeMeta} the title (when present) and the sorted, deduped, capped highlight lines
|
|
18
|
+
*/
|
|
19
|
+
export const parseCodeMeta = (meta) => {
|
|
20
|
+
const text = meta ?? "";
|
|
21
|
+
const lines = new Set();
|
|
22
|
+
for (const part of RANGES.exec(text)?.[1]?.split(",") ?? []) {
|
|
23
|
+
const match = /^\s*(\d+)\s*(?:-\s*(\d+)\s*)?$/.exec(part);
|
|
24
|
+
if (!match)
|
|
25
|
+
continue;
|
|
26
|
+
let from = Number(match[1]);
|
|
27
|
+
let to = match[2] === undefined ? from : Number(match[2]);
|
|
28
|
+
if (from > to)
|
|
29
|
+
[from, to] = [to, from];
|
|
30
|
+
from = Math.max(from, 1);
|
|
31
|
+
for (let line = from; line <= to && lines.size < MAX_LINES; line++)
|
|
32
|
+
lines.add(line);
|
|
33
|
+
}
|
|
34
|
+
const title = TITLE.exec(text);
|
|
35
|
+
const result = { highlight: [...lines].sort((a, b) => a - b) };
|
|
36
|
+
const name = title?.[1] ?? title?.[2];
|
|
37
|
+
if (name)
|
|
38
|
+
result.title = name;
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/textOf.ts
|
|
3
|
+
* @desc Flattens a ReactNode tree to plain text: strings and numbers pass through, arrays and
|
|
4
|
+
* element children are recursively joined, and null/undefined/boolean contribute nothing.
|
|
5
|
+
* Used to get a code block's plain text for Shiki and the copy button, since react-markdown
|
|
6
|
+
* hands components React children, not a string.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Sat Oct 3, 2026
|
|
9
|
+
* @modified Sat Oct 3, 2026
|
|
10
|
+
*/
|
|
11
|
+
import { type ReactNode } from "react";
|
|
12
|
+
/**
|
|
13
|
+
* @function textOf
|
|
14
|
+
* @param node {ReactNode} the node (or tree of nodes) to flatten
|
|
15
|
+
* @returns {string} the concatenated text content
|
|
16
|
+
*/
|
|
17
|
+
export declare const textOf: (node: ReactNode) => string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file src/components/mdx/textOf.ts
|
|
3
|
+
* @desc Flattens a ReactNode tree to plain text: strings and numbers pass through, arrays and
|
|
4
|
+
* element children are recursively joined, and null/undefined/boolean contribute nothing.
|
|
5
|
+
* Used to get a code block's plain text for Shiki and the copy button, since react-markdown
|
|
6
|
+
* hands components React children, not a string.
|
|
7
|
+
* @author David @dvhsh (https://dvh.sh)
|
|
8
|
+
* @created Sat Oct 3, 2026
|
|
9
|
+
* @modified Sat Oct 3, 2026
|
|
10
|
+
*/
|
|
11
|
+
import { isValidElement } from "react";
|
|
12
|
+
/**
|
|
13
|
+
* @function textOf
|
|
14
|
+
* @param node {ReactNode} the node (or tree of nodes) to flatten
|
|
15
|
+
* @returns {string} the concatenated text content
|
|
16
|
+
*/
|
|
17
|
+
export const textOf = (node) => {
|
|
18
|
+
if (node === null || node === undefined || typeof node === "boolean")
|
|
19
|
+
return "";
|
|
20
|
+
if (typeof node === "string" || typeof node === "number" || typeof node === "bigint") {
|
|
21
|
+
return String(node);
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(node))
|
|
24
|
+
return node.map(textOf).join("");
|
|
25
|
+
if (isValidElement(node))
|
|
26
|
+
return textOf(node.props.children);
|
|
27
|
+
return "";
|
|
28
|
+
};
|