+
{children}
)
diff --git a/packages/nextra/src/components/copy-to-clipboard.tsx b/packages/nextra/src/components/copy-to-clipboard.tsx
index db711b2d4b..8a206eb110 100644
--- a/packages/nextra/src/components/copy-to-clipboard.tsx
+++ b/packages/nextra/src/components/copy-to-clipboard.tsx
@@ -1,19 +1,18 @@
import React, {
ComponentProps,
ReactElement,
- ReactNode,
useCallback,
useEffect,
useState
} from 'react'
-import { onlyText } from 'react-children-utilities'
import { CheckIcon, CopyIcon } from '../icons'
+import { Button } from './button'
export const CopyToClipboard = ({
value,
className
}: {
- value: ReactNode
+ value: string
className?: string
}): ReactElement => {
const [isCopied, setCopied] = useState(false)
@@ -37,7 +36,7 @@ export const CopyToClipboard = ({
console.error('Access to clipboard rejected!')
}
try {
- await navigator.clipboard.writeText(onlyText(value))
+ await navigator.clipboard.writeText(JSON.parse(value))
} catch {
console.error('Failed to copy!')
}
@@ -46,16 +45,8 @@ export const CopyToClipboard = ({
const IconToUse = isCopied ? CheckIcon : CopyIcon
return (
-
)
}
diff --git a/packages/nextra/src/components/index.ts b/packages/nextra/src/components/index.ts
index 665fac60ba..ca41f57c6f 100644
--- a/packages/nextra/src/components/index.ts
+++ b/packages/nextra/src/components/index.ts
@@ -1,3 +1,4 @@
+export { Button } from './button'
export { CopyToClipboard } from './copy-to-clipboard'
export { Code } from './code'
export { Pre } from './pre'
diff --git a/packages/nextra/src/components/pre.tsx b/packages/nextra/src/components/pre.tsx
index e9fce9047c..614a5b93e4 100644
--- a/packages/nextra/src/components/pre.tsx
+++ b/packages/nextra/src/components/pre.tsx
@@ -1,16 +1,27 @@
-import React, { ComponentProps, ReactElement } from 'react'
+import React, { ComponentProps, ReactElement, useCallback } from 'react'
import { CopyToClipboard } from './copy-to-clipboard'
+import { Button } from './button'
+import { WordWrapIcon } from '../icons'
export const Pre = ({
children,
- className,
+ className = '',
+ value,
+ filename,
...props
}: ComponentProps<'pre'> & {
- 'data-filename'?: string
- 'data-nextra-copy'?: ''
+ filename?: string
+ value?: string
}): ReactElement => {
- const hasCopy = 'data-nextra-copy' in props
- const filename = props['data-filename']
+ const toggleWordWrap = useCallback(() => {
+ const htmlDataset = document.documentElement.dataset
+ const hasWordWrap = 'nextraWordWrap' in htmlDataset
+ if (hasWordWrap) {
+ delete htmlDataset.nextraWordWrap
+ } else {
+ htmlDataset.nextraWordWrap = ''
+ }
+ }, [])
return (
<>
@@ -23,18 +34,26 @@ export const Pre = ({
className={[
'bg-primary-700/5 mt-6 mb-4 overflow-x-auto rounded-xl font-medium subpixel-antialiased dark:bg-primary-300/10',
filename ? 'pt-12 pb-4' : 'py-4',
- className || ''
+ className
].join(' ')}
{...props}
>
{children}
- {hasCopy && (
-
- )}
+ &]:opacity-100',
+ 'flex gap-1 absolute m-2 right-0',
+ filename ? 'top-8' : 'top-0'
+ ].join(' ')}
+ >
+
+
+
+ {value && (
+
+ )}
+
>
)
}
diff --git a/packages/nextra/src/hooks/index.ts b/packages/nextra/src/hooks/index.ts
new file mode 100644
index 0000000000..a240426303
--- /dev/null
+++ b/packages/nextra/src/hooks/index.ts
@@ -0,0 +1 @@
+export * from './use-mounted'
diff --git a/packages/nextra-theme-docs/src/utils/use-mounted.ts b/packages/nextra/src/hooks/use-mounted.ts
similarity index 80%
rename from packages/nextra-theme-docs/src/utils/use-mounted.ts
rename to packages/nextra/src/hooks/use-mounted.ts
index aaa640a0e3..c2d8d44601 100644
--- a/packages/nextra-theme-docs/src/utils/use-mounted.ts
+++ b/packages/nextra/src/hooks/use-mounted.ts
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
-export function useMounted() {
+export function useMounted(): boolean {
const [mounted, setMounted] = useState(false)
useEffect(() => {
diff --git a/packages/nextra/src/icons/index.ts b/packages/nextra/src/icons/index.ts
index 82f06a0fc7..085af04294 100644
--- a/packages/nextra/src/icons/index.ts
+++ b/packages/nextra/src/icons/index.ts
@@ -8,4 +8,5 @@ export * from './menu'
export * from './moon'
export * from './spinner'
export * from './sun'
+export * from './word-wrap'
export * from './x'
diff --git a/packages/nextra/src/icons/word-wrap.tsx b/packages/nextra/src/icons/word-wrap.tsx
new file mode 100644
index 0000000000..c2bcb26389
--- /dev/null
+++ b/packages/nextra/src/icons/word-wrap.tsx
@@ -0,0 +1,12 @@
+import React, { ComponentProps, ReactElement } from 'react'
+
+export const WordWrapIcon = (props: ComponentProps<'svg'>): ReactElement => {
+ return (
+
+ )
+}
diff --git a/packages/nextra/src/mdx-plugins/rehype-handler.js b/packages/nextra/src/mdx-plugins/rehype-handler.js
index 97e7305c2d..61379379ce 100644
--- a/packages/nextra/src/mdx-plugins/rehype-handler.js
+++ b/packages/nextra/src/mdx-plugins/rehype-handler.js
@@ -9,58 +9,45 @@ function visit(node, tagNames, handler) {
node.children?.forEach(n => visit(n, tagNames, handler))
}
-export const parseMeta =
+export const parseMeta = () => tree => {
+ visit(tree, ['pre'], node => {
+ const [codeEl] = node.children
+ // Add default language `text` for code-blocks without languages
+ codeEl.properties.className ||= ['language-text']
+ node.__nextra_meta__ = codeEl.data?.meta
+ node.__nextra_text__ = codeEl.children[0].value
+ })
+}
+
+export const attachMeta =
({ defaultShowCopyCode }) =>
tree => {
- visit(tree, ['pre'], node => {
- if (
- // Block code
- Array.isArray(node.children) &&
- node.children.length === 1 &&
- node.children[0].tagName === 'code' &&
- typeof node.children[0].properties === 'object'
- ) {
- const meta =
- node.children[0].data?.meta ?? node.children[0].properties.metastring
+ const slugger = new Slugger()
- const hasCopy = meta
- ? (defaultShowCopyCode && !/( |^)copy=false($| )/.test(meta)) ||
- /( |^)copy($| )/.test(meta)
- : defaultShowCopyCode
- if (hasCopy) {
- node.__nextra_copy__ = true
- }
+ visit(tree, ['div', 'h2', 'h3', 'h4', 'h5', 'h6'], node => {
+ if (node.tagName !== 'div') {
+ // Attach slug
+ node.properties.id ||= slugger.slug(getFlattenedValue(node))
+ return
+ }
+ if (!('data-rehype-pretty-code-fragment' in node.properties)) {
+ return
+ }
+ const meta = node.__nextra_meta__
+ const hasCopy = meta
+ ? (defaultShowCopyCode && !/( |^)copy=false($| )/.test(meta)) ||
+ /( |^)copy($| )/.test(meta)
+ : defaultShowCopyCode
- if (!meta) {
- return
- }
- const filename = meta.match(/filename="([^"]+)"/)?.[1]
- if (filename) {
- node.__nextra_filename__ = filename
- }
+ const [preEl] = node.children
+ if (hasCopy) {
+ preEl.properties.value = JSON.stringify(node.__nextra_text__)
+ }
+
+ // Attach filename
+ const filename = meta?.match(/filename="([^"]+)"/)?.[1]
+ if (filename) {
+ preEl.properties.filename = filename
}
})
}
-
-export const attachMeta = () => tree => {
- const slugger = new Slugger()
-
- visit(tree, ['div', 'h2', 'h3', 'h4', 'h5', 'h6'], node => {
- if (node.tagName !== 'div') {
- // Attach slug
- node.properties.id ||= slugger.slug(getFlattenedValue(node))
- return
- }
- if (!('data-rehype-pretty-code-fragment' in node.properties)) {
- return
- }
- const preElement = node.children[0]
- if ('__nextra_copy__' in node) {
- preElement.properties['data-nextra-copy'] = ''
- }
- // Attach filename
- if ('__nextra_filename__' in node) {
- preElement.properties['data-filename'] = node.__nextra_filename__
- }
- })
-}
diff --git a/packages/nextra/src/types.ts b/packages/nextra/src/types.ts
index 1c63d88ec9..e2e1f67484 100644
--- a/packages/nextra/src/types.ts
+++ b/packages/nextra/src/types.ts
@@ -53,7 +53,7 @@ export type PageMapItem = Folder | MdxFile | MetaJsonFile
// PageMapItem without MetaJsonFile and with its meta from _meta.json
export type Page = (MdxFile | Folder) & {
- meta: Exclude
+ meta?: Exclude
}
export type Heading = MDASTHeading & {
diff --git a/packages/nextra/styles/code-block.css b/packages/nextra/styles/code-block.css
index e86550ef64..63b932ce3d 100644
--- a/packages/nextra/styles/code-block.css
+++ b/packages/nextra/styles/code-block.css
@@ -1,30 +1,25 @@
code {
- @apply border-black/5 bg-black/5 break-words rounded-md border py-0.5 px-[0.25em] text-[0.9em];
- @apply dark:border-white/10 dark:bg-white/10;
box-decoration-break: clone;
font-feature-settings: 'rlig' 1, 'calt' 1, 'ss01' 1;
- &[data-line-numbers] {
- counter-reset: line;
-
- & > .line::before {
+ &[data-line-numbers] > .line {
+ @apply inline-flex pl-2;
+ &::before {
counter-increment: line;
content: counter(line);
- @apply mr-6 inline-block w-4 text-right text-gray-500;
+ @apply h-full float-left pr-4 text-right min-w-[2.6rem] text-gray-500;
}
}
- .line.highlighted {
- @apply border-primary-600/60 bg-primary-600/10;
- span {
- @apply relative;
+ .line {
+ &.highlighted {
+ @apply border-primary-600/60 bg-primary-600/10;
+ }
+ .highlighted {
+ @apply rounded-sm shadow-[0_0_0_3px_rgba(0,0,0,.3)];
+ @apply bg-primary-800/10 shadow-primary-800/10;
+ @apply dark:bg-primary-300/10 dark:shadow-primary-300/10;
}
- }
-
- .line .highlighted {
- @apply rounded-sm shadow-[0_0_0_3px_rgba(0,0,0,0.3)];
- @apply bg-primary-800/10 shadow-primary-800/10;
- @apply dark:bg-primary-300/10 dark:shadow-primary-300/10;
}
}
@@ -32,30 +27,29 @@ pre {
/* content-visibility: auto; */
contain: paint;
code {
- @apply grid min-w-full rounded-none border-none bg-transparent p-0 text-sm leading-5 text-current dark:bg-transparent;
- /* code blocks without languages don't have children that have px-4 */
- &:not([data-language]) {
- @apply px-4;
+ @apply grid min-w-full rounded-none border-none !bg-transparent !p-0 text-sm leading-5 text-current dark:!bg-transparent;
+ .line {
+ @apply border-l-2 border-transparent px-4;
}
+ }
+ html[data-nextra-word-wrap] & {
+ word-break: break-word;
+ @apply whitespace-pre-wrap md:whitespace-pre;
.line {
- @apply border-l-2 border-transparent px-4;
+ @apply inline-block;
}
}
}
[data-rehype-pretty-code-fragment] {
@apply relative;
-
- &:hover .nextra-copy-button {
- @apply opacity-100;
- }
}
@supports (
(-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))
) {
- .nextra-copy-button {
+ .nextra-button {
@apply backdrop-blur-md bg-opacity-[.85] dark:bg-opacity-80;
}
}
diff --git a/packages/nextra/tsup.config.ts b/packages/nextra/tsup.config.ts
index 9da5ec4022..fb39e239a6 100644
--- a/packages/nextra/tsup.config.ts
+++ b/packages/nextra/tsup.config.ts
@@ -14,6 +14,7 @@ export default defineConfig([
{
name: 'nextra-esm',
entry: [
+ 'src/hooks/index.ts',
'src/loader.ts',
'src/compile.ts',
'src/icons/index.ts',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 69e738697c..ad1fbe3bcf 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -138,7 +138,6 @@ importers:
gray-matter: ^4.0.3
next: ^12.2.4
react: ^18.2.0
- react-children-utilities: ^2.8.0
react-dom: ^18.2.0
rehype-mdx-title: ^1.0.0
rehype-pretty-code: 0.2.4
@@ -153,7 +152,6 @@ importers:
github-slugger: 1.4.0
graceful-fs: 4.2.10
gray-matter: 4.0.3
- react-children-utilities: 2.8.0_react@18.2.0
rehype-mdx-title: 1.0.0
rehype-pretty-code: 0.2.4_shiki@0.10.1
remark-gfm: 3.0.1
@@ -6578,7 +6576,7 @@ packages:
resolution: {directory: packages/nextra-theme-blog, type: directory}
id: file:packages/nextra-theme-blog
name: nextra-theme-blog
- version: 2.0.0-beta.20
+ version: 2.0.0-beta.21
peerDependencies:
next: '>=9.5.3'
react: '>=16.13.1'
@@ -6599,7 +6597,7 @@ packages:
resolution: {directory: packages/nextra-theme-docs, type: directory}
id: file:packages/nextra-theme-docs
name: nextra-theme-docs
- version: 2.0.0-beta.20
+ version: 2.0.0-beta.21
peerDependencies:
next: '>=9.5.3'
react: '>=16.13.1'
@@ -6628,7 +6626,7 @@ packages:
resolution: {directory: packages/nextra, type: directory}
id: file:packages/nextra
name: nextra
- version: 2.0.0-beta.20
+ version: 2.0.0-beta.21
peerDependencies:
next: '>=9.5.3'
react: '>=16.13.1'