mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-28 17:57:48 +02:00
Merge branch 'main' of https://github.com/brunomartinspro/docusaurus
This commit is contained in:
commit
f927499c42
125 changed files with 3529 additions and 1994 deletions
2
.eslintrc.js
vendored
2
.eslintrc.js
vendored
|
@ -298,7 +298,7 @@ module.exports = {
|
|||
'jest/expect-expect': OFF,
|
||||
'jest/no-large-snapshots': [
|
||||
WARNING,
|
||||
{maxSize: Infinity, inlineMaxSize: 10},
|
||||
{maxSize: Infinity, inlineMaxSize: 50},
|
||||
],
|
||||
'jest/no-test-return-statement': ERROR,
|
||||
'jest/prefer-expect-resolves': WARNING,
|
||||
|
|
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
|
@ -15,4 +15,4 @@ jobs:
|
|||
- name: Checkout
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # 4.5.0
|
||||
uses: actions/dependency-review-action@ce3cf9537a52e8119d91fd484ab5b8a807627bf8 # 4.6.0
|
||||
|
|
|
@ -9,9 +9,9 @@ import fs from 'fs-extra';
|
|||
import path from 'path';
|
||||
import {fileURLToPath} from 'url';
|
||||
import {program} from 'commander';
|
||||
import logger from '@docusaurus/logger';
|
||||
import {logger} from '@docusaurus/logger';
|
||||
import sharp from 'sharp';
|
||||
import imageSize from 'image-size';
|
||||
import {imageSizeFromFile} from 'image-size/fromFile';
|
||||
|
||||
// You can use it as:
|
||||
//
|
||||
|
@ -64,7 +64,7 @@ program
|
|||
|
||||
await Promise.all(
|
||||
images.map(async (imgPath) => {
|
||||
const {width, height} = imageSize(imgPath);
|
||||
const {width, height} = await imageSizeFromFile(imgPath);
|
||||
const targetWidth =
|
||||
options.width ?? (imgPath.includes(showcasePath) ? 640 : 1000);
|
||||
const targetHeight =
|
||||
|
|
|
@ -13,7 +13,8 @@
|
|||
"pr: polish": ":nail_care: Polish",
|
||||
"pr: documentation": ":memo: Documentation",
|
||||
"pr: dependencies": ":robot: Dependencies",
|
||||
"pr: maintenance": ":wrench: Maintenance"
|
||||
"pr: maintenance": ":wrench: Maintenance",
|
||||
"pr: translations": ":globe_with_meridians: Translations"
|
||||
},
|
||||
"cacheDir": ".changelog"
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
"lint:js": "eslint --cache --report-unused-disable-directives \"**/*.{js,jsx,ts,tsx,mjs}\"",
|
||||
"lint:js:fix": "yarn lint:js --fix",
|
||||
"lint:spelling": "cspell \"**\" --no-progress --show-context --show-suggestions",
|
||||
"lint:spelling:fix": "yarn rimraf project-words.txt && echo \"# Project Words - DO NOT TOUCH - This is updated through CI\" >> project-words.txt && yarn -s lint:spelling --words-only --unique --no-exit-code --no-summary \"**\" | cross-env LC_ALL=en_US.UTF-8 sort --ignore-case >> project-words.txt",
|
||||
"lint:spelling:fix": "yarn rimraf project-words.txt && echo \"# Project Words - DO NOT TOUCH - This is updated through CI\" >> project-words.txt && yarn -s lint:spelling --words-only --unique --no-exit-code --no-summary \"**\" | cross-env LC_ALL=C sort --ignore-case >> project-words.txt",
|
||||
"lint:style": "stylelint \"**/*.css\"",
|
||||
"lint:style:fix": "yarn lint:style --fix",
|
||||
"lerna": "lerna",
|
||||
|
@ -91,7 +91,7 @@
|
|||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"cspell": "^8.1.0",
|
||||
"cspell": "^8.18.1",
|
||||
"eslint": "^8.45.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
|
@ -104,7 +104,7 @@
|
|||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-regexp": "^1.15.0",
|
||||
"husky": "^8.0.3",
|
||||
"image-size": "^1.0.2",
|
||||
"image-size": "^2.0.2",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-serializer-ansi-escapes": "^3.0.0",
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@docusaurus/types": "3.7.0",
|
||||
"@rspack/core": "^1.2.5",
|
||||
"@rspack/core": "^1.3.3",
|
||||
"@swc/core": "^1.7.39",
|
||||
"@swc/html": "^1.7.39",
|
||||
"browserslist": "^4.24.2",
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
"estree-util-value-to-estree": "^3.0.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"image-size": "^1.0.2",
|
||||
"image-size": "^2.0.2",
|
||||
"mdast-util-mdx": "^3.0.0",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
|
|
|
@ -25,6 +25,7 @@ const plugin: Plugin<unknown[], Root> = function plugin(): Transformer<Root> {
|
|||
node.data.hProperties = node.data.hProperties || {};
|
||||
node.data.hProperties.metastring = node.meta;
|
||||
|
||||
// TODO Docusaurus v4: remove special case
|
||||
// Retrocompatible support for live codeblock metastring
|
||||
// Not really the appropriate place to handle that :s
|
||||
node.data.hProperties.live = node.meta?.split(' ').includes('live');
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import path from 'path';
|
||||
import url from 'url';
|
||||
import fs from 'fs-extra';
|
||||
import {promisify} from 'util';
|
||||
import {
|
||||
toMessageRelativeFilePath,
|
||||
posixPath,
|
||||
|
@ -17,7 +16,7 @@ import {
|
|||
getFileLoaderUtils,
|
||||
} from '@docusaurus/utils';
|
||||
import escapeHtml from 'escape-html';
|
||||
import sizeOf from 'image-size';
|
||||
import {imageSizeFromFile} from 'image-size/fromFile';
|
||||
import logger from '@docusaurus/logger';
|
||||
import {assetRequireAttributeValue, transformNode} from '../utils';
|
||||
import type {Plugin, Transformer} from 'unified';
|
||||
|
@ -80,7 +79,7 @@ async function toImageRequireNode(
|
|||
}
|
||||
|
||||
try {
|
||||
const size = (await promisify(sizeOf)(imagePath))!;
|
||||
const size = (await imageSizeFromFile(imagePath))!;
|
||||
if (size.width) {
|
||||
attributes.push({
|
||||
type: 'mdxJsxAttribute',
|
||||
|
|
|
@ -122,6 +122,15 @@ declare module '@theme/Root' {
|
|||
export default function Root({children}: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/ThemeProvider' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
export interface Props {
|
||||
readonly children: ReactNode;
|
||||
}
|
||||
export default function ThemeProvider({children}: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/SiteMetadata' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
<div class="blog-posts">
|
||||
<xsl:for-each select="atom:feed/atom:entry">
|
||||
<div class="blog-post">
|
||||
<h3><a href="{atom:link[@rel='alternate']/@href}"><xsl:value-of
|
||||
<h3><a href="{atom:link/@href}"><xsl:value-of
|
||||
select="atom:title"
|
||||
/></a></h3>
|
||||
<div class="blog-post-date">
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
<div class="blog-posts">
|
||||
<xsl:for-each select="atom:feed/atom:entry">
|
||||
<div class="blog-post">
|
||||
<h3><a href="{atom:link[@rel='alternate']/@href}"><xsl:value-of
|
||||
<h3><a href="{atom:link/@href}"><xsl:value-of
|
||||
select="atom:title"
|
||||
/></a></h3>
|
||||
<div class="blog-post-date">
|
||||
|
|
|
@ -23,7 +23,6 @@ describe('normalizeSocials', () => {
|
|||
mastodon: 'Mastodon',
|
||||
};
|
||||
|
||||
// eslint-disable-next-line jest/no-large-snapshots
|
||||
expect(normalizeSocials(socials)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"bluesky": "https://bsky.app/profile/gingergeek.co.uk",
|
||||
|
|
|
@ -42,19 +42,16 @@ describe('validateSidebars', () => {
|
|||
});
|
||||
|
||||
it('sidebar category wrong label', () => {
|
||||
expect(
|
||||
() =>
|
||||
validateSidebars({
|
||||
docs: [
|
||||
{
|
||||
type: 'category',
|
||||
label: true,
|
||||
items: [{type: 'doc', id: 'doc1'}],
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
// eslint-disable-next-line jest/no-large-snapshots
|
||||
expect(() =>
|
||||
validateSidebars({
|
||||
docs: [
|
||||
{
|
||||
type: 'category',
|
||||
label: true,
|
||||
items: [{type: 'doc', id: 'doc1'}],
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`
|
||||
"{
|
||||
"type": "category",
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
---
|
||||
title: MDX page
|
||||
description: my MDX page
|
||||
slug: /custom-mdx/slug
|
||||
---
|
||||
|
||||
MDX page
|
||||
|
|
|
@ -32,11 +32,12 @@ exports[`docusaurus-plugin-content-pages loads simple pages 1`] = `
|
|||
"frontMatter": {
|
||||
"custom_frontMatter": "added by parseFrontMatter",
|
||||
"description": "my MDX page",
|
||||
"slug": "/custom-mdx/slug",
|
||||
"title": "MDX page",
|
||||
},
|
||||
"lastUpdatedAt": undefined,
|
||||
"lastUpdatedBy": undefined,
|
||||
"permalink": "/hello/mdxPage",
|
||||
"permalink": "/custom-mdx/slug",
|
||||
"source": "@site/src/pages/hello/mdxPage.mdx",
|
||||
"title": "MDX page",
|
||||
"type": "mdx",
|
||||
|
@ -101,11 +102,12 @@ exports[`docusaurus-plugin-content-pages loads simple pages with french translat
|
|||
"frontMatter": {
|
||||
"custom_frontMatter": "added by parseFrontMatter",
|
||||
"description": "my MDX page",
|
||||
"slug": "/custom-mdx/slug",
|
||||
"title": "MDX page",
|
||||
},
|
||||
"lastUpdatedAt": undefined,
|
||||
"lastUpdatedBy": undefined,
|
||||
"permalink": "/fr/hello/mdxPage",
|
||||
"permalink": "/fr/custom-mdx/slug",
|
||||
"source": "@site/src/pages/hello/mdxPage.mdx",
|
||||
"title": "MDX page",
|
||||
"type": "mdx",
|
||||
|
@ -170,11 +172,12 @@ exports[`docusaurus-plugin-content-pages loads simple pages with last update 1`]
|
|||
"frontMatter": {
|
||||
"custom_frontMatter": "added by parseFrontMatter",
|
||||
"description": "my MDX page",
|
||||
"slug": "/custom-mdx/slug",
|
||||
"title": "MDX page",
|
||||
},
|
||||
"lastUpdatedAt": 1539502055000,
|
||||
"lastUpdatedBy": "Author",
|
||||
"permalink": "/hello/mdxPage",
|
||||
"permalink": "/custom-mdx/slug",
|
||||
"source": "@site/src/pages/hello/mdxPage.mdx",
|
||||
"title": "MDX page",
|
||||
"type": "mdx",
|
||||
|
|
|
@ -106,12 +106,13 @@ async function processPageSourceFile(
|
|||
|
||||
const source = path.join(contentPath, relativeSource);
|
||||
const aliasedSourcePath = aliasedSitePath(source, siteDir);
|
||||
const permalink = normalizeUrl([
|
||||
baseUrl,
|
||||
options.routeBasePath,
|
||||
encodePath(fileToPath(relativeSource)),
|
||||
]);
|
||||
|
||||
const filenameSlug = encodePath(fileToPath(relativeSource));
|
||||
|
||||
if (!isMarkdownSource(relativeSource)) {
|
||||
// For now, slug can't be customized for JSX pages
|
||||
const slug = filenameSlug;
|
||||
const permalink = normalizeUrl([baseUrl, options.routeBasePath, slug]);
|
||||
return {
|
||||
type: 'jsx',
|
||||
permalink,
|
||||
|
@ -131,6 +132,9 @@ async function processPageSourceFile(
|
|||
});
|
||||
const frontMatter = validatePageFrontMatter(unsafeFrontMatter);
|
||||
|
||||
const slug = frontMatter.slug ?? filenameSlug;
|
||||
const permalink = normalizeUrl([baseUrl, options.routeBasePath, slug]);
|
||||
|
||||
const pagesDirPath = await getFolderContainingFile(
|
||||
getContentPathList(contentPaths),
|
||||
relativeSource,
|
||||
|
|
|
@ -22,6 +22,7 @@ const PageFrontMatterSchema = Joi.object<PageFrontMatter>({
|
|||
description: Joi.string().allow(''),
|
||||
keywords: Joi.array().items(Joi.string().required()),
|
||||
image: URISchema,
|
||||
slug: Joi.string(),
|
||||
wrapperClassName: Joi.string(),
|
||||
hide_table_of_contents: Joi.boolean(),
|
||||
...FrontMatterTOCHeadingLevels,
|
||||
|
|
|
@ -37,6 +37,7 @@ declare module '@docusaurus/plugin-content-pages' {
|
|||
readonly title?: string;
|
||||
readonly description?: string;
|
||||
readonly image?: string;
|
||||
readonly slug?: string;
|
||||
readonly keywords?: string[];
|
||||
readonly wrapperClassName?: string;
|
||||
readonly hide_table_of_contents?: string;
|
||||
|
|
|
@ -159,7 +159,7 @@ export default function getSwizzleConfig(): SwizzleConfig {
|
|||
'CodeBlock/Content': {
|
||||
actions: {
|
||||
eject: 'unsafe',
|
||||
wrap: 'forbidden',
|
||||
wrap: 'unsafe',
|
||||
},
|
||||
description:
|
||||
'The folder containing components responsible for rendering different types of CodeBlock content.',
|
||||
|
|
|
@ -426,17 +426,76 @@ declare module '@theme/CodeInline' {
|
|||
export default function CodeInline(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/CodeBlock/CopyButton' {
|
||||
declare module '@theme/CodeBlock/Provider' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
export interface Props {
|
||||
readonly code: string;
|
||||
readonly children: ReactNode;
|
||||
}
|
||||
|
||||
export default function CodeBlockProvider(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/CodeBlock/Title' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
export interface Props {
|
||||
readonly children: ReactNode;
|
||||
}
|
||||
|
||||
export default function CodeBlockTitle(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/CodeBlock/Layout' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
export interface Props {
|
||||
readonly className?: string;
|
||||
}
|
||||
|
||||
export default function CodeBlockLayout(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/CodeBlock/Buttons' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
export interface Props {
|
||||
readonly className?: string;
|
||||
}
|
||||
|
||||
export default function CodeBlockButtons(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/CodeBlock/Buttons/Button' {
|
||||
import type {ComponentProps, ReactNode} from 'react';
|
||||
|
||||
export interface Props extends ComponentProps<'button'> {
|
||||
readonly className?: string;
|
||||
}
|
||||
|
||||
export default function CopyButton(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/CodeBlock/Buttons/CopyButton' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
export interface Props {
|
||||
readonly className?: string;
|
||||
}
|
||||
|
||||
export default function CodeBlockButtonCopy(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/CodeBlock/Buttons/WordWrapButton' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
export interface Props {
|
||||
readonly className?: string;
|
||||
}
|
||||
|
||||
export default function CodeBlockButtonWordWrap(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/CodeBlock/Container' {
|
||||
import type {ReactNode} from 'react';
|
||||
import type {ComponentProps} from 'react';
|
||||
|
@ -447,13 +506,23 @@ declare module '@theme/CodeBlock/Container' {
|
|||
}: {as: T} & ComponentProps<T>): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/CodeBlock/Content' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
export interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function CodeBlockContent(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/CodeBlock/Content/Element' {
|
||||
import type {ReactNode} from 'react';
|
||||
import type {Props} from '@theme/CodeBlock';
|
||||
|
||||
export type {Props};
|
||||
|
||||
export default function CodeBlockElementContent(props: Props): ReactNode;
|
||||
export default function CodeBlockContentElement(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/CodeBlock/Content/String' {
|
||||
|
@ -464,7 +533,7 @@ declare module '@theme/CodeBlock/Content/String' {
|
|||
readonly children: string;
|
||||
}
|
||||
|
||||
export default function CodeBlockStringContent(props: Props): ReactNode;
|
||||
export default function CodeBlockContentString(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/CodeBlock/Line' {
|
||||
|
@ -488,16 +557,16 @@ declare module '@theme/CodeBlock/Line' {
|
|||
export default function CodeBlockLine(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/CodeBlock/WordWrapButton' {
|
||||
declare module '@theme/CodeBlock/Line/Token' {
|
||||
import type {ReactNode} from 'react';
|
||||
import type {Token, TokenOutputProps} from 'prism-react-renderer';
|
||||
|
||||
export interface Props {
|
||||
readonly className?: string;
|
||||
readonly onClick: React.MouseEventHandler;
|
||||
readonly isEnabled: boolean;
|
||||
export interface Props extends TokenOutputProps {
|
||||
readonly token: Token;
|
||||
readonly line: Token[];
|
||||
}
|
||||
|
||||
export default function WordWrapButton(props: Props): ReactNode;
|
||||
export default function CodeBlockLine(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/DocCard' {
|
||||
|
@ -1181,19 +1250,40 @@ declare module '@theme/NavbarItem/DefaultNavbarItem' {
|
|||
import type {ReactNode} from 'react';
|
||||
import type {Props as NavbarNavLinkProps} from '@theme/NavbarItem/NavbarNavLink';
|
||||
|
||||
export type DesktopOrMobileNavBarItemProps = NavbarNavLinkProps & {
|
||||
export type DefaultNavbarItemProps = NavbarNavLinkProps & {
|
||||
readonly isDropdownItem?: boolean;
|
||||
readonly className?: string;
|
||||
readonly position?: 'left' | 'right';
|
||||
};
|
||||
|
||||
export interface Props extends DesktopOrMobileNavBarItemProps {
|
||||
// TODO Docusaurus v4, remove old type name
|
||||
export type DesktopOrMobileNavBarItemProps = DefaultNavbarItemProps;
|
||||
|
||||
export interface Props extends DefaultNavbarItemProps {
|
||||
readonly mobile?: boolean;
|
||||
}
|
||||
|
||||
export default function DefaultNavbarItem(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/NavbarItem/DefaultNavbarItem/Mobile' {
|
||||
import type {ReactNode} from 'react';
|
||||
import type {DefaultNavbarItemProps} from '@theme/NavbarItem/DefaultNavbarItem';
|
||||
|
||||
export interface Props extends DefaultNavbarItemProps {}
|
||||
|
||||
export default function DefaultNavbarItemMobile(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/NavbarItem/DefaultNavbarItem/Desktop' {
|
||||
import type {ReactNode} from 'react';
|
||||
import type {DefaultNavbarItemProps} from '@theme/NavbarItem/DefaultNavbarItem';
|
||||
|
||||
export interface Props extends DefaultNavbarItemProps {}
|
||||
|
||||
export default function DefaultNavbarItemDesktop(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/NavbarItem/NavbarNavLink' {
|
||||
import type {ReactNode} from 'react';
|
||||
import type {Props as LinkProps} from '@docusaurus/Link';
|
||||
|
@ -1216,19 +1306,40 @@ declare module '@theme/NavbarItem/DropdownNavbarItem' {
|
|||
import type {Props as NavbarNavLinkProps} from '@theme/NavbarItem/NavbarNavLink';
|
||||
import type {LinkLikeNavbarItemProps} from '@theme/NavbarItem';
|
||||
|
||||
export type DesktopOrMobileNavBarItemProps = NavbarNavLinkProps & {
|
||||
export type DropdownNavbarItemProps = NavbarNavLinkProps & {
|
||||
readonly position?: 'left' | 'right';
|
||||
readonly items: readonly LinkLikeNavbarItemProps[];
|
||||
readonly className?: string;
|
||||
};
|
||||
|
||||
export interface Props extends DesktopOrMobileNavBarItemProps {
|
||||
// TODO Docusaurus v4, remove old type name
|
||||
export type DesktopOrMobileNavBarItemProps = DropdownNavbarItemProps;
|
||||
|
||||
export interface Props extends DropdownNavbarItemProps {
|
||||
readonly mobile?: boolean;
|
||||
}
|
||||
|
||||
export default function DropdownNavbarItem(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/NavbarItem/DropdownNavbarItem/Mobile' {
|
||||
import type {ReactNode} from 'react';
|
||||
import type {DropdownNavbarItemProps} from '@theme/NavbarItem/DropdownNavbarItem';
|
||||
|
||||
export interface Props extends DropdownNavbarItemProps {}
|
||||
|
||||
export default function DropdownNavbarItemMobile(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/NavbarItem/DropdownNavbarItem/Desktop' {
|
||||
import type {ReactNode} from 'react';
|
||||
import type {DropdownNavbarItemProps} from '@theme/NavbarItem/DropdownNavbarItem';
|
||||
|
||||
export interface Props extends DropdownNavbarItemProps {}
|
||||
|
||||
export default function DropdownNavbarItemDesktop(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/NavbarItem/SearchNavbarItem' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
|
@ -1458,6 +1569,17 @@ declare module '@theme/ThemedImage' {
|
|||
export default function ThemedImage(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/ThemeProvider/TitleFormatter' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
export interface Props {
|
||||
readonly children: ReactNode;
|
||||
}
|
||||
export default function ThemeProviderTitleFormatter({
|
||||
children,
|
||||
}: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/Details' {
|
||||
import {Details, type DetailsProps} from '@docusaurus/theme-common/Details';
|
||||
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import type {Props} from '@theme/CodeBlock/Buttons/Button';
|
||||
|
||||
export default function CodeBlockButton({
|
||||
className,
|
||||
...props
|
||||
}: Props): ReactNode {
|
||||
return (
|
||||
<button type="button" {...props} className={clsx('clean-btn', className)} />
|
||||
);
|
||||
}
|
|
@ -15,16 +15,44 @@ import React, {
|
|||
import clsx from 'clsx';
|
||||
import copy from 'copy-text-to-clipboard';
|
||||
import {translate} from '@docusaurus/Translate';
|
||||
import type {Props} from '@theme/CodeBlock/CopyButton';
|
||||
import {useCodeBlockContext} from '@docusaurus/theme-common/internal';
|
||||
import Button from '@theme/CodeBlock/Buttons/Button';
|
||||
import type {Props} from '@theme/CodeBlock/Buttons/CopyButton';
|
||||
import IconCopy from '@theme/Icon/Copy';
|
||||
import IconSuccess from '@theme/Icon/Success';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default function CopyButton({code, className}: Props): ReactNode {
|
||||
function title() {
|
||||
return translate({
|
||||
id: 'theme.CodeBlock.copy',
|
||||
message: 'Copy',
|
||||
description: 'The copy button label on code blocks',
|
||||
});
|
||||
}
|
||||
|
||||
function ariaLabel(isCopied: boolean) {
|
||||
return isCopied
|
||||
? translate({
|
||||
id: 'theme.CodeBlock.copied',
|
||||
message: 'Copied',
|
||||
description: 'The copied button label on code blocks',
|
||||
})
|
||||
: translate({
|
||||
id: 'theme.CodeBlock.copyButtonAriaLabel',
|
||||
message: 'Copy code to clipboard',
|
||||
description: 'The ARIA label for copy code blocks button',
|
||||
});
|
||||
}
|
||||
|
||||
function useCopyButton() {
|
||||
const {
|
||||
metadata: {code},
|
||||
} = useCodeBlockContext();
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const copyTimeout = useRef<number | undefined>(undefined);
|
||||
const handleCopyCode = useCallback(() => {
|
||||
|
||||
const copyCode = useCallback(() => {
|
||||
copy(code);
|
||||
setIsCopied(true);
|
||||
copyTimeout.current = window.setTimeout(() => {
|
||||
|
@ -34,38 +62,26 @@ export default function CopyButton({code, className}: Props): ReactNode {
|
|||
|
||||
useEffect(() => () => window.clearTimeout(copyTimeout.current), []);
|
||||
|
||||
return {copyCode, isCopied};
|
||||
}
|
||||
|
||||
export default function CopyButton({className}: Props): ReactNode {
|
||||
const {copyCode, isCopied} = useCopyButton();
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={
|
||||
isCopied
|
||||
? translate({
|
||||
id: 'theme.CodeBlock.copied',
|
||||
message: 'Copied',
|
||||
description: 'The copied button label on code blocks',
|
||||
})
|
||||
: translate({
|
||||
id: 'theme.CodeBlock.copyButtonAriaLabel',
|
||||
message: 'Copy code to clipboard',
|
||||
description: 'The ARIA label for copy code blocks button',
|
||||
})
|
||||
}
|
||||
title={translate({
|
||||
id: 'theme.CodeBlock.copy',
|
||||
message: 'Copy',
|
||||
description: 'The copy button label on code blocks',
|
||||
})}
|
||||
<Button
|
||||
aria-label={ariaLabel(isCopied)}
|
||||
title={title()}
|
||||
className={clsx(
|
||||
'clean-btn',
|
||||
className,
|
||||
styles.copyButton,
|
||||
isCopied && styles.copyButtonCopied,
|
||||
)}
|
||||
onClick={handleCopyCode}>
|
||||
onClick={copyCode}>
|
||||
<span className={styles.copyButtonIcons} aria-hidden="true">
|
||||
<IconCopy className={styles.copyButtonIcon} />
|
||||
<IconSuccess className={styles.copyButtonSuccessIcon} />
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
);
|
||||
}
|
|
@ -8,16 +8,21 @@
|
|||
import React, {type ReactNode} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {translate} from '@docusaurus/Translate';
|
||||
import type {Props} from '@theme/CodeBlock/WordWrapButton';
|
||||
import {useCodeBlockContext} from '@docusaurus/theme-common/internal';
|
||||
import Button from '@theme/CodeBlock/Buttons/Button';
|
||||
import type {Props} from '@theme/CodeBlock/Buttons/WordWrapButton';
|
||||
import IconWordWrap from '@theme/Icon/WordWrap';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default function WordWrapButton({
|
||||
className,
|
||||
onClick,
|
||||
isEnabled,
|
||||
}: Props): ReactNode {
|
||||
export default function WordWrapButton({className}: Props): ReactNode {
|
||||
const {wordWrap} = useCodeBlockContext();
|
||||
|
||||
const canShowButton = wordWrap.isEnabled || wordWrap.isCodeScrollable;
|
||||
if (!canShowButton) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const title = translate({
|
||||
id: 'theme.CodeBlock.wordWrapToggle',
|
||||
message: 'Toggle word wrap',
|
||||
|
@ -26,17 +31,15 @@ export default function WordWrapButton({
|
|||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
<Button
|
||||
onClick={() => wordWrap.toggle()}
|
||||
className={clsx(
|
||||
'clean-btn',
|
||||
className,
|
||||
isEnabled && styles.wordWrapButtonEnabled,
|
||||
wordWrap.isEnabled && styles.wordWrapButtonEnabled,
|
||||
)}
|
||||
aria-label={title}
|
||||
title={title}>
|
||||
<IconWordWrap className={styles.wordWrapButtonIcon} aria-hidden="true" />
|
||||
</button>
|
||||
</Button>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
|
||||
import CopyButton from '@theme/CodeBlock/Buttons/CopyButton';
|
||||
import WordWrapButton from '@theme/CodeBlock/Buttons/WordWrapButton';
|
||||
import type {Props} from '@theme/CodeBlock/Buttons';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
// Code block buttons are not server-rendered on purpose
|
||||
// Adding them to the initial HTML is useless and expensive (due to JSX SVG)
|
||||
// They are hidden by default and require React to become interactive
|
||||
export default function CodeBlockButtons({className}: Props): ReactNode {
|
||||
return (
|
||||
<BrowserOnly>
|
||||
{() => (
|
||||
<div className={clsx(className, styles.buttonGroup)}>
|
||||
<WordWrapButton />
|
||||
<CopyButton />
|
||||
</div>
|
||||
)}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
.buttonGroup {
|
||||
display: flex;
|
||||
column-gap: 0.2rem;
|
||||
position: absolute;
|
||||
/* rtl:ignore */
|
||||
right: calc(var(--ifm-pre-padding) / 2);
|
||||
top: calc(var(--ifm-pre-padding) / 2);
|
||||
}
|
||||
|
||||
.buttonGroup button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--prism-background-color);
|
||||
color: var(--prism-color);
|
||||
border: 1px solid var(--ifm-color-emphasis-300);
|
||||
border-radius: var(--ifm-global-radius);
|
||||
padding: 0.4rem;
|
||||
line-height: 0;
|
||||
transition: opacity var(--ifm-transition-fast) ease-in-out;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.buttonGroup button:focus-visible,
|
||||
.buttonGroup button:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
:global(.theme-code-block:hover) .buttonGroup button {
|
||||
opacity: 0.4;
|
||||
}
|
|
@ -12,8 +12,10 @@ import type {Props} from '@theme/CodeBlock/Content/Element';
|
|||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
// <pre> tags in markdown map to CodeBlocks. They may contain JSX children. When
|
||||
// the children is not a simple string, we just return a styled block without
|
||||
// TODO Docusaurus v4: move this component at the root?
|
||||
// This component only handles a rare edge-case: <pre><MyComp/></pre> in MDX
|
||||
// <pre> 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 (
|
||||
|
|
|
@ -6,126 +6,37 @@
|
|||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {useThemeConfig, usePrismTheme} from '@docusaurus/theme-common';
|
||||
import {useThemeConfig} from '@docusaurus/theme-common';
|
||||
import {
|
||||
parseCodeBlockTitle,
|
||||
parseLanguage,
|
||||
parseLines,
|
||||
getLineNumbersStart,
|
||||
CodeBlockContextProvider,
|
||||
type CodeBlockMetadata,
|
||||
createCodeBlockMetadata,
|
||||
useCodeWordWrap,
|
||||
} from '@docusaurus/theme-common/internal';
|
||||
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||
import {Highlight, type Language} from 'prism-react-renderer';
|
||||
import Line from '@theme/CodeBlock/Line';
|
||||
import CopyButton from '@theme/CodeBlock/CopyButton';
|
||||
import WordWrapButton from '@theme/CodeBlock/WordWrapButton';
|
||||
import Container from '@theme/CodeBlock/Container';
|
||||
import type {Props} from '@theme/CodeBlock/Content/String';
|
||||
import CodeBlockLayout from '@theme/CodeBlock/Layout';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
// Prism languages are always lowercase
|
||||
// We want to fail-safe and allow both "php" and "PHP"
|
||||
// See https://github.com/facebook/docusaurus/issues/9012
|
||||
function normalizeLanguage(language: string | undefined): string | undefined {
|
||||
return language?.toLowerCase();
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
export default function CodeBlockString({
|
||||
children,
|
||||
className: blockClassName = '',
|
||||
metastring,
|
||||
title: titleProp,
|
||||
showLineNumbers: showLineNumbersProp,
|
||||
language: languageProp,
|
||||
}: Props): ReactNode {
|
||||
const {
|
||||
prism: {defaultLanguage, magicComments},
|
||||
} = useThemeConfig();
|
||||
const language = normalizeLanguage(
|
||||
languageProp ?? parseLanguage(blockClassName) ?? defaultLanguage,
|
||||
);
|
||||
|
||||
const prismTheme = usePrismTheme();
|
||||
// TODO Docusaurus v4: move this component at the root?
|
||||
export default function CodeBlockString(props: Props): ReactNode {
|
||||
const metadata = useCodeBlockMetadata(props);
|
||||
const wordWrap = useCodeWordWrap();
|
||||
const isBrowser = useIsBrowser();
|
||||
|
||||
// We still parse the metastring in case we want to support more syntax in the
|
||||
// future. Note that MDX doesn't strip quotes when parsing metastring:
|
||||
// "title=\"xyz\"" => title: "\"xyz\""
|
||||
const title = parseCodeBlockTitle(metastring) || titleProp;
|
||||
|
||||
const {lineClassNames, code} = parseLines(children, {
|
||||
metastring,
|
||||
language,
|
||||
magicComments,
|
||||
});
|
||||
const lineNumbersStart = getLineNumbersStart({
|
||||
showLineNumbers: showLineNumbersProp,
|
||||
metastring,
|
||||
});
|
||||
|
||||
return (
|
||||
<Container
|
||||
as="div"
|
||||
className={clsx(
|
||||
blockClassName,
|
||||
language &&
|
||||
!blockClassName.includes(`language-${language}`) &&
|
||||
`language-${language}`,
|
||||
)}>
|
||||
{title && <div className={styles.codeBlockTitle}>{title}</div>}
|
||||
<div className={styles.codeBlockContent}>
|
||||
<Highlight
|
||||
theme={prismTheme}
|
||||
code={code}
|
||||
language={(language ?? 'text') as Language}>
|
||||
{({className, style, tokens, getLineProps, getTokenProps}) => (
|
||||
<pre
|
||||
/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */
|
||||
tabIndex={0}
|
||||
ref={wordWrap.codeBlockRef}
|
||||
className={clsx(className, styles.codeBlock, 'thin-scrollbar')}
|
||||
style={style}>
|
||||
<code
|
||||
className={clsx(
|
||||
styles.codeBlockLines,
|
||||
lineNumbersStart !== undefined &&
|
||||
styles.codeBlockLinesWithNumbering,
|
||||
)}
|
||||
style={
|
||||
lineNumbersStart === undefined
|
||||
? undefined
|
||||
: {counterReset: `line-count ${lineNumbersStart - 1}`}
|
||||
}>
|
||||
{tokens.map((line, i) => (
|
||||
<Line
|
||||
key={i}
|
||||
line={line}
|
||||
getLineProps={getLineProps}
|
||||
getTokenProps={getTokenProps}
|
||||
classNames={lineClassNames[i]}
|
||||
showLineNumbers={lineNumbersStart !== undefined}
|
||||
/>
|
||||
))}
|
||||
</code>
|
||||
</pre>
|
||||
)}
|
||||
</Highlight>
|
||||
{isBrowser ? (
|
||||
<div className={styles.buttonGroup}>
|
||||
{(wordWrap.isEnabled || wordWrap.isCodeScrollable) && (
|
||||
<WordWrapButton
|
||||
className={styles.codeButton}
|
||||
onClick={() => wordWrap.toggle()}
|
||||
isEnabled={wordWrap.isEnabled}
|
||||
/>
|
||||
)}
|
||||
<CopyButton className={styles.codeButton} code={code} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Container>
|
||||
<CodeBlockContextProvider metadata={metadata} wordWrap={wordWrap}>
|
||||
<CodeBlockLayout />
|
||||
</CodeBlockContextProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
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<HTMLPreElement, ComponentProps<'pre'>>(
|
||||
(props, ref) => {
|
||||
return (
|
||||
<pre
|
||||
ref={ref}
|
||||
/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */
|
||||
tabIndex={0}
|
||||
{...props}
|
||||
className={clsx(props.className, styles.codeBlock, 'thin-scrollbar')}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function Code(props: ComponentProps<'code'>) {
|
||||
const {metadata} = useCodeBlockContext();
|
||||
return (
|
||||
<code
|
||||
{...props}
|
||||
className={clsx(
|
||||
props.className,
|
||||
styles.codeBlockLines,
|
||||
metadata.lineNumbersStart !== undefined &&
|
||||
styles.codeBlockLinesWithNumbering,
|
||||
)}
|
||||
style={{
|
||||
...props.style,
|
||||
counterReset:
|
||||
metadata.lineNumbersStart === undefined
|
||||
? undefined
|
||||
: `line-count ${metadata.lineNumbersStart - 1}`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CodeBlockContent({
|
||||
className: classNameProp,
|
||||
}: Props): ReactNode {
|
||||
const {metadata, wordWrap} = useCodeBlockContext();
|
||||
const prismTheme = usePrismTheme();
|
||||
const {code, language, lineNumbersStart, lineClassNames} = metadata;
|
||||
return (
|
||||
<Highlight theme={prismTheme} code={code} language={language}>
|
||||
{({className, style, tokens: lines, getLineProps, getTokenProps}) => (
|
||||
<Pre
|
||||
ref={wordWrap.codeBlockRef}
|
||||
className={clsx(classNameProp, className)}
|
||||
style={style}>
|
||||
<Code>
|
||||
{lines.map((line, i) => (
|
||||
<Line
|
||||
key={i}
|
||||
line={line}
|
||||
getLineProps={getLineProps}
|
||||
getTokenProps={getTokenProps}
|
||||
classNames={lineClassNames[i]}
|
||||
showLineNumbers={lineNumbersStart !== undefined}
|
||||
/>
|
||||
))}
|
||||
</Code>
|
||||
</Pre>
|
||||
)}
|
||||
</Highlight>
|
||||
);
|
||||
}
|
|
@ -5,33 +5,12 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.codeBlock {
|
||||
--ifm-pre-background: var(--prism-background-color);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.codeBlockTitle + .codeBlockContent .codeBlock {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.codeBlockStandalone {
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -54,34 +33,3 @@
|
|||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.buttonGroup {
|
||||
display: flex;
|
||||
column-gap: 0.2rem;
|
||||
position: absolute;
|
||||
/* rtl:ignore */
|
||||
right: calc(var(--ifm-pre-padding) / 2);
|
||||
top: calc(var(--ifm-pre-padding) / 2);
|
||||
}
|
||||
|
||||
.buttonGroup button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--prism-background-color);
|
||||
color: var(--prism-color);
|
||||
border: 1px solid var(--ifm-color-emphasis-300);
|
||||
border-radius: var(--ifm-global-radius);
|
||||
padding: 0.4rem;
|
||||
line-height: 0;
|
||||
transition: opacity var(--ifm-transition-fast) ease-in-out;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.buttonGroup button:focus-visible,
|
||||
.buttonGroup button:hover {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
:global(.theme-code-block:hover) .buttonGroup button {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {useCodeBlockContext} from '@docusaurus/theme-common/internal';
|
||||
import Container from '@theme/CodeBlock/Container';
|
||||
import Title from '@theme/CodeBlock/Title';
|
||||
import Content from '@theme/CodeBlock/Content';
|
||||
import type {Props} from '@theme/CodeBlock/Layout';
|
||||
import Buttons from '@theme/CodeBlock/Buttons';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default function CodeBlockLayout({className}: Props): ReactNode {
|
||||
const {metadata} = useCodeBlockContext();
|
||||
return (
|
||||
<Container as="div" className={clsx(className, metadata.className)}>
|
||||
{metadata.title && (
|
||||
<div className={styles.codeBlockTitle}>
|
||||
<Title>{metadata.title}</Title>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.codeBlockContent}>
|
||||
<Content />
|
||||
<Buttons />
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
.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;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
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 <span {...props} />;
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
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';
|
||||
|
@ -40,9 +41,14 @@ export default function CodeBlockLine({
|
|||
className: clsx(classNames, showLineNumbers && styles.codeLine),
|
||||
});
|
||||
|
||||
const lineTokens = line.map((token, key) => (
|
||||
<span key={key} {...getTokenProps({token})} />
|
||||
));
|
||||
const lineTokens = line.map((token, key) => {
|
||||
const tokenProps = getTokenProps({token});
|
||||
return (
|
||||
<LineToken key={key} {...tokenProps} line={line} token={token}>
|
||||
{tokenProps.children}
|
||||
</LineToken>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<span {...lineProps}>
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import NavbarNavLink from '@theme/NavbarItem/NavbarNavLink';
|
||||
import type {
|
||||
DesktopOrMobileNavBarItemProps,
|
||||
Props,
|
||||
} from '@theme/NavbarItem/DefaultNavbarItem';
|
||||
|
||||
function DefaultNavbarItemDesktop({
|
||||
className,
|
||||
isDropdownItem = false,
|
||||
...props
|
||||
}: DesktopOrMobileNavBarItemProps) {
|
||||
const element = (
|
||||
<NavbarNavLink
|
||||
className={clsx(
|
||||
isDropdownItem ? 'dropdown__link' : 'navbar__item navbar__link',
|
||||
className,
|
||||
)}
|
||||
isDropdownLink={isDropdownItem}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
if (isDropdownItem) {
|
||||
return <li>{element}</li>;
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
function DefaultNavbarItemMobile({
|
||||
className,
|
||||
isDropdownItem,
|
||||
...props
|
||||
}: DesktopOrMobileNavBarItemProps) {
|
||||
return (
|
||||
<li className="menu__list-item">
|
||||
<NavbarNavLink className={clsx('menu__link', className)} {...props} />
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DefaultNavbarItem({
|
||||
mobile = false,
|
||||
position, // Need to destructure position from props so that it doesn't get passed on.
|
||||
...props
|
||||
}: Props): ReactNode {
|
||||
const Comp = mobile ? DefaultNavbarItemMobile : DefaultNavbarItemDesktop;
|
||||
return (
|
||||
<Comp
|
||||
{...props}
|
||||
activeClassName={
|
||||
props.activeClassName ??
|
||||
(mobile ? 'menu__link--active' : 'navbar__link--active')
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import NavbarNavLink from '@theme/NavbarItem/NavbarNavLink';
|
||||
import type {Props} from '@theme/NavbarItem/DefaultNavbarItem/Desktop';
|
||||
|
||||
export default function DefaultNavbarItemDesktop({
|
||||
className,
|
||||
isDropdownItem = false,
|
||||
...props
|
||||
}: Props): ReactNode {
|
||||
const element = (
|
||||
<NavbarNavLink
|
||||
className={clsx(
|
||||
isDropdownItem ? 'dropdown__link' : 'navbar__item navbar__link',
|
||||
className,
|
||||
)}
|
||||
isDropdownLink={isDropdownItem}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
if (isDropdownItem) {
|
||||
return <li>{element}</li>;
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import NavbarNavLink from '@theme/NavbarItem/NavbarNavLink';
|
||||
import type {Props} from '@theme/NavbarItem/DefaultNavbarItem/Mobile';
|
||||
|
||||
export default function DefaultNavbarItemMobile({
|
||||
className,
|
||||
isDropdownItem,
|
||||
...props
|
||||
}: Props): ReactNode {
|
||||
return (
|
||||
<li className="menu__list-item">
|
||||
<NavbarNavLink className={clsx('menu__link', className)} {...props} />
|
||||
</li>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import DefaultNavbarItemMobile from '@theme/NavbarItem/DefaultNavbarItem/Mobile';
|
||||
import DefaultNavbarItemDesktop from '@theme/NavbarItem/DefaultNavbarItem/Desktop';
|
||||
import type {Props} from '@theme/NavbarItem/DefaultNavbarItem';
|
||||
|
||||
export default function DefaultNavbarItem({
|
||||
mobile = false,
|
||||
position, // Need to destructure position from props so that it doesn't get passed on.
|
||||
...props
|
||||
}: Props): ReactNode {
|
||||
const Comp = mobile ? DefaultNavbarItemMobile : DefaultNavbarItemDesktop;
|
||||
return (
|
||||
<Comp
|
||||
{...props}
|
||||
activeClassName={
|
||||
props.activeClassName ??
|
||||
(mobile ? 'menu__link--active' : 'navbar__link--active')
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {useState, useRef, useEffect, type ReactNode} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import NavbarNavLink from '@theme/NavbarItem/NavbarNavLink';
|
||||
import NavbarItem from '@theme/NavbarItem';
|
||||
import type {Props} from '@theme/NavbarItem/DropdownNavbarItem/Desktop';
|
||||
|
||||
export default function DropdownNavbarItemDesktop({
|
||||
items,
|
||||
position,
|
||||
className,
|
||||
onClick,
|
||||
...props
|
||||
}: Props): ReactNode {
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (
|
||||
event: MouseEvent | TouchEvent | FocusEvent,
|
||||
) => {
|
||||
if (
|
||||
!dropdownRef.current ||
|
||||
dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setShowDropdown(false);
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
document.addEventListener('touchstart', handleClickOutside);
|
||||
document.addEventListener('focusin', handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
document.removeEventListener('touchstart', handleClickOutside);
|
||||
document.removeEventListener('focusin', handleClickOutside);
|
||||
};
|
||||
}, [dropdownRef]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className={clsx('navbar__item', 'dropdown', 'dropdown--hoverable', {
|
||||
'dropdown--right': position === 'right',
|
||||
'dropdown--show': showDropdown,
|
||||
})}>
|
||||
<NavbarNavLink
|
||||
aria-haspopup="true"
|
||||
aria-expanded={showDropdown}
|
||||
role="button"
|
||||
// # hash permits to make the <a> tag focusable in case no link target
|
||||
// See https://github.com/facebook/docusaurus/pull/6003
|
||||
// There's probably a better solution though...
|
||||
href={props.to ? undefined : '#'}
|
||||
className={clsx('navbar__link', className)}
|
||||
{...props}
|
||||
onClick={props.to ? undefined : (e) => e.preventDefault()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
setShowDropdown(!showDropdown);
|
||||
}
|
||||
}}>
|
||||
{props.children ?? props.label}
|
||||
</NavbarNavLink>
|
||||
<ul className="dropdown__menu">
|
||||
{items.map((childItemProps, i) => (
|
||||
<NavbarItem
|
||||
isDropdownItem
|
||||
activeClassName="dropdown__link--active"
|
||||
{...childItemProps}
|
||||
key={i}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {useEffect, type ReactNode, type ComponentProps} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
isRegexpStringMatch,
|
||||
useCollapsible,
|
||||
Collapsible,
|
||||
} from '@docusaurus/theme-common';
|
||||
import {isSamePath, useLocalPathname} from '@docusaurus/theme-common/internal';
|
||||
import {translate} from '@docusaurus/Translate';
|
||||
import NavbarNavLink from '@theme/NavbarItem/NavbarNavLink';
|
||||
import NavbarItem, {type LinkLikeNavbarItemProps} from '@theme/NavbarItem';
|
||||
import type {Props} from '@theme/NavbarItem/DropdownNavbarItem/Mobile';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
function isItemActive(
|
||||
item: LinkLikeNavbarItemProps,
|
||||
localPathname: string,
|
||||
): boolean {
|
||||
if (isSamePath(item.to, localPathname)) {
|
||||
return true;
|
||||
}
|
||||
if (isRegexpStringMatch(item.activeBaseRegex, localPathname)) {
|
||||
return true;
|
||||
}
|
||||
if (item.activeBasePath && localPathname.startsWith(item.activeBasePath)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function containsActiveItems(
|
||||
items: readonly LinkLikeNavbarItemProps[],
|
||||
localPathname: string,
|
||||
): boolean {
|
||||
return items.some((item) => isItemActive(item, localPathname));
|
||||
}
|
||||
|
||||
function CollapseButton({
|
||||
collapsed,
|
||||
onClick,
|
||||
}: {
|
||||
collapsed: boolean;
|
||||
onClick: ComponentProps<'button'>['onClick'];
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
aria-label={
|
||||
collapsed
|
||||
? translate({
|
||||
id: 'theme.navbar.mobileDropdown.collapseButton.expandAriaLabel',
|
||||
message: 'Expand the dropdown',
|
||||
description:
|
||||
'The ARIA label of the button to expand the mobile dropdown navbar item',
|
||||
})
|
||||
: translate({
|
||||
id: 'theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel',
|
||||
message: 'Collapse the dropdown',
|
||||
description:
|
||||
'The ARIA label of the button to collapse the mobile dropdown navbar item',
|
||||
})
|
||||
}
|
||||
aria-expanded={!collapsed}
|
||||
type="button"
|
||||
className="clean-btn menu__caret"
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function useItemCollapsible({active}: {active: boolean}) {
|
||||
const {collapsed, toggleCollapsed, setCollapsed} = useCollapsible({
|
||||
initialState: () => !active,
|
||||
});
|
||||
|
||||
// Expand if any item active after a navigation
|
||||
useEffect(() => {
|
||||
if (active) {
|
||||
setCollapsed(false);
|
||||
}
|
||||
}, [active, setCollapsed]);
|
||||
|
||||
return {
|
||||
collapsed,
|
||||
toggleCollapsed,
|
||||
};
|
||||
}
|
||||
|
||||
export default function DropdownNavbarItemMobile({
|
||||
items,
|
||||
className,
|
||||
position, // Need to destructure position from props so that it doesn't get passed on.
|
||||
onClick,
|
||||
...props
|
||||
}: Props): ReactNode {
|
||||
const localPathname = useLocalPathname();
|
||||
const isActive = isSamePath(props.to, localPathname);
|
||||
const containsActive = containsActiveItems(items, localPathname);
|
||||
|
||||
const {collapsed, toggleCollapsed} = useItemCollapsible({
|
||||
active: isActive || containsActive,
|
||||
});
|
||||
|
||||
// # hash permits to make the <a> tag focusable in case no link target
|
||||
// See https://github.com/facebook/docusaurus/pull/6003
|
||||
// There's probably a better solution though...
|
||||
const href = props.to ? undefined : '#';
|
||||
|
||||
return (
|
||||
<li
|
||||
className={clsx('menu__list-item', {
|
||||
'menu__list-item--collapsed': collapsed,
|
||||
})}>
|
||||
<div
|
||||
className={clsx('menu__list-item-collapsible', {
|
||||
'menu__list-item-collapsible--active': isActive,
|
||||
})}>
|
||||
<NavbarNavLink
|
||||
role="button"
|
||||
className={clsx(
|
||||
styles.dropdownNavbarItemMobile,
|
||||
'menu__link menu__link--sublist',
|
||||
className,
|
||||
)}
|
||||
href={href}
|
||||
{...props}
|
||||
onClick={(e) => {
|
||||
// Prevent navigation when link is "#"
|
||||
if (href === '#') {
|
||||
e.preventDefault();
|
||||
}
|
||||
// Otherwise we let navigation eventually happen, and/or collapse
|
||||
toggleCollapsed();
|
||||
}}>
|
||||
{props.children ?? props.label}
|
||||
</NavbarNavLink>
|
||||
<CollapseButton
|
||||
collapsed={collapsed}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
toggleCollapsed();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Collapsible lazy as="ul" className="menu__list" collapsed={collapsed}>
|
||||
{items.map((childItemProps, i) => (
|
||||
<NavbarItem
|
||||
mobile
|
||||
isDropdownItem
|
||||
onClick={onClick}
|
||||
activeClassName="menu__link--active"
|
||||
{...childItemProps}
|
||||
key={i}
|
||||
/>
|
||||
))}
|
||||
</Collapsible>
|
||||
</li>
|
||||
);
|
||||
}
|
|
@ -5,178 +5,10 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {useState, useRef, useEffect, type ReactNode} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
isRegexpStringMatch,
|
||||
useCollapsible,
|
||||
Collapsible,
|
||||
} from '@docusaurus/theme-common';
|
||||
import {isSamePath, useLocalPathname} from '@docusaurus/theme-common/internal';
|
||||
import NavbarNavLink from '@theme/NavbarItem/NavbarNavLink';
|
||||
import NavbarItem, {type LinkLikeNavbarItemProps} from '@theme/NavbarItem';
|
||||
import type {
|
||||
DesktopOrMobileNavBarItemProps,
|
||||
Props,
|
||||
} from '@theme/NavbarItem/DropdownNavbarItem';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
function isItemActive(
|
||||
item: LinkLikeNavbarItemProps,
|
||||
localPathname: string,
|
||||
): boolean {
|
||||
if (isSamePath(item.to, localPathname)) {
|
||||
return true;
|
||||
}
|
||||
if (isRegexpStringMatch(item.activeBaseRegex, localPathname)) {
|
||||
return true;
|
||||
}
|
||||
if (item.activeBasePath && localPathname.startsWith(item.activeBasePath)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function containsActiveItems(
|
||||
items: readonly LinkLikeNavbarItemProps[],
|
||||
localPathname: string,
|
||||
): boolean {
|
||||
return items.some((item) => isItemActive(item, localPathname));
|
||||
}
|
||||
|
||||
function DropdownNavbarItemDesktop({
|
||||
items,
|
||||
position,
|
||||
className,
|
||||
onClick,
|
||||
...props
|
||||
}: DesktopOrMobileNavBarItemProps) {
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (
|
||||
event: MouseEvent | TouchEvent | FocusEvent,
|
||||
) => {
|
||||
if (
|
||||
!dropdownRef.current ||
|
||||
dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setShowDropdown(false);
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
document.addEventListener('touchstart', handleClickOutside);
|
||||
document.addEventListener('focusin', handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
document.removeEventListener('touchstart', handleClickOutside);
|
||||
document.removeEventListener('focusin', handleClickOutside);
|
||||
};
|
||||
}, [dropdownRef]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className={clsx('navbar__item', 'dropdown', 'dropdown--hoverable', {
|
||||
'dropdown--right': position === 'right',
|
||||
'dropdown--show': showDropdown,
|
||||
})}>
|
||||
<NavbarNavLink
|
||||
aria-haspopup="true"
|
||||
aria-expanded={showDropdown}
|
||||
role="button"
|
||||
// # hash permits to make the <a> tag focusable in case no link target
|
||||
// See https://github.com/facebook/docusaurus/pull/6003
|
||||
// There's probably a better solution though...
|
||||
href={props.to ? undefined : '#'}
|
||||
className={clsx('navbar__link', className)}
|
||||
{...props}
|
||||
onClick={props.to ? undefined : (e) => e.preventDefault()}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
setShowDropdown(!showDropdown);
|
||||
}
|
||||
}}>
|
||||
{props.children ?? props.label}
|
||||
</NavbarNavLink>
|
||||
<ul className="dropdown__menu">
|
||||
{items.map((childItemProps, i) => (
|
||||
<NavbarItem
|
||||
isDropdownItem
|
||||
activeClassName="dropdown__link--active"
|
||||
{...childItemProps}
|
||||
key={i}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownNavbarItemMobile({
|
||||
items,
|
||||
className,
|
||||
position, // Need to destructure position from props so that it doesn't get passed on.
|
||||
onClick,
|
||||
...props
|
||||
}: DesktopOrMobileNavBarItemProps) {
|
||||
const localPathname = useLocalPathname();
|
||||
const containsActive = containsActiveItems(items, localPathname);
|
||||
|
||||
const {collapsed, toggleCollapsed, setCollapsed} = useCollapsible({
|
||||
initialState: () => !containsActive,
|
||||
});
|
||||
|
||||
// Expand/collapse if any item active after a navigation
|
||||
useEffect(() => {
|
||||
if (containsActive) {
|
||||
setCollapsed(!containsActive);
|
||||
}
|
||||
}, [localPathname, containsActive, setCollapsed]);
|
||||
|
||||
return (
|
||||
<li
|
||||
className={clsx('menu__list-item', {
|
||||
'menu__list-item--collapsed': collapsed,
|
||||
})}>
|
||||
<NavbarNavLink
|
||||
role="button"
|
||||
className={clsx(
|
||||
styles.dropdownNavbarItemMobile,
|
||||
'menu__link menu__link--sublist menu__link--sublist-caret',
|
||||
className,
|
||||
)}
|
||||
// # hash permits to make the <a> tag focusable in case no link target
|
||||
// See https://github.com/facebook/docusaurus/pull/6003
|
||||
// There's probably a better solution though...
|
||||
href={props.to ? undefined : '#'}
|
||||
{...props}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
toggleCollapsed();
|
||||
}}>
|
||||
{props.children ?? props.label}
|
||||
</NavbarNavLink>
|
||||
<Collapsible lazy as="ul" className="menu__list" collapsed={collapsed}>
|
||||
{items.map((childItemProps, i) => (
|
||||
<NavbarItem
|
||||
mobile
|
||||
isDropdownItem
|
||||
onClick={onClick}
|
||||
activeClassName="menu__link--active"
|
||||
{...childItemProps}
|
||||
key={i}
|
||||
/>
|
||||
))}
|
||||
</Collapsible>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
import React, {type ReactNode} from 'react';
|
||||
import DropdownNavbarItemMobile from '@theme/NavbarItem/DropdownNavbarItem/Mobile';
|
||||
import DropdownNavbarItemDesktop from '@theme/NavbarItem/DropdownNavbarItem/Desktop';
|
||||
import type {Props} from '@theme/NavbarItem/DropdownNavbarItem';
|
||||
|
||||
export default function DropdownNavbarItem({
|
||||
mobile = false,
|
||||
|
|
|
@ -20,6 +20,7 @@ export default function Tag({
|
|||
}: Props): ReactNode {
|
||||
return (
|
||||
<Link
|
||||
rel="tag"
|
||||
href={permalink}
|
||||
title={description}
|
||||
className={clsx(
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ComponentProps, type ReactNode} from 'react';
|
||||
import {TitleFormatterProvider} from '@docusaurus/theme-common/internal';
|
||||
import type {Props} from '@theme/ThemeProvider/TitleFormatter';
|
||||
|
||||
type FormatterProp = ComponentProps<typeof TitleFormatterProvider>['formatter'];
|
||||
|
||||
const formatter: FormatterProp = (params) => {
|
||||
// Add your own title formatting logic here!
|
||||
return params.defaultFormatter(params);
|
||||
};
|
||||
|
||||
export default function ThemeProviderTitleFormatter({
|
||||
children,
|
||||
}: Props): ReactNode {
|
||||
return (
|
||||
<TitleFormatterProvider formatter={formatter}>
|
||||
{children}
|
||||
</TitleFormatterProvider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import TitleFormatterProvider from '@theme/ThemeProvider/TitleFormatter';
|
||||
import type {Props} from '@theme/ThemeProvider';
|
||||
|
||||
export default function ThemeProvider({children}: Props): ReactNode {
|
||||
return <TitleFormatterProvider>{children}</TitleFormatterProvider>;
|
||||
}
|
|
@ -52,12 +52,14 @@ function useTabBecameVisibleCallback(
|
|||
);
|
||||
}
|
||||
|
||||
export function useCodeWordWrap(): {
|
||||
export type WordWrap = {
|
||||
readonly codeBlockRef: RefObject<HTMLPreElement>;
|
||||
readonly isEnabled: boolean;
|
||||
readonly isCodeScrollable: boolean;
|
||||
readonly toggle: () => void;
|
||||
} {
|
||||
};
|
||||
|
||||
export function useCodeWordWrap(): WordWrap {
|
||||
const [isEnabled, setIsEnabled] = useState(false);
|
||||
const [isCodeScrollable, setIsCodeScrollable] = useState<boolean>(false);
|
||||
const codeBlockRef = useRef<HTMLPreElement>(null);
|
||||
|
|
|
@ -34,15 +34,19 @@ export {ColorModeProvider} from './contexts/colorMode';
|
|||
export {useAlternatePageUtils} from './utils/useAlternatePageUtils';
|
||||
|
||||
export {
|
||||
parseCodeBlockTitle,
|
||||
parseLanguage,
|
||||
parseLines,
|
||||
getLineNumbersStart,
|
||||
type CodeBlockMetadata,
|
||||
createCodeBlockMetadata,
|
||||
getPrismCssVariables,
|
||||
CodeBlockContextProvider,
|
||||
useCodeBlockContext,
|
||||
} from './utils/codeBlockUtils';
|
||||
|
||||
export {DEFAULT_SEARCH_TAG} from './utils/searchUtils';
|
||||
|
||||
export {useTitleFormatter} from './utils/generalUtils';
|
||||
export {
|
||||
TitleFormatterProvider,
|
||||
useTitleFormatter,
|
||||
} from './utils/titleFormatterUtils';
|
||||
|
||||
export {useLocationChange} from './utils/useLocationChange';
|
||||
|
||||
|
@ -88,7 +92,6 @@ export {
|
|||
} from './hooks/useKeyboardNavigation';
|
||||
export {useLockBodyScroll} from './hooks/useLockBodyScroll';
|
||||
export {useCodeWordWrap} from './hooks/useCodeWordWrap';
|
||||
export {getPrismCssVariables} from './utils/codeBlockUtils';
|
||||
export {useBackToTopButton} from './hooks/useBackToTopButton';
|
||||
|
||||
export {
|
||||
|
|
|
@ -1,333 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`getLineNumbersStart handles metadata combined with other options set as flag 1`] = `1`;
|
||||
|
||||
exports[`getLineNumbersStart handles metadata combined with other options set with number 1`] = `10`;
|
||||
|
||||
exports[`getLineNumbersStart handles metadata standalone set as flag 1`] = `1`;
|
||||
|
||||
exports[`getLineNumbersStart handles metadata standalone set with number 1`] = `10`;
|
||||
|
||||
exports[`getLineNumbersStart handles prop combined with metastring set to false 1`] = `undefined`;
|
||||
|
||||
exports[`getLineNumbersStart handles prop combined with metastring set to number 1`] = `10`;
|
||||
|
||||
exports[`getLineNumbersStart handles prop combined with metastring set to true 1`] = `1`;
|
||||
|
||||
exports[`getLineNumbersStart handles prop standalone set to false 1`] = `undefined`;
|
||||
|
||||
exports[`getLineNumbersStart handles prop standalone set to number 1`] = `10`;
|
||||
|
||||
exports[`getLineNumbersStart handles prop standalone set to true 1`] = `1`;
|
||||
|
||||
exports[`getLineNumbersStart with nothing set 1`] = `undefined`;
|
||||
|
||||
exports[`getLineNumbersStart with nothing set 2`] = `undefined`;
|
||||
|
||||
exports[`parseLines does not parse content with metastring 1`] = `
|
||||
{
|
||||
"code": "aaaaa
|
||||
nnnnn",
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseLines does not parse content with metastring 2`] = `
|
||||
{
|
||||
"code": "// highlight-next-line
|
||||
aaaaa
|
||||
bbbbb",
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseLines does not parse content with metastring 3`] = `
|
||||
{
|
||||
"code": "aaaaa
|
||||
bbbbb",
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseLines does not parse content with no language 1`] = `
|
||||
{
|
||||
"code": "// highlight-next-line
|
||||
aaaaa
|
||||
bbbbb",
|
||||
"lineClassNames": {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseLines handles one line with multiple class names 1`] = `
|
||||
{
|
||||
"code": "
|
||||
highlighted and collapsed
|
||||
highlighted and collapsed
|
||||
highlighted and collapsed
|
||||
Only highlighted
|
||||
Only collapsed
|
||||
highlighted and collapsed
|
||||
highlighted and collapsed
|
||||
Only collapsed
|
||||
highlighted and collapsed",
|
||||
"lineClassNames": {
|
||||
"1": [
|
||||
"highlight",
|
||||
"collapse",
|
||||
],
|
||||
"2": [
|
||||
"highlight",
|
||||
"collapse",
|
||||
],
|
||||
"3": [
|
||||
"highlight",
|
||||
"collapse",
|
||||
],
|
||||
"4": [
|
||||
"highlight",
|
||||
],
|
||||
"5": [
|
||||
"collapse",
|
||||
],
|
||||
"6": [
|
||||
"highlight",
|
||||
"collapse",
|
||||
],
|
||||
"7": [
|
||||
"highlight",
|
||||
"collapse",
|
||||
],
|
||||
"8": [
|
||||
"collapse",
|
||||
],
|
||||
"9": [
|
||||
"highlight",
|
||||
"collapse",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseLines handles one line with multiple class names 2`] = `
|
||||
{
|
||||
"code": "line
|
||||
line",
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d",
|
||||
],
|
||||
"1": [
|
||||
"b",
|
||||
"d",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseLines parses multiple types of magic comments 1`] = `
|
||||
{
|
||||
"code": "
|
||||
highlighted
|
||||
collapsed
|
||||
collapsed
|
||||
collapsed",
|
||||
"lineClassNames": {
|
||||
"1": [
|
||||
"highlight",
|
||||
],
|
||||
"2": [
|
||||
"collapse",
|
||||
],
|
||||
"3": [
|
||||
"collapse",
|
||||
],
|
||||
"4": [
|
||||
"collapse",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseLines removes lines correctly 1`] = `
|
||||
{
|
||||
"code": "aaaaa
|
||||
bbbbb",
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseLines removes lines correctly 2`] = `
|
||||
{
|
||||
"code": "aaaaa
|
||||
bbbbb",
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseLines removes lines correctly 3`] = `
|
||||
{
|
||||
"code": "aaaaa
|
||||
bbbbbbb
|
||||
bbbbb",
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"1": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"2": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseLines respects language: html 1`] = `
|
||||
{
|
||||
"code": "aaaa
|
||||
{/* highlight-next-line */}
|
||||
bbbbb
|
||||
dddd",
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"3": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseLines respects language: js 1`] = `
|
||||
{
|
||||
"code": "# highlight-next-line
|
||||
aaaaa
|
||||
bbbbb",
|
||||
"lineClassNames": {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseLines respects language: jsx 1`] = `
|
||||
{
|
||||
"code": "aaaa
|
||||
bbbbb
|
||||
<!-- highlight-next-line -->
|
||||
dddd",
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"1": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseLines respects language: md 1`] = `
|
||||
{
|
||||
"code": "---
|
||||
aaa: boo
|
||||
---
|
||||
|
||||
aaaa
|
||||
|
||||
<div>
|
||||
foo
|
||||
</div>
|
||||
|
||||
bbbbb
|
||||
dddd
|
||||
|
||||
\`\`\`js
|
||||
// highlight-next-line
|
||||
console.log("preserved");
|
||||
\`\`\`",
|
||||
"lineClassNames": {
|
||||
"1": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"11": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"7": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseLines respects language: none 1`] = `
|
||||
{
|
||||
"code": "aaaa
|
||||
bbbbb
|
||||
ccccc
|
||||
dddd",
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"1": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"2": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"3": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseLines respects language: py 1`] = `
|
||||
{
|
||||
"code": "/* highlight-next-line */
|
||||
aaaaa
|
||||
bbbbb",
|
||||
"lineClassNames": {},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`parseLines respects language: py 2`] = `
|
||||
{
|
||||
"code": "// highlight-next-line
|
||||
aaaa
|
||||
/* highlight-next-line */
|
||||
bbbbb
|
||||
ccccc
|
||||
<!-- highlight-next-line -->
|
||||
dddd",
|
||||
"lineClassNames": {
|
||||
"4": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
|
@ -9,10 +9,19 @@ import {
|
|||
getLineNumbersStart,
|
||||
type MagicCommentConfig,
|
||||
parseCodeBlockTitle,
|
||||
parseLanguage,
|
||||
parseClassNameLanguage,
|
||||
parseLines,
|
||||
createCodeBlockMetadata,
|
||||
} from '../codeBlockUtils';
|
||||
|
||||
const defaultMagicComments: MagicCommentConfig[] = [
|
||||
{
|
||||
className: 'theme-code-block-highlighted-line',
|
||||
line: 'highlight-next-line',
|
||||
block: {start: 'highlight-start', end: 'highlight-end'},
|
||||
},
|
||||
];
|
||||
|
||||
describe('parseCodeBlockTitle', () => {
|
||||
it('parses double quote delimited title', () => {
|
||||
expect(parseCodeBlockTitle(`title="index.js"`)).toBe(`index.js`);
|
||||
|
@ -59,24 +68,16 @@ describe('parseCodeBlockTitle', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('parseLanguage', () => {
|
||||
describe('parseClassNameLanguage', () => {
|
||||
it('works', () => {
|
||||
expect(parseLanguage('language-foo xxx yyy')).toBe('foo');
|
||||
expect(parseLanguage('xxxxx language-foo yyy')).toBe('foo');
|
||||
expect(parseLanguage('xx-language-foo yyyy')).toBeUndefined();
|
||||
expect(parseLanguage('xxx yyy zzz')).toBeUndefined();
|
||||
expect(parseClassNameLanguage('language-foo xxx yyy')).toBe('foo');
|
||||
expect(parseClassNameLanguage('xxxxx language-foo yyy')).toBe('foo');
|
||||
expect(parseClassNameLanguage('xx-language-foo yyyy')).toBeUndefined();
|
||||
expect(parseClassNameLanguage('xxx yyy zzz')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseLines', () => {
|
||||
const defaultMagicComments: MagicCommentConfig[] = [
|
||||
{
|
||||
className: 'theme-code-block-highlighted-line',
|
||||
line: 'highlight-next-line',
|
||||
block: {start: 'highlight-start', end: 'highlight-end'},
|
||||
},
|
||||
];
|
||||
|
||||
it('does not parse content with metastring', () => {
|
||||
expect(
|
||||
parseLines('aaaaa\nnnnnn', {
|
||||
|
@ -84,7 +85,18 @@ describe('parseLines', () => {
|
|||
language: 'js',
|
||||
magicComments: defaultMagicComments,
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"code": "aaaaa
|
||||
nnnnn",
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
expect(
|
||||
parseLines(
|
||||
`// highlight-next-line
|
||||
|
@ -96,7 +108,19 @@ bbbbb`,
|
|||
magicComments: defaultMagicComments,
|
||||
},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"code": "// highlight-next-line
|
||||
aaaaa
|
||||
bbbbb",
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
expect(
|
||||
parseLines(
|
||||
`aaaaa
|
||||
|
@ -107,7 +131,18 @@ bbbbb`,
|
|||
magicComments: defaultMagicComments,
|
||||
},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"code": "aaaaa
|
||||
bbbbb",
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
expect(() =>
|
||||
parseLines(
|
||||
`aaaaa
|
||||
|
@ -122,6 +157,7 @@ bbbbb`,
|
|||
`"A highlight range has been given in code block's metastring (\`\`\` {1}), but no magic comment config is available. Docusaurus applies the first magic comment entry's className for metastring ranges."`,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not parse content with no language', () => {
|
||||
expect(
|
||||
parseLines(
|
||||
|
@ -134,8 +170,16 @@ bbbbb`,
|
|||
magicComments: defaultMagicComments,
|
||||
},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"code": "// highlight-next-line
|
||||
aaaaa
|
||||
bbbbb",
|
||||
"lineClassNames": {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('removes lines correctly', () => {
|
||||
expect(
|
||||
parseLines(
|
||||
|
@ -144,7 +188,18 @@ aaaaa
|
|||
bbbbb`,
|
||||
{metastring: '', language: 'js', magicComments: defaultMagicComments},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"code": "aaaaa
|
||||
bbbbb",
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
expect(
|
||||
parseLines(
|
||||
`// highlight-start
|
||||
|
@ -153,7 +208,18 @@ aaaaa
|
|||
bbbbb`,
|
||||
{metastring: '', language: 'js', magicComments: defaultMagicComments},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"code": "aaaaa
|
||||
bbbbb",
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
expect(
|
||||
parseLines(
|
||||
`// highlight-start
|
||||
|
@ -165,8 +231,27 @@ bbbbbbb
|
|||
bbbbb`,
|
||||
{metastring: '', language: 'js', magicComments: defaultMagicComments},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"code": "aaaaa
|
||||
bbbbbbb
|
||||
bbbbb",
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"1": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"2": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('respects language', () => {
|
||||
expect(
|
||||
parseLines(
|
||||
|
@ -175,7 +260,15 @@ aaaaa
|
|||
bbbbb`,
|
||||
{metastring: '', language: 'js', magicComments: defaultMagicComments},
|
||||
),
|
||||
).toMatchSnapshot('js');
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"code": "# highlight-next-line
|
||||
aaaaa
|
||||
bbbbb",
|
||||
"lineClassNames": {},
|
||||
}
|
||||
`);
|
||||
|
||||
expect(
|
||||
parseLines(
|
||||
`/* highlight-next-line */
|
||||
|
@ -183,7 +276,15 @@ aaaaa
|
|||
bbbbb`,
|
||||
{metastring: '', language: 'py', magicComments: defaultMagicComments},
|
||||
),
|
||||
).toMatchSnapshot('py');
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"code": "/* highlight-next-line */
|
||||
aaaaa
|
||||
bbbbb",
|
||||
"lineClassNames": {},
|
||||
}
|
||||
`);
|
||||
|
||||
expect(
|
||||
parseLines(
|
||||
`// highlight-next-line
|
||||
|
@ -196,7 +297,23 @@ ccccc
|
|||
dddd`,
|
||||
{metastring: '', language: 'py', magicComments: defaultMagicComments},
|
||||
),
|
||||
).toMatchSnapshot('py');
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"code": "// highlight-next-line
|
||||
aaaa
|
||||
/* highlight-next-line */
|
||||
bbbbb
|
||||
ccccc
|
||||
<!-- highlight-next-line -->
|
||||
dddd",
|
||||
"lineClassNames": {
|
||||
"4": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
expect(
|
||||
parseLines(
|
||||
`// highlight-next-line
|
||||
|
@ -209,7 +326,29 @@ ccccc
|
|||
dddd`,
|
||||
{metastring: '', language: '', magicComments: defaultMagicComments},
|
||||
),
|
||||
).toMatchSnapshot('none');
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"code": "aaaa
|
||||
bbbbb
|
||||
ccccc
|
||||
dddd",
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"1": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"2": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"3": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
expect(
|
||||
parseLines(
|
||||
`// highlight-next-line
|
||||
|
@ -220,7 +359,23 @@ bbbbb
|
|||
dddd`,
|
||||
{metastring: '', language: 'jsx', magicComments: defaultMagicComments},
|
||||
),
|
||||
).toMatchSnapshot('jsx');
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"code": "aaaa
|
||||
bbbbb
|
||||
<!-- highlight-next-line -->
|
||||
dddd",
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"1": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
expect(
|
||||
parseLines(
|
||||
`// highlight-next-line
|
||||
|
@ -231,7 +386,23 @@ bbbbb
|
|||
dddd`,
|
||||
{metastring: '', language: 'html', magicComments: defaultMagicComments},
|
||||
),
|
||||
).toMatchSnapshot('html');
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"code": "aaaa
|
||||
{/* highlight-next-line */}
|
||||
bbbbb
|
||||
dddd",
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"3": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
expect(
|
||||
parseLines(
|
||||
`---
|
||||
|
@ -257,7 +428,38 @@ console.log("preserved");
|
|||
`,
|
||||
{metastring: '', language: 'md', magicComments: defaultMagicComments},
|
||||
),
|
||||
).toMatchSnapshot('md');
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"code": "---
|
||||
aaa: boo
|
||||
---
|
||||
|
||||
aaaa
|
||||
|
||||
<div>
|
||||
foo
|
||||
</div>
|
||||
|
||||
bbbbb
|
||||
dddd
|
||||
|
||||
\`\`\`js
|
||||
// highlight-next-line
|
||||
console.log("preserved");
|
||||
\`\`\`",
|
||||
"lineClassNames": {
|
||||
"1": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"11": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
"7": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('parses multiple types of magic comments', () => {
|
||||
|
@ -290,7 +492,29 @@ collapsed
|
|||
],
|
||||
},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"code": "
|
||||
highlighted
|
||||
collapsed
|
||||
collapsed
|
||||
collapsed",
|
||||
"lineClassNames": {
|
||||
"1": [
|
||||
"highlight",
|
||||
],
|
||||
"2": [
|
||||
"collapse",
|
||||
],
|
||||
"3": [
|
||||
"collapse",
|
||||
],
|
||||
"4": [
|
||||
"collapse",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('handles one line with multiple class names', () => {
|
||||
|
@ -335,7 +559,56 @@ highlighted and collapsed
|
|||
],
|
||||
},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"code": "
|
||||
highlighted and collapsed
|
||||
highlighted and collapsed
|
||||
highlighted and collapsed
|
||||
Only highlighted
|
||||
Only collapsed
|
||||
highlighted and collapsed
|
||||
highlighted and collapsed
|
||||
Only collapsed
|
||||
highlighted and collapsed",
|
||||
"lineClassNames": {
|
||||
"1": [
|
||||
"highlight",
|
||||
"collapse",
|
||||
],
|
||||
"2": [
|
||||
"highlight",
|
||||
"collapse",
|
||||
],
|
||||
"3": [
|
||||
"highlight",
|
||||
"collapse",
|
||||
],
|
||||
"4": [
|
||||
"highlight",
|
||||
],
|
||||
"5": [
|
||||
"collapse",
|
||||
],
|
||||
"6": [
|
||||
"highlight",
|
||||
"collapse",
|
||||
],
|
||||
"7": [
|
||||
"highlight",
|
||||
"collapse",
|
||||
],
|
||||
"8": [
|
||||
"collapse",
|
||||
],
|
||||
"9": [
|
||||
"highlight",
|
||||
"collapse",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
expect(
|
||||
parseLines(
|
||||
`// a
|
||||
|
@ -358,7 +631,67 @@ line
|
|||
],
|
||||
},
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"code": "line
|
||||
line",
|
||||
"lineClassNames": {
|
||||
"0": [
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d",
|
||||
],
|
||||
"1": [
|
||||
"b",
|
||||
"d",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('handles CRLF line breaks with highlight comments correctly', () => {
|
||||
expect(
|
||||
parseLines(
|
||||
`aaaaa\r\n// highlight-start\r\nbbbbb\r\n// highlight-end\r\n`,
|
||||
{
|
||||
metastring: '',
|
||||
language: 'js',
|
||||
magicComments: defaultMagicComments,
|
||||
},
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"code": "aaaaa
|
||||
bbbbb",
|
||||
"lineClassNames": {
|
||||
"1": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('handles CRLF line breaks with highlight metastring', () => {
|
||||
expect(
|
||||
parseLines(`aaaaa\r\nbbbbb\r\n`, {
|
||||
metastring: '{2}',
|
||||
language: 'js',
|
||||
magicComments: defaultMagicComments,
|
||||
}),
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
"code": "aaaaa
|
||||
bbbbb",
|
||||
"lineClassNames": {
|
||||
"1": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -369,13 +702,13 @@ describe('getLineNumbersStart', () => {
|
|||
showLineNumbers: undefined,
|
||||
metastring: undefined,
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`undefined`);
|
||||
expect(
|
||||
getLineNumbersStart({
|
||||
showLineNumbers: undefined,
|
||||
metastring: '',
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`undefined`);
|
||||
});
|
||||
|
||||
describe('handles prop', () => {
|
||||
|
@ -386,7 +719,7 @@ describe('getLineNumbersStart', () => {
|
|||
showLineNumbers: true,
|
||||
metastring: 'showLineNumbers=2',
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`1`);
|
||||
});
|
||||
|
||||
it('set to false', () => {
|
||||
|
@ -395,7 +728,7 @@ describe('getLineNumbersStart', () => {
|
|||
showLineNumbers: false,
|
||||
metastring: 'showLineNumbers=2',
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`undefined`);
|
||||
});
|
||||
|
||||
it('set to number', () => {
|
||||
|
@ -404,7 +737,7 @@ describe('getLineNumbersStart', () => {
|
|||
showLineNumbers: 10,
|
||||
metastring: 'showLineNumbers=2',
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`10`);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -415,7 +748,7 @@ describe('getLineNumbersStart', () => {
|
|||
showLineNumbers: true,
|
||||
metastring: undefined,
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`1`);
|
||||
});
|
||||
|
||||
it('set to false', () => {
|
||||
|
@ -424,7 +757,7 @@ describe('getLineNumbersStart', () => {
|
|||
showLineNumbers: false,
|
||||
metastring: undefined,
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`undefined`);
|
||||
});
|
||||
|
||||
it('set to number', () => {
|
||||
|
@ -433,7 +766,7 @@ describe('getLineNumbersStart', () => {
|
|||
showLineNumbers: 10,
|
||||
metastring: undefined,
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`10`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -446,7 +779,7 @@ describe('getLineNumbersStart', () => {
|
|||
showLineNumbers: undefined,
|
||||
metastring: 'showLineNumbers',
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`1`);
|
||||
});
|
||||
it('set with number', () => {
|
||||
expect(
|
||||
|
@ -454,7 +787,7 @@ describe('getLineNumbersStart', () => {
|
|||
showLineNumbers: undefined,
|
||||
metastring: 'showLineNumbers=10',
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`10`);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -465,7 +798,7 @@ describe('getLineNumbersStart', () => {
|
|||
showLineNumbers: undefined,
|
||||
metastring: '{1,2-3} title="file.txt" showLineNumbers noInline',
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`1`);
|
||||
});
|
||||
it('set with number', () => {
|
||||
expect(
|
||||
|
@ -473,8 +806,219 @@ describe('getLineNumbersStart', () => {
|
|||
showLineNumbers: undefined,
|
||||
metastring: '{1,2-3} title="file.txt" showLineNumbers=10 noInline',
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`10`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCodeBlockMetadata', () => {
|
||||
type Params = Parameters<typeof createCodeBlockMetadata>[0];
|
||||
|
||||
const defaultParams: Params = {
|
||||
code: '',
|
||||
className: undefined,
|
||||
metastring: '',
|
||||
language: undefined,
|
||||
defaultLanguage: undefined,
|
||||
magicComments: defaultMagicComments,
|
||||
title: undefined,
|
||||
showLineNumbers: undefined,
|
||||
};
|
||||
|
||||
function create(params?: Partial<Params>) {
|
||||
return createCodeBlockMetadata({...defaultParams, ...params});
|
||||
}
|
||||
|
||||
it('creates basic metadata', () => {
|
||||
const meta = create();
|
||||
expect(meta).toMatchInlineSnapshot(`
|
||||
{
|
||||
"className": "language-text",
|
||||
"code": "",
|
||||
"codeInput": "",
|
||||
"language": "text",
|
||||
"lineClassNames": {},
|
||||
"lineNumbersStart": undefined,
|
||||
"title": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
describe('language', () => {
|
||||
it('returns input language', () => {
|
||||
const meta = create({language: 'js'});
|
||||
expect(meta.language).toBe('js');
|
||||
});
|
||||
|
||||
it('returns className language', () => {
|
||||
const meta = create({className: 'x language-ts y z'});
|
||||
expect(meta.language).toBe('ts');
|
||||
});
|
||||
|
||||
it('returns default language', () => {
|
||||
const meta = create({defaultLanguage: 'jsx'});
|
||||
expect(meta.language).toBe('jsx');
|
||||
});
|
||||
|
||||
it('returns fallback language', () => {
|
||||
const meta = create();
|
||||
expect(meta.language).toBe('text');
|
||||
});
|
||||
|
||||
it('returns language with expected precedence', () => {
|
||||
expect(
|
||||
create({
|
||||
language: 'js',
|
||||
className: 'x language-ts y z',
|
||||
defaultLanguage: 'jsx',
|
||||
}).language,
|
||||
).toBe('js');
|
||||
expect(
|
||||
create({
|
||||
language: undefined,
|
||||
className: 'x language-ts y z',
|
||||
defaultLanguage: 'jsx',
|
||||
}).language,
|
||||
).toBe('ts');
|
||||
expect(
|
||||
create({
|
||||
language: undefined,
|
||||
className: 'x y z',
|
||||
defaultLanguage: 'jsx',
|
||||
}).language,
|
||||
).toBe('jsx');
|
||||
expect(
|
||||
create({
|
||||
language: undefined,
|
||||
className: 'x y z',
|
||||
defaultLanguage: undefined,
|
||||
}).language,
|
||||
).toBe('text');
|
||||
});
|
||||
});
|
||||
|
||||
describe('code highlighting', () => {
|
||||
it('returns code with no highlighting', () => {
|
||||
const code = 'const x = 42;';
|
||||
const meta = create({code});
|
||||
expect(meta.codeInput).toBe(code);
|
||||
expect(meta.code).toBe(code);
|
||||
expect(meta.lineClassNames).toMatchInlineSnapshot(`{}`);
|
||||
});
|
||||
|
||||
it('returns code with metastring highlighting', () => {
|
||||
const code = 'const x = 42;';
|
||||
const meta = create({code, metastring: '{1}'});
|
||||
expect(meta.codeInput).toBe(code);
|
||||
expect(meta.code).toBe(code);
|
||||
expect(meta.lineClassNames).toMatchInlineSnapshot(
|
||||
`
|
||||
{
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
}
|
||||
`,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns code with magic comment highlighting', () => {
|
||||
const code = 'const x = 42;';
|
||||
const inputCode = `// highlight-next-line\n${code}`;
|
||||
|
||||
const meta = create({code: inputCode});
|
||||
expect(meta.codeInput).toBe(inputCode);
|
||||
expect(meta.code).toBe(code);
|
||||
expect(meta.lineClassNames).toMatchInlineSnapshot(
|
||||
`
|
||||
{
|
||||
"0": [
|
||||
"theme-code-block-highlighted-line",
|
||||
],
|
||||
}
|
||||
`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('className', () => {
|
||||
it('returns provided className with current language', () => {
|
||||
const meta = create({language: 'js', className: 'some-class'});
|
||||
expect(meta.className).toBe('some-class language-js');
|
||||
});
|
||||
|
||||
it('returns provided className with fallback language', () => {
|
||||
const meta = create({className: 'some-class'});
|
||||
expect(meta.className).toBe('some-class language-text');
|
||||
});
|
||||
|
||||
it('returns provided className without duplicating className language', () => {
|
||||
const meta = create({
|
||||
language: 'js',
|
||||
className: 'some-class language-js',
|
||||
});
|
||||
expect(meta.className).toBe('some-class language-js');
|
||||
});
|
||||
});
|
||||
|
||||
describe('title', () => {
|
||||
it('returns no title', () => {
|
||||
const meta = create();
|
||||
expect(meta.title).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns title from metastring', () => {
|
||||
const meta = create({metastring: "title='my title meta'"});
|
||||
expect(meta.title).toBe('my title meta');
|
||||
});
|
||||
|
||||
it('returns title from param', () => {
|
||||
const meta = create({title: 'my title param'});
|
||||
expect(meta.title).toBe('my title param');
|
||||
});
|
||||
|
||||
it('returns title from meta over params', () => {
|
||||
const meta = create({
|
||||
metastring: "title='my title meta'",
|
||||
title: 'my title param',
|
||||
});
|
||||
expect(meta.title).toBe('my title meta');
|
||||
});
|
||||
});
|
||||
|
||||
describe('showLineNumbers', () => {
|
||||
it('returns no lineNumbersStart', () => {
|
||||
const meta = create();
|
||||
expect(meta.lineNumbersStart).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns lineNumbersStart - params.showLineNumbers=true', () => {
|
||||
const meta = create({showLineNumbers: true});
|
||||
expect(meta.lineNumbersStart).toBe(1);
|
||||
});
|
||||
|
||||
it('returns lineNumbersStart - params.showLineNumbers=3', () => {
|
||||
const meta = create({showLineNumbers: 3});
|
||||
expect(meta.lineNumbersStart).toBe(3);
|
||||
});
|
||||
|
||||
it('returns lineNumbersStart - meta showLineNumbers', () => {
|
||||
const meta = create({metastring: 'showLineNumbers'});
|
||||
expect(meta.lineNumbersStart).toBe(1);
|
||||
});
|
||||
|
||||
it('returns lineNumbersStart - meta showLineNumbers=2', () => {
|
||||
const meta = create({metastring: 'showLineNumbers=2'});
|
||||
expect(meta.lineNumbersStart).toBe(2);
|
||||
});
|
||||
|
||||
it('returns lineNumbersStart - params.showLineNumbers=3 + meta showLineNumbers=2', () => {
|
||||
const meta = create({
|
||||
showLineNumbers: 3,
|
||||
metastring: 'showLineNumbers=2',
|
||||
});
|
||||
expect(meta.lineNumbersStart).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {renderHook} from '@testing-library/react-hooks';
|
||||
import {Context} from '@docusaurus/core/src/client/docusaurusContext';
|
||||
import {useTitleFormatter} from '../generalUtils';
|
||||
import type {DocusaurusContext} from '@docusaurus/types';
|
||||
|
||||
describe('useTitleFormatter', () => {
|
||||
const createUseTitleFormatterMock =
|
||||
(context: DocusaurusContext) => (title?: string) =>
|
||||
renderHook(() => useTitleFormatter(title), {
|
||||
wrapper: ({children}) => (
|
||||
<Context.Provider value={context}>{children}</Context.Provider>
|
||||
),
|
||||
}).result.current;
|
||||
it('works', () => {
|
||||
const mockUseTitleFormatter = createUseTitleFormatterMock({
|
||||
siteConfig: {
|
||||
title: 'my site',
|
||||
titleDelimiter: '·',
|
||||
},
|
||||
} as DocusaurusContext);
|
||||
expect(mockUseTitleFormatter('a page')).toBe('a page · my site');
|
||||
expect(mockUseTitleFormatter(undefined)).toBe('my site');
|
||||
expect(mockUseTitleFormatter(' ')).toBe('my site');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {TitleFormatterFnDefault} from '../titleFormatterUtils';
|
||||
|
||||
describe('TitleFormatterFnDefault', () => {
|
||||
it('works', () => {
|
||||
expect(
|
||||
TitleFormatterFnDefault({
|
||||
title: 'a page',
|
||||
siteTitle: 'my site',
|
||||
titleDelimiter: '·',
|
||||
}),
|
||||
).toBe('a page · my site');
|
||||
});
|
||||
|
||||
it('ignores empty title', () => {
|
||||
expect(
|
||||
TitleFormatterFnDefault({
|
||||
title: ' ',
|
||||
siteTitle: 'my site',
|
||||
titleDelimiter: '·',
|
||||
}),
|
||||
).toBe('my site');
|
||||
});
|
||||
|
||||
it('does not duplicate site title', () => {
|
||||
// Users may pass <Layout title={siteTitle}> leading to duplicate titles
|
||||
// By default it's preferable to avoid duplicate siteTitle
|
||||
// See also https://github.com/facebook/docusaurus/issues/5878#issuecomment-961505856
|
||||
expect(
|
||||
TitleFormatterFnDefault({
|
||||
title: 'my site',
|
||||
siteTitle: 'my site',
|
||||
titleDelimiter: '·',
|
||||
}),
|
||||
).toBe('my site');
|
||||
});
|
||||
});
|
|
@ -5,9 +5,13 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import type {CSSProperties} from 'react';
|
||||
import type {CSSProperties, ReactNode} from 'react';
|
||||
import {createContext, useContext, useMemo} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import rangeParser from 'parse-numeric-range';
|
||||
import {ReactContextError} from './reactUtils';
|
||||
import type {PrismTheme, PrismThemeEntry} from 'prism-react-renderer';
|
||||
import type {WordWrap} from '../hooks/useCodeWordWrap';
|
||||
|
||||
const codeBlockTitleRegex = /title=(?<quote>["'])(?<title>.*?)\1/;
|
||||
const metastringLinesRangeRegex = /\{(?<range>[\d,-]+)\}/;
|
||||
|
@ -184,65 +188,42 @@ export function getLineNumbersStart({
|
|||
return getMetaLineNumbersStart(metastring);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the language name from the class name (set by MDX).
|
||||
* e.g. `"language-javascript"` => `"javascript"`.
|
||||
* Returns undefined if there is no language class name.
|
||||
*/
|
||||
export function parseLanguage(className: string): string | undefined {
|
||||
const languageClassName = className
|
||||
.split(' ')
|
||||
.find((str) => str.startsWith('language-'));
|
||||
return languageClassName?.replace(/language-/, '');
|
||||
}
|
||||
type ParseCodeLinesParam = {
|
||||
/**
|
||||
* The full metastring, as received from MDX. Line ranges declared here
|
||||
* start at 1.
|
||||
*/
|
||||
metastring: string | undefined;
|
||||
/**
|
||||
* Language of the code block, used to determine which kinds of magic
|
||||
* comment styles to enable.
|
||||
*/
|
||||
language: string | undefined;
|
||||
/**
|
||||
* Magic comment types that we should try to parse. Each entry would
|
||||
* correspond to one class name to apply to each line.
|
||||
*/
|
||||
magicComments: MagicCommentConfig[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses the code content, strips away any magic comments, and returns the
|
||||
* clean content and the highlighted lines marked by the comments or metastring.
|
||||
*
|
||||
* If the metastring contains a range, the `content` will be returned as-is
|
||||
* without any parsing. The returned `lineClassNames` will be a map from that
|
||||
* number range to the first magic comment config entry (which _should_ be for
|
||||
* line highlight directives.)
|
||||
*
|
||||
* @param content The raw code with magic comments. Trailing newline will be
|
||||
* trimmed upfront.
|
||||
* @param options Options for parsing behavior.
|
||||
* The highlighted lines, 0-indexed. e.g. `{ 0: ["highlight", "sample"] }`
|
||||
* means the 1st line should have `highlight` and `sample` as class names.
|
||||
*/
|
||||
export function parseLines(
|
||||
content: string,
|
||||
options: {
|
||||
/**
|
||||
* The full metastring, as received from MDX. Line ranges declared here
|
||||
* start at 1.
|
||||
*/
|
||||
metastring: string | undefined;
|
||||
/**
|
||||
* Language of the code block, used to determine which kinds of magic
|
||||
* comment styles to enable.
|
||||
*/
|
||||
language: string | undefined;
|
||||
/**
|
||||
* Magic comment types that we should try to parse. Each entry would
|
||||
* correspond to one class name to apply to each line.
|
||||
*/
|
||||
magicComments: MagicCommentConfig[];
|
||||
},
|
||||
): {
|
||||
/**
|
||||
* The highlighted lines, 0-indexed. e.g. `{ 0: ["highlight", "sample"] }`
|
||||
* means the 1st line should have `highlight` and `sample` as class names.
|
||||
*/
|
||||
lineClassNames: {[lineIndex: number]: string[]};
|
||||
/**
|
||||
* If there's number range declared in the metastring, the code block is
|
||||
* returned as-is (no parsing); otherwise, this is the clean code with all
|
||||
* magic comments stripped away.
|
||||
*/
|
||||
type CodeLineClassNames = {[lineIndex: number]: string[]};
|
||||
|
||||
/**
|
||||
* Code lines after applying magic comments or metastring highlight ranges
|
||||
*/
|
||||
type ParsedCodeLines = {
|
||||
code: string;
|
||||
} {
|
||||
let code = content.replace(/\n$/, '');
|
||||
const {language, magicComments, metastring} = options;
|
||||
lineClassNames: CodeLineClassNames;
|
||||
};
|
||||
|
||||
function parseCodeLinesFromMetastring(
|
||||
code: string,
|
||||
{metastring, magicComments}: ParseCodeLinesParam,
|
||||
): ParsedCodeLines | null {
|
||||
// Highlighted lines specified in props: don't parse the content
|
||||
if (metastring && metastringLinesRangeRegex.test(metastring)) {
|
||||
const linesRange = metastring.match(metastringLinesRangeRegex)!.groups!
|
||||
|
@ -258,6 +239,14 @@ export function parseLines(
|
|||
.map((n) => [n - 1, [metastringRangeClassName]] as [number, string[]]);
|
||||
return {lineClassNames: Object.fromEntries(lines), code};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseCodeLinesFromContent(
|
||||
code: string,
|
||||
params: ParseCodeLinesParam,
|
||||
): ParsedCodeLines {
|
||||
const {language, magicComments} = params;
|
||||
if (language === undefined) {
|
||||
return {lineClassNames: {}, code};
|
||||
}
|
||||
|
@ -266,7 +255,7 @@ export function parseLines(
|
|||
magicComments,
|
||||
);
|
||||
// Go through line by line
|
||||
const lines = code.split('\n');
|
||||
const lines = code.split(/\r?\n/);
|
||||
const blocks = Object.fromEntries(
|
||||
magicComments.map((d) => [d.className, {start: 0, range: ''}]),
|
||||
);
|
||||
|
@ -307,7 +296,7 @@ export function parseLines(
|
|||
}
|
||||
lines.splice(lineNumber, 1);
|
||||
}
|
||||
code = lines.join('\n');
|
||||
|
||||
const lineClassNames: {[lineIndex: number]: string[]} = {};
|
||||
Object.entries(blocks).forEach(([className, {range}]) => {
|
||||
rangeParser(range).forEach((l) => {
|
||||
|
@ -315,7 +304,145 @@ export function parseLines(
|
|||
lineClassNames[l]!.push(className);
|
||||
});
|
||||
});
|
||||
return {lineClassNames, code};
|
||||
|
||||
return {code: lines.join('\n'), lineClassNames};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the code content, strips away any magic comments, and returns the
|
||||
* clean content and the highlighted lines marked by the comments or metastring.
|
||||
*
|
||||
* If the metastring contains a range, the `content` will be returned as-is
|
||||
* without any parsing. The returned `lineClassNames` will be a map from that
|
||||
* number range to the first magic comment config entry (which _should_ be for
|
||||
* line highlight directives.)
|
||||
*/
|
||||
export function parseLines(
|
||||
code: string,
|
||||
params: ParseCodeLinesParam,
|
||||
): ParsedCodeLines {
|
||||
// Historical behavior: we remove last line break
|
||||
const newCode = code.replace(/\r?\n$/, '');
|
||||
// Historical behavior: we try one strategy after the other
|
||||
// we don't support mixing metastring ranges + magic comments
|
||||
return (
|
||||
parseCodeLinesFromMetastring(newCode, {...params}) ??
|
||||
parseCodeLinesFromContent(newCode, {...params})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the language name from the class name (set by MDX).
|
||||
* e.g. `"language-javascript"` => `"javascript"`.
|
||||
* Returns undefined if there is no language class name.
|
||||
*/
|
||||
export function parseClassNameLanguage(
|
||||
className: string | undefined,
|
||||
): string | undefined {
|
||||
if (!className) {
|
||||
return undefined;
|
||||
}
|
||||
const languageClassName = className
|
||||
.split(' ')
|
||||
.find((str) => str.startsWith('language-'));
|
||||
return languageClassName?.replace(/language-/, '');
|
||||
}
|
||||
|
||||
// Prism languages are always lowercase
|
||||
// We want to fail-safe and allow both "php" and "PHP"
|
||||
// See https://github.com/facebook/docusaurus/issues/9012
|
||||
function normalizeLanguage(language: string | undefined): string | undefined {
|
||||
return language?.toLowerCase();
|
||||
}
|
||||
|
||||
function getLanguage(params: {
|
||||
language: string | undefined;
|
||||
className: string | undefined;
|
||||
defaultLanguage: string | undefined;
|
||||
}): string {
|
||||
return (
|
||||
normalizeLanguage(
|
||||
params.language ??
|
||||
parseClassNameLanguage(params.className) ??
|
||||
params.defaultLanguage,
|
||||
) ?? 'text'
|
||||
); // There's always a language, required by Prism;
|
||||
}
|
||||
|
||||
/**
|
||||
* This ensures that we always have the code block language as className
|
||||
* For MDX code blocks this is provided automatically by MDX
|
||||
* For JSX code blocks, the language gets added by this function
|
||||
* This ensures both cases lead to a consistent HTML output
|
||||
*/
|
||||
function ensureLanguageClassName({
|
||||
className,
|
||||
language,
|
||||
}: {
|
||||
className: string | undefined;
|
||||
language: string;
|
||||
}): string {
|
||||
return clsx(
|
||||
className,
|
||||
language &&
|
||||
!className?.includes(`language-${language}`) &&
|
||||
`language-${language}`,
|
||||
);
|
||||
}
|
||||
|
||||
export interface CodeBlockMetadata {
|
||||
codeInput: string; // Including magic comments
|
||||
code: string; // Rendered code, excluding magic comments
|
||||
className: string; // There's always a "language-<lang>" className
|
||||
language: string;
|
||||
title: ReactNode;
|
||||
lineNumbersStart: number | undefined;
|
||||
lineClassNames: CodeLineClassNames;
|
||||
}
|
||||
|
||||
export function createCodeBlockMetadata(params: {
|
||||
code: string;
|
||||
className: string | undefined;
|
||||
language: string | undefined;
|
||||
defaultLanguage: string | undefined;
|
||||
metastring: string | undefined;
|
||||
magicComments: MagicCommentConfig[];
|
||||
title: ReactNode;
|
||||
showLineNumbers: boolean | number | undefined;
|
||||
}): CodeBlockMetadata {
|
||||
const language = getLanguage({
|
||||
language: params.language,
|
||||
defaultLanguage: params.defaultLanguage,
|
||||
className: params.className,
|
||||
});
|
||||
|
||||
const {lineClassNames, code} = parseLines(params.code, {
|
||||
metastring: params.metastring,
|
||||
magicComments: params.magicComments,
|
||||
language,
|
||||
});
|
||||
|
||||
const className = ensureLanguageClassName({
|
||||
className: params.className,
|
||||
language,
|
||||
});
|
||||
|
||||
const title = parseCodeBlockTitle(params.metastring) || params.title;
|
||||
|
||||
const lineNumbersStart = getLineNumbersStart({
|
||||
showLineNumbers: params.showLineNumbers,
|
||||
metastring: params.metastring,
|
||||
});
|
||||
|
||||
return {
|
||||
codeInput: params.code,
|
||||
code,
|
||||
className,
|
||||
language,
|
||||
title,
|
||||
lineNumbersStart,
|
||||
lineClassNames,
|
||||
};
|
||||
}
|
||||
|
||||
export function getPrismCssVariables(prismTheme: PrismTheme): CSSProperties {
|
||||
|
@ -333,3 +460,39 @@ export function getPrismCssVariables(prismTheme: PrismTheme): CSSProperties {
|
|||
});
|
||||
return properties;
|
||||
}
|
||||
|
||||
type CodeBlockContextValue = {
|
||||
metadata: CodeBlockMetadata;
|
||||
wordWrap: WordWrap;
|
||||
};
|
||||
|
||||
const CodeBlockContext = createContext<CodeBlockContextValue | null>(null);
|
||||
|
||||
export function CodeBlockContextProvider({
|
||||
metadata,
|
||||
wordWrap,
|
||||
children,
|
||||
}: {
|
||||
metadata: CodeBlockMetadata;
|
||||
wordWrap: WordWrap;
|
||||
children: ReactNode;
|
||||
}): ReactNode {
|
||||
// Should we optimize this in 2 contexts?
|
||||
// Unlike metadata, wordWrap is stateful and likely to trigger re-renders
|
||||
const value: CodeBlockContextValue = useMemo(() => {
|
||||
return {metadata, wordWrap};
|
||||
}, [metadata, wordWrap]);
|
||||
return (
|
||||
<CodeBlockContext.Provider value={value}>
|
||||
{children}
|
||||
</CodeBlockContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useCodeBlockContext(): CodeBlockContextValue {
|
||||
const value = useContext(CodeBlockContext);
|
||||
if (value === null) {
|
||||
throw new ReactContextError('CodeBlockContextProvider');
|
||||
}
|
||||
return value;
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
|
||||
/**
|
||||
* Formats the page's title based on relevant site config and other contexts.
|
||||
*/
|
||||
export function useTitleFormatter(title?: string | undefined): string {
|
||||
const {siteConfig} = useDocusaurusContext();
|
||||
const {title: siteTitle, titleDelimiter} = siteConfig;
|
||||
return title?.trim().length
|
||||
? `${title.trim()} ${titleDelimiter} ${siteTitle}`
|
||||
: siteTitle;
|
||||
}
|
|
@ -10,7 +10,7 @@ import clsx from 'clsx';
|
|||
import Head from '@docusaurus/Head';
|
||||
import useRouteContext from '@docusaurus/useRouteContext';
|
||||
import {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
|
||||
import {useTitleFormatter} from './generalUtils';
|
||||
import {useTitleFormatter} from './titleFormatterUtils';
|
||||
|
||||
type PageMetadataProps = {
|
||||
readonly title?: string;
|
||||
|
@ -20,6 +20,55 @@ type PageMetadataProps = {
|
|||
readonly children?: ReactNode;
|
||||
};
|
||||
|
||||
function TitleMetadata({title}: {title: string}) {
|
||||
const titleFormatter = useTitleFormatter();
|
||||
const formattedTitle = titleFormatter.format(title);
|
||||
return (
|
||||
<Head>
|
||||
<title>{formattedTitle}</title>
|
||||
<meta property="og:title" content={formattedTitle} />
|
||||
</Head>
|
||||
);
|
||||
}
|
||||
|
||||
function DescriptionMetadata({description}: {description: string}) {
|
||||
return (
|
||||
<Head>
|
||||
<meta name="description" content={description} />
|
||||
<meta property="og:description" content={description} />
|
||||
</Head>
|
||||
);
|
||||
}
|
||||
|
||||
function ImageMetadata({image}: {image: string}) {
|
||||
const {withBaseUrl} = useBaseUrlUtils();
|
||||
const pageImage = withBaseUrl(image, {absolute: true});
|
||||
return (
|
||||
<Head>
|
||||
<meta property="og:image" content={pageImage} />
|
||||
<meta name="twitter:image" content={pageImage} />
|
||||
</Head>
|
||||
);
|
||||
}
|
||||
|
||||
function KeywordsMetadata({
|
||||
keywords,
|
||||
}: {
|
||||
keywords: PageMetadataProps['keywords'];
|
||||
}) {
|
||||
return (
|
||||
<Head>
|
||||
<meta
|
||||
name="keywords"
|
||||
content={
|
||||
// https://github.com/microsoft/TypeScript/issues/17002
|
||||
(Array.isArray(keywords) ? keywords.join(',') : keywords) as string
|
||||
}
|
||||
/>
|
||||
</Head>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper component to manipulate page metadata and override site defaults.
|
||||
* Works in the same way as Helmet.
|
||||
|
@ -31,33 +80,14 @@ export function PageMetadata({
|
|||
image,
|
||||
children,
|
||||
}: PageMetadataProps): ReactNode {
|
||||
const pageTitle = useTitleFormatter(title);
|
||||
const {withBaseUrl} = useBaseUrlUtils();
|
||||
const pageImage = image ? withBaseUrl(image, {absolute: true}) : undefined;
|
||||
|
||||
return (
|
||||
<Head>
|
||||
{title && <title>{pageTitle}</title>}
|
||||
{title && <meta property="og:title" content={pageTitle} />}
|
||||
|
||||
{description && <meta name="description" content={description} />}
|
||||
{description && <meta property="og:description" content={description} />}
|
||||
|
||||
{keywords && (
|
||||
<meta
|
||||
name="keywords"
|
||||
content={
|
||||
// https://github.com/microsoft/TypeScript/issues/17002
|
||||
(Array.isArray(keywords) ? keywords.join(',') : keywords) as string
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{pageImage && <meta property="og:image" content={pageImage} />}
|
||||
{pageImage && <meta name="twitter:image" content={pageImage} />}
|
||||
|
||||
{children}
|
||||
</Head>
|
||||
<>
|
||||
{title && <TitleMetadata title={title} />}
|
||||
{description && <DescriptionMetadata description={description} />}
|
||||
{keywords && <KeywordsMetadata keywords={keywords} />}
|
||||
{image && <ImageMetadata image={image} />}
|
||||
{children && <Head>{children}</Head>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {createContext, useContext} from 'react';
|
||||
import type {ReactNode} from 'react';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import useRouteContext from '@docusaurus/useRouteContext';
|
||||
import {ReactContextError} from './reactUtils';
|
||||
|
||||
type TitleFormatterParams = {
|
||||
/**
|
||||
* The page title to format
|
||||
* Usually provided with these APIs:
|
||||
* - <PageMetadata title={title}>
|
||||
* - useTitleFormatter().format(title)
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* The siteConfig.title value
|
||||
*/
|
||||
siteTitle: string;
|
||||
|
||||
/**
|
||||
* The siteConfig.titleDelimiter value
|
||||
*/
|
||||
titleDelimiter: string;
|
||||
|
||||
/**
|
||||
* The plugin that created the page you are on
|
||||
*/
|
||||
plugin: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the full formatting function, including all useful params
|
||||
* Can be customized through React context with the provider
|
||||
*/
|
||||
export type TitleFormatterFn = (params: TitleFormatterParams) => string;
|
||||
|
||||
/**
|
||||
* The default formatter is provided in params for convenience
|
||||
*/
|
||||
export type TitleFormatterFnWithDefault = (
|
||||
params: TitleFormatterParams & {
|
||||
defaultFormatter: (params: TitleFormatterParams) => string;
|
||||
},
|
||||
) => string;
|
||||
|
||||
export const TitleFormatterFnDefault: TitleFormatterFn = ({
|
||||
title,
|
||||
siteTitle,
|
||||
titleDelimiter,
|
||||
}): string => {
|
||||
const trimmedTitle = title?.trim();
|
||||
if (!trimmedTitle || trimmedTitle === siteTitle) {
|
||||
return siteTitle;
|
||||
}
|
||||
return `${trimmedTitle} ${titleDelimiter} ${siteTitle}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the simpler API exposed to theme/users
|
||||
*/
|
||||
type TitleFormatter = {format: (title: string) => string};
|
||||
|
||||
const TitleFormatterContext = createContext<TitleFormatterFnWithDefault | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
export function TitleFormatterProvider({
|
||||
formatter,
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
formatter: TitleFormatterFnWithDefault;
|
||||
}): ReactNode {
|
||||
return (
|
||||
<TitleFormatterContext.Provider value={formatter}>
|
||||
{children}
|
||||
</TitleFormatterContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function useTitleFormatterContext() {
|
||||
const value = useContext(TitleFormatterContext);
|
||||
if (value === null) {
|
||||
throw new ReactContextError('TitleFormatterProvider');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function to format the page title
|
||||
*/
|
||||
export function useTitleFormatter(): TitleFormatter {
|
||||
const formatter = useTitleFormatterContext();
|
||||
const {siteConfig} = useDocusaurusContext();
|
||||
const {title: siteTitle, titleDelimiter} = siteConfig;
|
||||
|
||||
// Unfortunately we can only call this hook here, not in the provider
|
||||
// Route context can't be accessed in any provider applied above the router
|
||||
const {plugin} = useRouteContext();
|
||||
|
||||
return {
|
||||
format: (title: string) =>
|
||||
formatter({
|
||||
title,
|
||||
siteTitle,
|
||||
titleDelimiter,
|
||||
plugin,
|
||||
defaultFormatter: TitleFormatterFnDefault,
|
||||
}),
|
||||
};
|
||||
}
|
|
@ -104,6 +104,7 @@ export type TableOfContents = {
|
|||
maxHeadingLevel: number;
|
||||
};
|
||||
|
||||
// TODO Docusaurus v4: use interface + declaration merging to enhance
|
||||
// Theme config after validation/normalization
|
||||
export type ThemeConfig = {
|
||||
docs: {
|
||||
|
|
|
@ -16,6 +16,14 @@ declare module '@docusaurus/theme-live-codeblock' {
|
|||
};
|
||||
}
|
||||
|
||||
declare module '@theme/LiveCodeBlock' {
|
||||
import type {Props as BaseProps} from '@theme/CodeBlock';
|
||||
|
||||
export interface Props extends BaseProps {}
|
||||
|
||||
export default function LiveCodeBlock(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/Playground' {
|
||||
import type {ReactNode} from 'react';
|
||||
import type {Props as BaseProps} from '@theme/CodeBlock';
|
||||
|
@ -31,6 +39,64 @@ declare module '@theme/Playground' {
|
|||
export default function Playground(props: LiveProviderProps): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/Playground/Provider' {
|
||||
import type {ReactNode} from 'react';
|
||||
import type {Props as PlaygroundProps} from '@theme/Playground';
|
||||
|
||||
export interface Props extends Omit<PlaygroundProps, 'children'> {
|
||||
code: string | undefined;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function PlaygroundProvider(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/Playground/Container' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
export interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export default function PlaygroundContainer(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/Playground/Layout' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface Props {}
|
||||
|
||||
export default function PlaygroundLayout(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/Playground/Preview' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface Props {}
|
||||
|
||||
export default function PlaygroundPreview(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/Playground/Editor' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface Props {}
|
||||
|
||||
export default function PlaygroundEditor(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/Playground/Header' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface Props {}
|
||||
|
||||
export default function PlaygroundHeader(props: Props): ReactNode;
|
||||
}
|
||||
|
||||
declare module '@theme/ReactLiveScope' {
|
||||
type Scope = {
|
||||
[key: string]: unknown;
|
||||
|
|
|
@ -5,21 +5,28 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Playground from '@theme/Playground';
|
||||
import ReactLiveScope from '@theme/ReactLiveScope';
|
||||
import CodeBlock, {type Props} from '@theme-init/CodeBlock';
|
||||
import React, {type ReactNode} from 'react';
|
||||
import type {Props as CodeBlockProps} from '@theme/CodeBlock';
|
||||
import OriginalCodeBlock from '@theme-init/CodeBlock';
|
||||
import LiveCodeBlock from '@theme/LiveCodeBlock';
|
||||
|
||||
const withLiveEditor = (Component: typeof CodeBlock) => {
|
||||
function WrappedComponent(props: Props) {
|
||||
if (props.live) {
|
||||
return <Playground scope={ReactLiveScope} {...props} />;
|
||||
}
|
||||
|
||||
return <Component {...props} />;
|
||||
// TODO Docusaurus v4: remove special case
|
||||
// see packages/docusaurus-mdx-loader/src/remark/mdx1Compat/codeCompatPlugin.ts
|
||||
// we can just use the metastring instead
|
||||
declare module '@theme/CodeBlock' {
|
||||
interface Props {
|
||||
live?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
return WrappedComponent;
|
||||
};
|
||||
function isLiveCodeBlock(props: CodeBlockProps): boolean {
|
||||
return !!props.live;
|
||||
}
|
||||
|
||||
export default withLiveEditor(CodeBlock);
|
||||
export default function CodeBlockEnhancer(props: CodeBlockProps): ReactNode {
|
||||
return isLiveCodeBlock(props) ? (
|
||||
<LiveCodeBlock {...props} />
|
||||
) : (
|
||||
<OriginalCodeBlock {...props} />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import Playground from '@theme/Playground';
|
||||
import ReactLiveScope from '@theme/ReactLiveScope';
|
||||
import type {Props} from '@theme/LiveCodeBlock';
|
||||
|
||||
export default function LiveCodeBlock(props: Props): ReactNode {
|
||||
return <Playground scope={ReactLiveScope} {...props} />;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
|
||||
import type {Props} from '@theme/Playground/Container';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default function PlaygroundContainer({children}: Props): ReactNode {
|
||||
return <div className={styles.playgroundContainer}>{children}</div>;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
.playgroundContainer {
|
||||
margin-bottom: var(--ifm-leading);
|
||||
border-radius: var(--ifm-global-radius);
|
||||
box-shadow: var(--ifm-global-shadow-lw);
|
||||
overflow: hidden;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import {LiveEditor} from 'react-live';
|
||||
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
import PlaygroundHeader from '@theme/Playground/Header';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default function PlaygroundEditor(): ReactNode {
|
||||
const isBrowser = useIsBrowser();
|
||||
return (
|
||||
<>
|
||||
<PlaygroundHeader>
|
||||
<Translate
|
||||
id="theme.Playground.liveEditor"
|
||||
description="The live editor label of the live codeblocks">
|
||||
Live Editor
|
||||
</Translate>
|
||||
</PlaygroundHeader>
|
||||
<LiveEditor
|
||||
// We force remount the editor on hydration,
|
||||
// otherwise dark prism theme is not applied
|
||||
key={String(isBrowser)}
|
||||
className={styles.playgroundEditor}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
.playgroundEditor {
|
||||
font: var(--ifm-code-font-size) / var(--ifm-pre-line-height)
|
||||
var(--ifm-font-family-monospace) !important;
|
||||
/* rtl:ignore */
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.playgroundEditor pre {
|
||||
border-radius: 0;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
export default function PlaygroundHeader({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}): ReactNode {
|
||||
return <div className={clsx(styles.playgroundHeader)}>{children}</div>;
|
||||
}
|
|
@ -5,13 +5,6 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
.playgroundContainer {
|
||||
margin-bottom: var(--ifm-leading);
|
||||
border-radius: var(--ifm-global-radius);
|
||||
box-shadow: var(--ifm-global-shadow-lw);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.playgroundHeader {
|
||||
letter-spacing: 0.08rem;
|
||||
padding: 0.75rem;
|
||||
|
@ -26,19 +19,3 @@
|
|||
background: var(--ifm-color-emphasis-600);
|
||||
color: var(--ifm-color-content-inverse);
|
||||
}
|
||||
|
||||
.playgroundEditor {
|
||||
font: var(--ifm-code-font-size) / var(--ifm-pre-line-height)
|
||||
var(--ifm-font-family-monospace) !important;
|
||||
/* rtl:ignore */
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.playgroundEditor pre {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.playgroundPreview {
|
||||
padding: 1rem;
|
||||
background-color: var(--ifm-pre-background);
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import {useThemeConfig} from '@docusaurus/theme-common';
|
||||
import PlaygroundPreview from '@theme/Playground/Preview';
|
||||
import PlaygroundEditor from '@theme/Playground/Editor';
|
||||
|
||||
import type {ThemeConfig} from '@docusaurus/theme-live-codeblock';
|
||||
|
||||
function useLiveCodeBlockThemeConfig() {
|
||||
const themeConfig = useThemeConfig() as unknown as ThemeConfig;
|
||||
return themeConfig.liveCodeBlock;
|
||||
}
|
||||
|
||||
export default function PlaygroundLayout(): ReactNode {
|
||||
const {playgroundPosition} = useLiveCodeBlockThemeConfig();
|
||||
return (
|
||||
<>
|
||||
{playgroundPosition === 'top' ? (
|
||||
<>
|
||||
<PlaygroundPreview />
|
||||
<PlaygroundEditor />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlaygroundEditor />
|
||||
<PlaygroundPreview />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import {LiveError, LivePreview} from 'react-live';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
import {ErrorBoundaryErrorMessageFallback} from '@docusaurus/theme-common';
|
||||
import ErrorBoundary from '@docusaurus/ErrorBoundary';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
import PlaygroundHeader from '@theme/Playground/Header';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
function Loader() {
|
||||
// Is it worth improving/translating?
|
||||
// eslint-disable-next-line @docusaurus/no-untranslated-text
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
function PlaygroundLivePreview(): ReactNode {
|
||||
// No SSR for the live preview
|
||||
// See https://github.com/facebook/docusaurus/issues/5747
|
||||
return (
|
||||
<BrowserOnly fallback={<Loader />}>
|
||||
{() => (
|
||||
<>
|
||||
<ErrorBoundary
|
||||
fallback={(params) => (
|
||||
<ErrorBoundaryErrorMessageFallback {...params} />
|
||||
)}>
|
||||
<LivePreview />
|
||||
</ErrorBoundary>
|
||||
<LiveError />
|
||||
</>
|
||||
)}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PlaygroundPreview(): ReactNode {
|
||||
return (
|
||||
<>
|
||||
<PlaygroundHeader>
|
||||
<Translate
|
||||
id="theme.Playground.result"
|
||||
description="The result label of the live codeblocks">
|
||||
Result
|
||||
</Translate>
|
||||
</PlaygroundHeader>
|
||||
<div className={styles.playgroundPreview}>
|
||||
<PlaygroundLivePreview />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
.playgroundPreview {
|
||||
padding: 1rem;
|
||||
background-color: var(--ifm-pre-background);
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import {LiveProvider} from 'react-live';
|
||||
import {usePrismTheme} from '@docusaurus/theme-common';
|
||||
|
||||
import type {Props} from '@theme/Playground/Provider';
|
||||
|
||||
// this should rather be a stable function
|
||||
// see https://github.com/facebook/docusaurus/issues/9630#issuecomment-1855682643
|
||||
const DEFAULT_TRANSFORM_CODE = (code: string) => `${code};`;
|
||||
|
||||
export default function PlaygroundProvider({
|
||||
code,
|
||||
children,
|
||||
...props
|
||||
}: Props): ReactNode {
|
||||
const prismTheme = usePrismTheme();
|
||||
const noInline = props.metastring?.includes('noInline') ?? false;
|
||||
return (
|
||||
<LiveProvider
|
||||
noInline={noInline}
|
||||
theme={prismTheme}
|
||||
{...props}
|
||||
code={code?.replace(/\n$/, '')}
|
||||
transformCode={props.transformCode ?? DEFAULT_TRANSFORM_CODE}>
|
||||
{children}
|
||||
</LiveProvider>
|
||||
);
|
||||
}
|
|
@ -6,137 +6,22 @@
|
|||
*/
|
||||
|
||||
import React, {type ReactNode} from 'react';
|
||||
import clsx from 'clsx';
|
||||
import useIsBrowser from '@docusaurus/useIsBrowser';
|
||||
import {LiveProvider, LiveEditor, LiveError, LivePreview} from 'react-live';
|
||||
import Translate from '@docusaurus/Translate';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import BrowserOnly from '@docusaurus/BrowserOnly';
|
||||
import {
|
||||
ErrorBoundaryErrorMessageFallback,
|
||||
usePrismTheme,
|
||||
} from '@docusaurus/theme-common';
|
||||
import ErrorBoundary from '@docusaurus/ErrorBoundary';
|
||||
import PlaygroundProvider from '@theme/Playground/Provider';
|
||||
import PlaygroundContainer from '@theme/Playground/Container';
|
||||
import PlaygroundLayout from '@theme/Playground/Layout';
|
||||
|
||||
import type {Props} from '@theme/Playground';
|
||||
import type {ThemeConfig} from '@docusaurus/theme-live-codeblock';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
|
||||
function Header({children}: {children: ReactNode}) {
|
||||
return <div className={clsx(styles.playgroundHeader)}>{children}</div>;
|
||||
}
|
||||
|
||||
function LivePreviewLoader() {
|
||||
// Is it worth improving/translating?
|
||||
// eslint-disable-next-line @docusaurus/no-untranslated-text
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
function Preview() {
|
||||
// No SSR for the live preview
|
||||
// See https://github.com/facebook/docusaurus/issues/5747
|
||||
return (
|
||||
<BrowserOnly fallback={<LivePreviewLoader />}>
|
||||
{() => (
|
||||
<>
|
||||
<ErrorBoundary
|
||||
fallback={(params) => (
|
||||
<ErrorBoundaryErrorMessageFallback {...params} />
|
||||
)}>
|
||||
<LivePreview />
|
||||
</ErrorBoundary>
|
||||
<LiveError />
|
||||
</>
|
||||
)}
|
||||
</BrowserOnly>
|
||||
);
|
||||
}
|
||||
|
||||
function ResultWithHeader() {
|
||||
return (
|
||||
<>
|
||||
<Header>
|
||||
<Translate
|
||||
id="theme.Playground.result"
|
||||
description="The result label of the live codeblocks">
|
||||
Result
|
||||
</Translate>
|
||||
</Header>
|
||||
{/* https://github.com/facebook/docusaurus/issues/5747 */}
|
||||
<div className={styles.playgroundPreview}>
|
||||
<Preview />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ThemedLiveEditor() {
|
||||
const isBrowser = useIsBrowser();
|
||||
return (
|
||||
<LiveEditor
|
||||
// We force remount the editor on hydration,
|
||||
// otherwise dark prism theme is not applied
|
||||
key={String(isBrowser)}
|
||||
className={styles.playgroundEditor}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function EditorWithHeader() {
|
||||
return (
|
||||
<>
|
||||
<Header>
|
||||
<Translate
|
||||
id="theme.Playground.liveEditor"
|
||||
description="The live editor label of the live codeblocks">
|
||||
Live Editor
|
||||
</Translate>
|
||||
</Header>
|
||||
<ThemedLiveEditor />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// this should rather be a stable function
|
||||
// see https://github.com/facebook/docusaurus/issues/9630#issuecomment-1855682643
|
||||
const DEFAULT_TRANSFORM_CODE = (code: string) => `${code};`;
|
||||
|
||||
export default function Playground({
|
||||
children,
|
||||
transformCode,
|
||||
...props
|
||||
}: Props): ReactNode {
|
||||
const {
|
||||
siteConfig: {themeConfig},
|
||||
} = useDocusaurusContext();
|
||||
const {
|
||||
liveCodeBlock: {playgroundPosition},
|
||||
} = themeConfig as ThemeConfig;
|
||||
const prismTheme = usePrismTheme();
|
||||
|
||||
const noInline = props.metastring?.includes('noInline') ?? false;
|
||||
|
||||
return (
|
||||
<div className={styles.playgroundContainer}>
|
||||
<LiveProvider
|
||||
code={children?.replace(/\n$/, '')}
|
||||
noInline={noInline}
|
||||
transformCode={transformCode ?? DEFAULT_TRANSFORM_CODE}
|
||||
theme={prismTheme}
|
||||
{...props}>
|
||||
{playgroundPosition === 'top' ? (
|
||||
<>
|
||||
<ResultWithHeader />
|
||||
<EditorWithHeader />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<EditorWithHeader />
|
||||
<ResultWithHeader />
|
||||
</>
|
||||
)}
|
||||
</LiveProvider>
|
||||
</div>
|
||||
<PlaygroundContainer>
|
||||
<PlaygroundProvider code={children} {...props}>
|
||||
<PlaygroundLayout />
|
||||
</PlaygroundProvider>
|
||||
</PlaygroundContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/// <reference types="@docusaurus/theme-classic" />
|
||||
/// <reference types="@docusaurus/module-type-aliases" />
|
||||
|
||||
declare module '@theme-init/CodeBlock' {
|
||||
import type CodeBlock from '@theme/CodeBlock';
|
||||
import type {Props as BaseProps} from '@theme/CodeBlock';
|
||||
|
||||
export interface Props extends BaseProps {
|
||||
live?: boolean;
|
||||
}
|
||||
const CodeBlockComp: typeof CodeBlock;
|
||||
export default CodeBlockComp;
|
||||
}
|
|
@ -38,7 +38,7 @@
|
|||
"@docusaurus/theme-common": "3.7.0",
|
||||
"@docusaurus/types": "3.7.0",
|
||||
"@docusaurus/utils-validation": "3.7.0",
|
||||
"mermaid": ">=10.4",
|
||||
"mermaid": ">=11.6.0",
|
||||
"tslib": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -25,11 +25,11 @@ import Link from '@docusaurus/Link';
|
|||
import {useAllDocsData} from '@docusaurus/plugin-content-docs/client';
|
||||
import {
|
||||
HtmlClassNameProvider,
|
||||
PageMetadata,
|
||||
useEvent,
|
||||
usePluralForm,
|
||||
useSearchQueryString,
|
||||
} from '@docusaurus/theme-common';
|
||||
import {useTitleFormatter} from '@docusaurus/theme-common/internal';
|
||||
import Translate, {translate} from '@docusaurus/Translate';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import {
|
||||
|
@ -160,6 +160,25 @@ type ResultDispatcher =
|
|||
| {type: 'update'; value: ResultDispatcherState}
|
||||
| {type: 'advance'; value?: undefined};
|
||||
|
||||
function getSearchPageTitle(searchQuery: string | undefined): string {
|
||||
return searchQuery
|
||||
? translate(
|
||||
{
|
||||
id: 'theme.SearchPage.existingResultsTitle',
|
||||
message: 'Search results for "{query}"',
|
||||
description: 'The search page title for non-empty query',
|
||||
},
|
||||
{
|
||||
query: searchQuery,
|
||||
},
|
||||
)
|
||||
: translate({
|
||||
id: 'theme.SearchPage.emptyResultsTitle',
|
||||
message: 'Search the documentation',
|
||||
description: 'The search page title for empty query',
|
||||
});
|
||||
}
|
||||
|
||||
function SearchPageContent(): ReactNode {
|
||||
const {
|
||||
i18n: {currentLocale},
|
||||
|
@ -167,12 +186,13 @@ function SearchPageContent(): ReactNode {
|
|||
const {
|
||||
algolia: {appId, apiKey, indexName, contextualSearch},
|
||||
} = useAlgoliaThemeConfig();
|
||||
|
||||
const processSearchResultUrl = useSearchResultUrlProcessor();
|
||||
const documentsFoundPlural = useDocumentsFoundPlural();
|
||||
|
||||
const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers();
|
||||
const [searchQuery, setSearchQuery] = useSearchQueryString();
|
||||
const pageTitle = getSearchPageTitle(searchQuery);
|
||||
|
||||
const initialSearchResultState: ResultDispatcherState = {
|
||||
items: [],
|
||||
query: null,
|
||||
|
@ -310,24 +330,6 @@ function SearchPageContent(): ReactNode {
|
|||
),
|
||||
);
|
||||
|
||||
const getTitle = () =>
|
||||
searchQuery
|
||||
? translate(
|
||||
{
|
||||
id: 'theme.SearchPage.existingResultsTitle',
|
||||
message: 'Search results for "{query}"',
|
||||
description: 'The search page title for non-empty query',
|
||||
},
|
||||
{
|
||||
query: searchQuery,
|
||||
},
|
||||
)
|
||||
: translate({
|
||||
id: 'theme.SearchPage.emptyResultsTitle',
|
||||
message: 'Search the documentation',
|
||||
description: 'The search page title for empty query',
|
||||
});
|
||||
|
||||
const makeSearch = useEvent((page: number = 0) => {
|
||||
if (contextualSearch) {
|
||||
algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default');
|
||||
|
@ -380,8 +382,9 @@ function SearchPageContent(): ReactNode {
|
|||
|
||||
return (
|
||||
<Layout>
|
||||
<PageMetadata title={pageTitle} />
|
||||
|
||||
<Head>
|
||||
<title>{useTitleFormatter(getTitle())}</title>
|
||||
{/*
|
||||
We should not index search pages
|
||||
See https://github.com/facebook/docusaurus/pull/3233
|
||||
|
@ -390,7 +393,7 @@ function SearchPageContent(): ReactNode {
|
|||
</Head>
|
||||
|
||||
<div className="container margin-vert--lg">
|
||||
<Heading as="h1">{getTitle()}</Heading>
|
||||
<Heading as="h1">{pageTitle}</Heading>
|
||||
|
||||
<form className="row" onSubmit={(e) => e.preventDefault()}>
|
||||
<div
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"theme.lastUpdated.atDate": " في {date}",
|
||||
"theme.lastUpdated.byUser": " بواسطة {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "آخر تحديث{atDate}{byUser}",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "اللغات",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "→ العودة إلى القائمة الرئيسية",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "إصدارات",
|
||||
|
|
|
@ -144,6 +144,10 @@
|
|||
"theme.lastUpdated.byUser___DESCRIPTION": "The words used to describe by who the page has been last updated",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "Last updated{atDate}{byUser}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy___DESCRIPTION": "The sentence used to display when a page has been last updated, and by who",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel___DESCRIPTION": "The ARIA label of the button to collapse the mobile dropdown navbar item",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel___DESCRIPTION": "The ARIA label of the button to expand the mobile dropdown navbar item",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "Languages",
|
||||
"theme.navbar.mobileLanguageDropdown.label___DESCRIPTION": "The label for the mobile language switcher dropdown",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"theme.lastUpdated.atDate": " на {date}",
|
||||
"theme.lastUpdated.byUser": " от {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "Последно обновено{atDate}{byUser}",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "Езици",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Назад към главното меню",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "Версии",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"theme.lastUpdated.atDate": " {date} তারিখে",
|
||||
"theme.lastUpdated.byUser": "{user} দ্বারা",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "সর্বশেষ সংষ্করণ{atDate}{byUser}",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "Languages",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← মেন মেনুতে যান",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "Versions",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"theme.lastUpdated.atDate": " {date}",
|
||||
"theme.lastUpdated.byUser": " od {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "Naposledy aktualizováno{atDate}{byUser}",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "Languages",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Zpět na hlavní menu",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "Versions",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"theme.lastUpdated.atDate": " den {date}",
|
||||
"theme.lastUpdated.byUser": " af {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "Senest opdateret{atDate}{byUser}",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "Languages",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "Versions",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"theme.lastUpdated.atDate": " am {date}",
|
||||
"theme.lastUpdated.byUser": " von {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "Letztes Update{atDate}{byUser}",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "Sprachen",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Zurück zum Hauptmenü",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "Versionen",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"theme.lastUpdated.atDate": " en {date}",
|
||||
"theme.lastUpdated.byUser": " por {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "Última actualización{atDate}{byUser}",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "Idiomas",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Volver al menú principal",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "Versiones",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"theme.lastUpdated.atDate": " {date}",
|
||||
"theme.lastUpdated.byUser": " {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "Viimane uuendus{atDate}{byUser}",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "Keeled",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Tagasi põhi menüüsse",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "Versioonid",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"theme.lastUpdated.atDate": " در تاریخ {date}",
|
||||
"theme.lastUpdated.byUser": " توسط {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "آخرین به روز رسانی{atDate}{byUser}",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "Languages",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "→ بازگشت به منو اصلی",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "نسخهها",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"theme.lastUpdated.atDate": " noong {date}",
|
||||
"theme.lastUpdated.byUser": " ni {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "Huling inapdeyt{atDate}{byUser}",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "Languages",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Back to main menu",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "Versions",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"theme.lastUpdated.atDate": " le {date}",
|
||||
"theme.lastUpdated.byUser": " par {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "Dernière mise à jour{atDate}{byUser}",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "Langues",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Retour au menu principal",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "Versions",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"theme.lastUpdated.atDate": " בתאריך {date}",
|
||||
"theme.lastUpdated.byUser": " על ידי {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "עודכן{atDate}{byUser}",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "Languages",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← חזרה לתפריט הראשי",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "Versions",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"theme.lastUpdated.atDate": " {date} पर",
|
||||
"theme.lastUpdated.byUser": " {user} द्वारा",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "आखरी अपडेट{atDate}{byUser}",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "Languages",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← मुख्य मेनू में वापस जाएं",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "Versions",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"theme.lastUpdated.atDate": " {date} napján",
|
||||
"theme.lastUpdated.byUser": " {user} által",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "Utolsó frissítés{atDate}{byUser}",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "Nyelvek",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Vissza a főmenühöz",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "Verziók",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"theme.lastUpdated.atDate": " pada {date}",
|
||||
"theme.lastUpdated.byUser": " oleh {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "Diperbaharui{atDate}{byUser}",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "Bahasa",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Kembali ke menu utama",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "Versi",
|
||||
|
|
|
@ -73,6 +73,8 @@
|
|||
"theme.lastUpdated.atDate": " þann {date}",
|
||||
"theme.lastUpdated.byUser": " af {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "Seinast uppfært{atDate}{byUser}",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "Tungumál",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Til baka á aðal valmynd",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "Útgáfur",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"theme.lastUpdated.atDate": " il {date}",
|
||||
"theme.lastUpdated.byUser": " da {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "Ultimo aggiornamento{atDate}{byUser}",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "Linguaggio",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Indietro al menu principale",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "Versioni",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"theme.lastUpdated.atDate": "{date}に",
|
||||
"theme.lastUpdated.byUser": "{user}が",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "{atDate}{byUser}最終更新",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "他の言語",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← メインメニューに戻る",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "他のバージョン",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"theme.lastUpdated.atDate": " {date}에",
|
||||
"theme.lastUpdated.byUser": " {user}가",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "최종 수정: {atDate}{byUser}",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "언어",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← 메인 메뉴로 돌아가기",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "버전",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"theme.lastUpdated.atDate": " den {date}",
|
||||
"theme.lastUpdated.byUser": " av {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "Sist oppdatert{atDate}{byUser}",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "Språk",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Tilbake til hovedmenyen",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "Versjoner",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"theme.lastUpdated.atDate": " op {date}",
|
||||
"theme.lastUpdated.byUser": " door {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "Laatst bijgewerkt{atDate}{byUser}",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "Talen",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Terug naar het hoofdmenu",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "Versies",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"theme.lastUpdated.atDate": " dnia {date}",
|
||||
"theme.lastUpdated.byUser": " przez {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "Ostatnia aktualizacja{atDate}{byUser}",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "Języki",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Wróć do menu głównego",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "Wersje",
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
"theme.lastUpdated.atDate": " em {date}",
|
||||
"theme.lastUpdated.byUser": " por {user}",
|
||||
"theme.lastUpdated.lastUpdatedAtBy": "Última atualização {atDate}{byUser}",
|
||||
"theme.navbar.mobileDropdown.collapseButton.collapseAriaLabel": "Collapse the dropdown",
|
||||
"theme.navbar.mobileDropdown.collapseButton.expandAriaLabel": "Expand the dropdown",
|
||||
"theme.navbar.mobileLanguageDropdown.label": "Linguagens",
|
||||
"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel": "← Voltar para o menu principal",
|
||||
"theme.navbar.mobileVersionsDropdown.label": "Versões",
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue