diff --git a/docusaurus.config.js b/docusaurus.config.js
index ee4e113b6a2..39135d06150 100755
--- a/docusaurus.config.js
+++ b/docusaurus.config.js
@@ -1,5 +1,6 @@
import path from 'path';
import remarkNpm2Yarn from '@docusaurus/remark-plugin-npm2yarn';
+import rehypeCodeblockMeta from './src/plugins/rehype-codeblock-meta.mjs';
export default {
title: 'React Navigation',
@@ -146,6 +147,9 @@ export default {
includeCurrentVersion: false,
lastVersion: '6.x',
remarkPlugins: [[remarkNpm2Yarn, { sync: true }]],
+ rehypePlugins: [
+ [rehypeCodeblockMeta, { match: { snack: true } }],
+ ],
},
blog: {
remarkPlugins: [[remarkNpm2Yarn, { sync: true }]],
diff --git a/src/components/Pre.js b/src/components/Pre.js
new file mode 100644
index 00000000000..8c39db7f679
--- /dev/null
+++ b/src/components/Pre.js
@@ -0,0 +1,219 @@
+import { useColorMode } from '@docusaurus/theme-common';
+import MDXPre from '@theme-original/MDXComponents/Pre';
+import CodeBlock from '@theme-original/CodeBlock';
+import React from 'react';
+
+const peers = {
+ 'react-native-safe-area-context': '*',
+ 'react-native-screens': '*',
+};
+
+const versions = {
+ 7: {
+ '@react-navigation/bottom-tabs': ['7.0.0-alpha.7', peers],
+ '@react-navigation/core': '7.0.0-alpha.6',
+ '@react-navigation/native': '7.0.0-alpha.6',
+ '@react-navigation/drawer': [
+ '7.0.0-alpha.7',
+ {
+ ...peers,
+ 'react-native-reanimated': '*',
+ },
+ ],
+ '@react-navigation/elements': ['2.0.0-alpha.4', peers],
+ '@react-navigation/material-top-tabs': [
+ '7.0.0-alpha.6',
+ {
+ ...peers,
+ 'react-native-pager-view': '*',
+ },
+ ],
+ '@react-navigation/native-stack': ['7.0.0-alpha.7', peers],
+ '@react-navigation/routers': '7.0.0-alpha.4',
+ '@react-navigation/stack': [
+ '7.0.0-alpha.7',
+ {
+ ...peers,
+ 'react-native-gesture-handler': '*',
+ },
+ ],
+ 'react-native-drawer-layout': [
+ '4.0.0-alpha.3',
+ {
+ 'react-native-gesture-handler': '*',
+ 'react-native-reanimated': '*',
+ },
+ ],
+ 'react-native-tab-view': [
+ '4.0.0-alpha.2',
+ {
+ 'react-native-pager-view': '*',
+ },
+ ],
+ },
+};
+
+export default function Pre({
+ children,
+ 'data-name': name,
+ 'data-snack': snack,
+ 'data-version': version,
+ 'data-dependencies': deps,
+ ...rest
+}) {
+ const { colorMode } = useColorMode();
+
+ if (snack) {
+ const code = React.Children.only(children).props.children;
+
+ if (typeof code !== 'string') {
+ throw new Error(
+ 'Playground code must be a string, but received ' + typeof code
+ );
+ }
+
+ const dependencies = deps
+ ? Object.fromEntries(deps.split(',').map((entry) => entry.split('@')))
+ : {};
+
+ Object.assign(
+ dependencies,
+ Object.entries(versions[version]).reduce((acc, [key, value]) => {
+ if (code.includes(`from '${key}'`)) {
+ if (Array.isArray(value)) {
+ const [version, peers] = value;
+
+ Object.assign(acc, {
+ [key]: version,
+ ...peers,
+ });
+ } else {
+ acc[key] = value;
+ }
+ }
+
+ return acc;
+ }, {})
+ );
+
+ // FIXME: use staging for now since react-navigation fails to build on prod
+ const url = new URL('https://staging-snack.expo.dev');
+
+ if (name) {
+ url.searchParams.set('name', name);
+ }
+
+ url.searchParams.set(
+ 'code',
+ // Remove highlight and codeblock focus comments from code
+ code
+ .split('\n')
+ .filter((line) =>
+ [
+ '// highlight-start',
+ '// highlight-end',
+ '// highlight-next-line',
+ '// codeblock-focus-start',
+ '// codeblock-focus-end',
+ ].every((comment) => line.trim() !== comment)
+ )
+ .join('\n')
+ );
+
+ url.searchParams.set(
+ 'dependencies',
+ Object.entries(dependencies)
+ .map(([key, value]) => `${key}@${value}`)
+ .join(',')
+ );
+
+ url.searchParams.set('platform', 'web');
+ url.searchParams.set('supportedPlatforms', 'ios,android,web');
+ url.searchParams.set('preview', 'true');
+ url.searchParams.set('hideQueryParams', 'true');
+
+ if (snack === 'embed') {
+ url.searchParams.set('theme', colorMode === 'dark' ? 'dark' : 'light');
+ url.pathname = 'embedded';
+
+ return (
+
+ );
+ }
+
+ // Only keep the lines between `// codeblock-focus-{start,end} comments
+ if (code.includes('// codeblock-focus-start')) {
+ const lines = code.split('\n');
+
+ let content = '';
+ let focus = false;
+ let indent;
+
+ for (const line of lines) {
+ if (line.trim() === '// codeblock-focus-start') {
+ focus = true;
+ } else if (line.trim() === '// codeblock-focus-end') {
+ focus = false;
+ } else if (focus) {
+ if (indent === undefined) {
+ indent = line.match(/^\s*/)[0];
+ }
+
+ if (line.startsWith(indent)) {
+ content += line.slice(indent.length) + '\n';
+ } else {
+ content += line + '\n';
+ }
+ }
+ }
+
+ children = React.Children.map(children, (child) =>
+ React.cloneElement(child, { children: content })
+ );
+ }
+
+ return (
+ <>
+ {children}
+
+ Try this example on Snack{' '}
+
+
+ >
+ );
+ }
+
+ return {children};
+}
diff --git a/src/plugins/rehype-codeblock-meta.mjs b/src/plugins/rehype-codeblock-meta.mjs
new file mode 100644
index 00000000000..1dac09d65c2
--- /dev/null
+++ b/src/plugins/rehype-codeblock-meta.mjs
@@ -0,0 +1,67 @@
+import { visit } from 'unist-util-visit';
+
+/**
+ * Plugin to process codeblock meta
+ *
+ * @param {{ match: { [key: string]: string }, element: JSX.ElementType }} options
+ */
+export default function rehypeCodeblockMeta(options) {
+ if (!options?.match) {
+ throw new Error('rehype-codeblock-meta: `match` option is required');
+ }
+
+ return (tree) => {
+ visit(tree, 'element', (node) => {
+ if (
+ node.tagName === 'pre' &&
+ node.children?.length === 1 &&
+ node.children[0].tagName === 'code'
+ ) {
+ const codeblock = node.children[0];
+ const meta = codeblock.data?.meta;
+
+ if (meta) {
+ let segments = [];
+
+ // Walk through meta string and split it into segments based on space unless it's inside quotes
+ for (let i = 0; i < meta.length; i++) {
+ let segment = '';
+ let quote = false;
+
+ for (; i < meta.length; i++) {
+ if (meta[i] === '"') {
+ quote = !quote;
+ } else if (meta[i] === ' ' && !quote) {
+ break;
+ }
+
+ segment += meta[i];
+ }
+
+ segments.push(segment);
+ }
+
+ const attributes = segments.reduce((acc, attribute) => {
+ const [key, value = 'true'] = attribute.split('=');
+
+ return Object.assign(acc, {
+ [`data-${key}`]: value.replace(/^"(.+(?="$))"$/, '$1'),
+ });
+ }, {});
+
+ if (
+ Object.entries(options.match).some(([key, value]) => {
+ if (value === true) {
+ return attributes[`data-${key}`];
+ } else {
+ return attributes[`data-${key}`] === value;
+ }
+ })
+ ) {
+ Object.assign(node.properties, attributes);
+ }
+ }
+ }
+ });
+ };
+}
diff --git a/src/theme/MDXComponents.js b/src/theme/MDXComponents.js
new file mode 100644
index 00000000000..97cc95545ff
--- /dev/null
+++ b/src/theme/MDXComponents.js
@@ -0,0 +1,7 @@
+import MDXComponents from '@theme-original/MDXComponents';
+import Pre from '../components/Pre';
+
+export default {
+ ...MDXComponents,
+ pre: Pre,
+};