): ReactNode {
+ const { hasCommand, withCommand } = useCommandContext();
+
+ const prismTheme = usePrismTheme();
+ const prismCssVariables = getPrismCssVariables(prismTheme);
+
+ return (
+
+ );
+}
diff --git a/src/theme/CodeBlock/Container/styles.module.css b/src/theme/CodeBlock/Container/styles.module.css
new file mode 100644
index 0000000..d669f6e
--- /dev/null
+++ b/src/theme/CodeBlock/Container/styles.module.css
@@ -0,0 +1,25 @@
+.codeBlockContainer,
+.codeBlockContainerCommand,
+.codeBlockContainerWithCommand {
+ background: var(--prism-background-color);
+ color: var(--prism-color);
+ box-shadow: var(--ifm-global-shadow-lw);
+}
+
+.codeBlockContainer {
+ border-radius: var(--ifm-code-border-radius);
+ margin-bottom: var(--ifm-leading);
+}
+
+.codeBlockContainerCommand {
+ border-top-left-radius: var(--ifm-code-border-radius);
+ border-top-right-radius: var(--ifm-code-border-radius);
+ border-bottom: 1px solid var(--ifm-color-gray-400);
+}
+
+.codeBlockContainerWithCommand {
+ margin-bottom: var(--ifm-leading);
+ box-shadow: var(--ifm-global-shadow-lw);
+ border-bottom-left-radius: var(--ifm-code-border-radius);
+ border-bottom-right-radius: var(--ifm-code-border-radius);
+}
diff --git a/src/theme/CodeBlock/Content/Element.tsx b/src/theme/CodeBlock/Content/Element.tsx
new file mode 100644
index 0000000..d0bf90a
--- /dev/null
+++ b/src/theme/CodeBlock/Content/Element.tsx
@@ -0,0 +1,26 @@
+import Container from "@theme/CodeBlock/Container";
+import type { Props } from "@theme/CodeBlock/Content/Element";
+import clsx from "clsx";
+import { type ReactNode } from "react";
+
+import styles from "./styles.module.css";
+
+// TODO Docusaurus v4: move this component at the root?
+// This component only handles a rare edge-case:
in MDX
+// tags in markdown map to CodeBlocks. They may contain JSX children.
+// When children is not a simple string, we just return a styled block without
+// actually highlighting.
+export default function CodeBlockJSX({
+ children,
+ className,
+}: Props): ReactNode {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/theme/CodeBlock/Content/String.tsx b/src/theme/CodeBlock/Content/String.tsx
new file mode 100644
index 0000000..799fb21
--- /dev/null
+++ b/src/theme/CodeBlock/Content/String.tsx
@@ -0,0 +1,35 @@
+import { useThemeConfig } from "@docusaurus/theme-common";
+import {
+ CodeBlockContextProvider,
+ type CodeBlockMetadata,
+ createCodeBlockMetadata,
+ useCodeWordWrap,
+} from "@docusaurus/theme-common/internal";
+import type { Props } from "@theme/CodeBlock/Content/String";
+import CodeBlockLayout from "@theme/CodeBlock/Layout";
+import { type ReactNode } from "react";
+
+function useCodeBlockMetadata(props: Props): CodeBlockMetadata {
+ const { prism } = useThemeConfig();
+ return createCodeBlockMetadata({
+ code: props.children,
+ className: props.className,
+ metastring: props.metastring,
+ magicComments: prism.magicComments,
+ defaultLanguage: prism.defaultLanguage,
+ language: props.language,
+ title: props.title,
+ showLineNumbers: props.showLineNumbers,
+ });
+}
+
+// TODO Docusaurus v4: move this component at the root?
+export default function CodeBlockString(props: Props): ReactNode {
+ const metadata = useCodeBlockMetadata(props);
+ const wordWrap = useCodeWordWrap();
+ return (
+
+
+
+ );
+}
diff --git a/src/theme/CodeBlock/Content/index.tsx b/src/theme/CodeBlock/Content/index.tsx
new file mode 100644
index 0000000..60a8d55
--- /dev/null
+++ b/src/theme/CodeBlock/Content/index.tsx
@@ -0,0 +1,77 @@
+import React, {type ComponentProps, type ReactNode} from 'react';
+import clsx from 'clsx';
+import {useCodeBlockContext} from '@docusaurus/theme-common/internal';
+import {usePrismTheme} from '@docusaurus/theme-common';
+import {Highlight} from 'prism-react-renderer';
+import type {Props} from '@theme/CodeBlock/Content';
+import Line from '@theme/CodeBlock/Line';
+
+import styles from './styles.module.css';
+
+// TODO Docusaurus v4: remove useless forwardRef
+const Pre = React.forwardRef>(
+ (props, ref) => {
+ return (
+
+ );
+ },
+);
+
+function Code(props: ComponentProps<'code'>) {
+ const {metadata} = useCodeBlockContext();
+ return (
+
+ );
+}
+
+export default function CodeBlockContent({
+ className: classNameProp,
+}: Props): ReactNode {
+ const {metadata, wordWrap} = useCodeBlockContext();
+ const prismTheme = usePrismTheme();
+ const {code, language, lineNumbersStart, lineClassNames} = metadata;
+ return (
+
+ {({className, style, tokens: lines, getLineProps, getTokenProps}) => (
+
+
+ {lines.map((line, i) => (
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/src/theme/CodeBlock/Content/styles.module.css b/src/theme/CodeBlock/Content/styles.module.css
new file mode 100644
index 0000000..7ea8c7d
--- /dev/null
+++ b/src/theme/CodeBlock/Content/styles.module.css
@@ -0,0 +1,28 @@
+.codeBlock {
+ --ifm-pre-background: var(--prism-background-color);
+ margin: 0;
+ padding: 0;
+}
+
+.codeBlockStandalone {
+ padding: 0;
+}
+
+.codeBlockLines {
+ font: inherit;
+ /* rtl:ignore */
+ float: left;
+ min-width: 100%;
+ padding: var(--ifm-pre-padding);
+}
+
+.codeBlockLinesWithNumbering {
+ display: table;
+ padding: var(--ifm-pre-padding) 0;
+}
+
+@media print {
+ .codeBlockLines {
+ white-space: pre-wrap;
+ }
+}
diff --git a/src/theme/CodeBlock/Layout/index.tsx b/src/theme/CodeBlock/Layout/index.tsx
new file mode 100644
index 0000000..5ea6486
--- /dev/null
+++ b/src/theme/CodeBlock/Layout/index.tsx
@@ -0,0 +1,27 @@
+import { useCodeBlockContext } from "@docusaurus/theme-common/internal";
+import Buttons from "@theme/CodeBlock/Buttons";
+import Container from "@theme/CodeBlock/Container";
+import Content from "@theme/CodeBlock/Content";
+import type { Props } from "@theme/CodeBlock/Layout";
+import Title from "@theme/CodeBlock/Title";
+import clsx from "clsx";
+import { type ReactNode } from "react";
+
+import styles from "./styles.module.css";
+
+export default function CodeBlockLayout({ className }: Props): ReactNode {
+ const { metadata } = useCodeBlockContext();
+ return (
+
+ {metadata.title && (
+
+
{metadata.title}
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/src/theme/CodeBlock/Layout/styles.module.css b/src/theme/CodeBlock/Layout/styles.module.css
new file mode 100644
index 0000000..2bb13ec
--- /dev/null
+++ b/src/theme/CodeBlock/Layout/styles.module.css
@@ -0,0 +1,20 @@
+.codeBlockContent {
+ position: relative;
+ /* rtl:ignore */
+ direction: ltr;
+ border-radius: inherit;
+}
+
+.codeBlockTitle {
+ border-bottom: 1px solid var(--ifm-color-emphasis-300);
+ font-size: var(--ifm-code-font-size);
+ font-weight: 500;
+ padding: 0.75rem var(--ifm-pre-padding);
+ border-top-left-radius: inherit;
+ border-top-right-radius: inherit;
+}
+
+.codeBlockTitle + .codeBlockContent .codeBlock {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
diff --git a/src/theme/CodeBlock/Line/Token/index.tsx b/src/theme/CodeBlock/Line/Token/index.tsx
new file mode 100644
index 0000000..db1b470
--- /dev/null
+++ b/src/theme/CodeBlock/Line/Token/index.tsx
@@ -0,0 +1,11 @@
+import React, {type ReactNode} from 'react';
+import type {Props} from '@theme/CodeBlock/Line/Token';
+
+// Pass-through components that users can swizzle and customize
+export default function CodeBlockLineToken({
+ line,
+ token,
+ ...props
+}: Props): ReactNode {
+ return ;
+}
diff --git a/src/theme/CodeBlock/Line/index.tsx b/src/theme/CodeBlock/Line/index.tsx
new file mode 100644
index 0000000..1e11ab1
--- /dev/null
+++ b/src/theme/CodeBlock/Line/index.tsx
@@ -0,0 +1,59 @@
+import React, {type ReactNode} from 'react';
+import clsx from 'clsx';
+import LineToken from '@theme/CodeBlock/Line/Token';
+import type {Props} from '@theme/CodeBlock/Line';
+
+import styles from './styles.module.css';
+
+type Token = Props['line'][number];
+
+// Replaces '\n' by ''
+// Historical code, not sure why we even need this :/
+function fixLineBreak(line: Token[]) {
+ const singleLineBreakToken =
+ line.length === 1 && line[0]!.content === '\n' ? line[0] : undefined;
+
+ if (singleLineBreakToken) {
+ return [{...singleLineBreakToken, content: ''}];
+ }
+
+ return line;
+}
+
+export default function CodeBlockLine({
+ line: lineProp,
+ classNames,
+ showLineNumbers,
+ getLineProps,
+ getTokenProps,
+}: Props): ReactNode {
+ const line = fixLineBreak(lineProp);
+
+ const lineProps = getLineProps({
+ line,
+ className: clsx(classNames, showLineNumbers && styles.codeLine),
+ });
+
+ const lineTokens = line.map((token, key) => {
+ const tokenProps = getTokenProps({token});
+ return (
+
+ {tokenProps.children}
+
+ );
+ });
+
+ return (
+
+ {showLineNumbers ? (
+ <>
+
+ {lineTokens}
+ >
+ ) : (
+ lineTokens
+ )}
+
+
+ );
+}
diff --git a/src/theme/CodeBlock/Line/styles.module.css b/src/theme/CodeBlock/Line/styles.module.css
new file mode 100644
index 0000000..7c28ed9
--- /dev/null
+++ b/src/theme/CodeBlock/Line/styles.module.css
@@ -0,0 +1,45 @@
+/* Intentionally has zero specificity, so that to be able to override
+the background in custom CSS file due bug https://github.com/facebook/docusaurus/issues/3678 */
+:where(:root) {
+ --docusaurus-highlighted-code-line-bg: rgb(72 77 91);
+}
+
+:where([data-theme='dark']) {
+ --docusaurus-highlighted-code-line-bg: rgb(100 100 100);
+}
+
+:global(.theme-code-block-highlighted-line) {
+ background-color: var(--docusaurus-highlighted-code-line-bg);
+ display: block;
+ margin: 0 calc(-1 * var(--ifm-pre-padding));
+ padding: 0 var(--ifm-pre-padding);
+}
+
+.codeLine {
+ display: table-row;
+ counter-increment: line-count;
+}
+
+.codeLineNumber {
+ display: table-cell;
+ text-align: right;
+ width: 1%;
+ position: sticky;
+ left: 0;
+ padding: 0 var(--ifm-pre-padding);
+ background: var(--ifm-pre-background);
+ overflow-wrap: normal;
+}
+
+.codeLineNumber::before {
+ content: counter(line-count);
+ opacity: 0.4;
+}
+
+:global(.theme-code-block-highlighted-line) .codeLineNumber::before {
+ opacity: 0.8;
+}
+
+.codeLineContent {
+ padding-right: var(--ifm-pre-padding);
+}
diff --git a/src/theme/CodeBlock/Title/index.tsx b/src/theme/CodeBlock/Title/index.tsx
new file mode 100644
index 0000000..5e1bbe3
--- /dev/null
+++ b/src/theme/CodeBlock/Title/index.tsx
@@ -0,0 +1,8 @@
+import type {ReactNode} from 'react';
+
+import type {Props} from '@theme/CodeBlock/Title';
+
+// Just a pass-through component that users can swizzle and customize
+export default function CodeBlockTitle({children}: Props): ReactNode {
+ return children;
+}
diff --git a/src/theme/CodeBlock/context/command.tsx b/src/theme/CodeBlock/context/command.tsx
new file mode 100644
index 0000000..f2aa7eb
--- /dev/null
+++ b/src/theme/CodeBlock/context/command.tsx
@@ -0,0 +1,37 @@
+import { createContext, PropsWithChildren, useContext, useMemo } from "react";
+
+const CodeBlockCommandContext = createContext({
+ hasCommand: false,
+ withCommand: false,
+});
+
+export function useCommandContext() {
+ const ctx = useContext(CodeBlockCommandContext);
+
+ return ctx;
+}
+
+type CodeBlockCommandProviderProps = {
+ // Whether the code block contains command(s)
+ hasCommand?: boolean;
+ // Whether the code block is displayed alongside another code block containing command(s)
+ withCommand?: boolean;
+};
+
+export function CodeBlockCommandProvider(
+ props: PropsWithChildren
+) {
+ const value = useMemo(
+ () => ({
+ hasCommand: props.hasCommand,
+ withCommand: props.withCommand,
+ }),
+ [props.hasCommand, props.withCommand]
+ );
+
+ return (
+
+ {props.children}
+
+ );
+}
diff --git a/src/theme/CodeBlock/index.tsx b/src/theme/CodeBlock/index.tsx
new file mode 100644
index 0000000..17da028
--- /dev/null
+++ b/src/theme/CodeBlock/index.tsx
@@ -0,0 +1,89 @@
+import useIsBrowser from "@docusaurus/useIsBrowser";
+import type { Props } from "@theme/CodeBlock";
+import ElementContent from "@theme/CodeBlock/Content/Element";
+import StringContent from "@theme/CodeBlock/Content/String";
+import React, { isValidElement, type ReactNode } from "react";
+import { CodeBlockCommandProvider } from "./context/command";
+
+/**
+ * Best attempt to make the children a plain string so it is copyable. If there
+ * are react elements, we will not be able to copy the content, and it will
+ * return `children` as-is; otherwise, it concatenates the string children
+ * together.
+ */
+function maybeStringifyChildren(children: ReactNode): ReactNode {
+ if (React.Children.toArray(children).some((el) => isValidElement(el))) {
+ return children;
+ }
+ // The children is now guaranteed to be one/more plain strings
+ return Array.isArray(children) ? children.join("") : (children as string);
+}
+
+/**
+ * Checks to see if the metastring contains contains a bash command like so
+ * `command="npm install"`
+ * If it does, it will return the command, otherwise it returns null
+ */
+function getCommand(metastring: Props["metastring"]): null | string {
+ const match = /command=["'](.*)["']/.exec(metastring ?? "");
+ return match?.[1] ?? null;
+}
+
+/**
+ * Transforms sequences into actual new lines for multi-line commands
+ */
+function formatCommand(command: string): string {
+ // For now we only support bash commands
+ return command.replace(//g, "\\\n");
+}
+
+export default function CodeBlock({
+ children: rawChildren,
+ hasCommand = false,
+ ...props
+}: Props & { hasCommand?: boolean }): ReactNode {
+ // The Prism theme on SSR is always the default theme but the site theme can
+ // be in a different mode. React hydration doesn't update DOM styles that come
+ // from SSR. Hence force a re-render after mounting to apply the current
+ // relevant styles.
+ const isBrowser = useIsBrowser();
+ const children = maybeStringifyChildren(rawChildren);
+
+ const CodeBlockComp =
+ typeof children === "string" ? StringContent : ElementContent;
+
+ const command = getCommand(props.metastring);
+
+ let metastring = props.metastring;
+
+ let formattedCommand = "";
+ if (command) {
+ // Remove the command from the metastring so that it doesn't appear in the
+ metastring = metastring.replace(`command="${command}"`, "").trim();
+
+ formattedCommand = formatCommand(command);
+
+ console.log(command, "=>", formattedCommand);
+ }
+
+ return (
+ <>
+ {command && (
+
+ )}
+
+
+ {children as string}
+
+
+ >
+ );
+}
diff --git a/src/theme/prism-include-languages.ts b/src/theme/prism-include-languages.ts
index 825fee4..59601ff 100644
--- a/src/theme/prism-include-languages.ts
+++ b/src/theme/prism-include-languages.ts
@@ -3,7 +3,7 @@ import type * as PrismNamespace from "prismjs";
import type { Optional } from "utility-types";
export default function prismIncludeLanguages(
- PrismObject: typeof PrismNamespace,
+ PrismObject: typeof PrismNamespace
): void {
const {
themeConfig: { prism },
@@ -35,16 +35,42 @@ export default function prismIncludeLanguages(
if ("pattern" in PrismObject.languages.bash.function) {
PrismObject.languages.bash.function.pattern =
- /(^|[\s;|&]|[<>]\()(?:add|apropos|apt|apt-cache|apt-get|aptitude|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cargo|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|docker|docker-compose|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|java|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|node|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|podman|podman-compose|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|sysctl|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vcpkg|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper|lttle)(?=$|[)\s;|&])/;
+ /(^|[\s;|&]|[<>]\()(?:add|apropos|apt|apt-cache|apt-get|aptitude|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bun|bunx|bzip2|cal|cargo|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|docker|docker-compose|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|java|jobs|join|jq|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|node|nohup|notify-send|npm|npx|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|podman|podman-compose|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|sysctl|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vcpkg|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper|lttle)(?=$|[)\s;|&])/;
PrismObject.languages.bash.keyword =
/(^|[\s;|&]|[<>]\()(?:case|do|done|elif|else|esac|fi|for|function|if|in|select|then|until|while|deploy|login|machine|service|svc|namespace|ns|volume|certificate|cert)(?=$|[)\s;|&])/;
} else {
console.error(
- "Could not add our custom functions & keywords to prismjs bash component",
+ "Could not add our custom functions & keywords to prismjs bash component"
);
}
+ // IMPORTANT: Prism.js does not support .env files natively, so we are adding it here manually
+ PrismObject.languages.env = {
+ comment: {
+ pattern: /(^|\s)#.*/m,
+ greedy: true,
+ },
+ key: {
+ pattern: /^[A-Za-z_][A-Za-z0-9_]*(?=\s*=)/m,
+ greedy: true,
+ alias: "attr-name",
+ },
+ value: {
+ pattern: /=.*/m,
+ greedy: true,
+ inside: {
+ punctuation: /^=/,
+ string: {
+ pattern: /(["']).*?\1/,
+ greedy: true,
+ },
+ boolean: /\b(?:true|false)\b/,
+ number: /\b\d+(?:\.\d+)?\b/,
+ },
+ },
+ };
+
// Clean up and eventually restore former globalThis.Prism object (if any)
delete (globalThis as Optional).Prism;
if (typeof PrismBefore !== "undefined") {