chore: backport retro compatible commits for the Docusaurus v2.3 release (#8585)

Co-authored-by: stnor <stefan@selessia.com>
Co-authored-by: Joshua Chen <sidachen2003@gmail.com>
Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
Co-authored-by: Sébastien Lorber <slorber@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matija Sirk <matija.sirk@kopit.si>
Co-authored-by: AHMET BAYHAN BAYRAMOGLU <49499275+ABB65@users.noreply.github.com>
Co-authored-by: Stefan Norberg <stefan@norberg.org>
Co-authored-by: Josh Goldberg <git@joshuakgoldberg.com>
Co-authored-by: Muhammad Hammad <33136628+mhnaeem@users.noreply.github.com>
Co-authored-by: Denis Al-Khelali <denis.al-khelali@itechart-group.com>
Co-authored-by: Balthasar Hofer <lebalz@outlook.com>
Co-authored-by: Danny Kim <0916dhkim@gmail.com>
Co-authored-by: Frieder Bluemle <frieder.bluemle@gmail.com>
Co-authored-by: John Reilly <johnny_reilly@hotmail.com>
Co-authored-by: Robert Lawrence <62929526+r-lawrence@users.noreply.github.com>
Co-authored-by: Sadegh Karimi <sadegh.krmi@gmail.com>
Co-authored-by: Lachlan Heywood <lachieh@users.noreply.github.com>
Co-authored-by: mturoci <64769322+mturoci@users.noreply.github.com>
Co-authored-by: 宋锦丰 <36468758+SJFCS@users.noreply.github.com>
Co-authored-by: Nguyễn Thành Nam <namnguyenthanh.work@gmail.com>
Co-authored-by: Dongjoon Lee <djunnni@gmail.com>
Co-authored-by: Thomas.CA <44041651+Thomascogez@users.noreply.github.com>
Co-authored-by: Riccardo <riccardo.odone@gmail.com>
Co-authored-by: Lane Goolsby <lanegoolsby@yahoo.com>
Co-authored-by: Mariusz Krzaczkowski <m.krzaczkowski@yetiforce.com>
Co-authored-by: Matija Sirk <sirkmatija@gmail.com>
Co-authored-by: Jiří <zmrhal.j@gmail.com>
This commit is contained in:
Sébastien Lorber 2023-01-27 17:02:15 +01:00 committed by GitHub
parent de972142a8
commit c84d779627
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
173 changed files with 2640 additions and 1199 deletions

View file

@ -1,6 +1,6 @@
{
"name": "new.docusaurus.io",
"version": "2.2.0",
"version": "2.3.0",
"private": true,
"scripts": {
"start": "npx --package netlify-cli netlify dev"

View file

@ -1,5 +1,5 @@
{
"version": "2.2.0",
"version": "2.3.0",
"npmClient": "yarn",
"useWorkspaces": true,
"changelog": {

View file

@ -1,6 +1,6 @@
{
"name": "create-docusaurus",
"version": "2.2.0",
"version": "2.3.0",
"description": "Create Docusaurus apps easily.",
"type": "module",
"repository": {
@ -22,8 +22,8 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/logger": "2.2.0",
"@docusaurus/utils": "2.2.0",
"@docusaurus/logger": "2.3.0",
"@docusaurus/utils": "2.3.0",
"commander": "^5.1.0",
"fs-extra": "^10.1.0",
"lodash": "^4.17.21",

View file

@ -1,6 +1,6 @@
{
"name": "docusaurus-2-classic-typescript-template",
"version": "2.2.0",
"version": "2.3.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
@ -15,8 +15,8 @@
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "2.2.0",
"@docusaurus/preset-classic": "2.2.0",
"@docusaurus/core": "2.3.0",
"@docusaurus/preset-classic": "2.3.0",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"prism-react-renderer": "^1.3.5",
@ -24,7 +24,7 @@
"react-dom": "^17.0.2"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.2.0",
"@docusaurus/module-type-aliases": "2.3.0",
"@tsconfig/docusaurus": "^1.0.5",
"typescript": "^4.7.4"
},

View file

@ -8,17 +8,22 @@ const darkCodeTheme = require('prism-react-renderer/themes/dracula');
const config = {
title: 'My Site',
tagline: 'Dinosaurs are cool',
url: 'https://your-docusaurus-test-site.com',
baseUrl: '/',
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
favicon: 'img/favicon.ico',
// Set the production url of your site here
url: 'https://your-docusaurus-test-site.com',
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: '/',
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: 'facebook', // Usually your GitHub org/user name.
projectName: 'docusaurus', // Usually your repo name.
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
// Even if you don't use internalization, you can use this field to set useful
// metadata like html lang. For example, if your site is Chinese, you may want
// to replace "en" with "zh-Hans".
@ -56,6 +61,8 @@ const config = {
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
// Replace with your project's social card
image: 'img/docusaurus-social-card.jpg',
navbar: {
title: 'My Site',
logo: {

View file

@ -1,6 +1,6 @@
{
"name": "docusaurus-2-classic-template",
"version": "2.2.0",
"version": "2.3.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
@ -14,8 +14,8 @@
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docusaurus/core": "2.2.0",
"@docusaurus/preset-classic": "2.2.0",
"@docusaurus/core": "2.3.0",
"@docusaurus/preset-classic": "2.3.0",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"prism-react-renderer": "^1.3.5",
@ -23,7 +23,7 @@
"react-dom": "^17.0.2"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.2.0"
"@docusaurus/module-type-aliases": "2.3.0"
},
"browserslist": {
"production": [

View file

@ -13,17 +13,22 @@
const config = {
title: 'My Site',
tagline: 'The tagline of my site',
url: 'https://your-docusaurus-test-site.com',
baseUrl: '/',
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
favicon: 'img/favicon.ico',
// Set the production url of your site here
url: 'https://your-docusaurus-test-site.com',
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: '/',
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: 'facebook', // Usually your GitHub org/user name.
projectName: 'docusaurus', // Usually your repo name.
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
presets: [
[
'classic',
@ -53,6 +58,7 @@ const config = {
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
image: 'img/docusaurus-social-card.jpg',
navbar: {
title: 'My Meta Project',
logo: {

View file

@ -1,6 +1,6 @@
{
"name": "docusaurus-2-facebook-template",
"version": "2.2.0",
"version": "2.3.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
@ -18,8 +18,8 @@
"format:diff": "prettier --config .prettierrc --list-different \"**/*.{js,jsx,ts,tsx,md,mdx}\""
},
"dependencies": {
"@docusaurus/core": "2.2.0",
"@docusaurus/preset-classic": "2.2.0",
"@docusaurus/core": "2.3.0",
"@docusaurus/preset-classic": "2.3.0",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"react": "^17.0.2",

View file

@ -51,7 +51,11 @@ You can use absolute paths to reference images in the static directory (`static/
![Docusaurus logo](/img/docusaurus.png)
You can reference images relative to the current file as well, as shown in [the extra guides](../tutorial-extras/manage-docs-versions.md).
You can reference images relative to the current file as well. This is particularly useful to colocate images close to the Markdown files using them:
```md
![Docusaurus logo](./img/docusaurus.png)
```
## Code Blocks

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/cssnano-preset",
"version": "2.2.0",
"version": "2.3.0",
"description": "Advanced cssnano preset for maximum optimization.",
"main": "lib/index.js",
"license": "MIT",

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/logger",
"version": "2.2.0",
"version": "2.3.0",
"description": "An encapsulated logger for semantically formatting console messages.",
"main": "./lib/index.js",
"repository": {

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/mdx-loader",
"version": "2.2.0",
"version": "2.3.0",
"description": "Docusaurus Loader for MDX",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@ -20,8 +20,8 @@
"dependencies": {
"@babel/parser": "^7.18.8",
"@babel/traverse": "^7.18.8",
"@docusaurus/logger": "2.2.0",
"@docusaurus/utils": "2.2.0",
"@docusaurus/logger": "2.3.0",
"@docusaurus/utils": "2.3.0",
"@mdx-js/mdx": "^1.6.22",
"escape-html": "^1.0.3",
"file-loader": "^6.2.0",
@ -37,10 +37,9 @@
"webpack": "^5.73.0"
},
"devDependencies": {
"@docusaurus/types": "2.2.0",
"@docusaurus/types": "2.3.0",
"@types/escape-html": "^1.0.2",
"@types/mdast": "^3.0.10",
"@types/mermaid": "^8.2.9",
"@types/stringify-object": "^3.3.1",
"@types/unist": "^2.0.6",
"rehype-stringify": "^8.0.0",

View file

@ -0,0 +1,10 @@
Test nested Admonitions
::::info **Weather**
On nice days, you can enjoy skiing in the mountains.
:::danger *Storms*
Take care of snowstorms...
:::
::::

View file

@ -42,3 +42,8 @@ exports[`admonitions remark plugin interpolation 1`] = `
"<p>Test admonition with interpolated title/body</p>
<admonition type="tip"><mdxAdmonitionTitle>My <code>interpolated</code> <strong>title</strong> &#x3C;button style={{color: "red"}} onClick={() => alert("click")}>test</mdxAdmonitionTitle><p><code>body</code> <strong>interpolated</strong> content</p></admonition>"
`;
exports[`admonitions remark plugin nesting 1`] = `
"<p>Test nested Admonitions</p>
<admonition type="info"><mdxAdmonitionTitle><strong>Weather</strong></mdxAdmonitionTitle><p>On nice days, you can enjoy skiing in the mountains.</p><admonition type="danger"><mdxAdmonitionTitle><em>Storms</em></mdxAdmonitionTitle><p>Take care of snowstorms...</p></admonition></admonition>"
`;

View file

@ -50,4 +50,9 @@ describe('admonitions remark plugin', () => {
const result = await processFixture('interpolation');
expect(result).toMatchSnapshot();
});
it('nesting', async () => {
const result = await processFixture('nesting');
expect(result).toMatchSnapshot();
});
});

View file

@ -52,9 +52,20 @@ const plugin: Plugin = function plugin(
const options = normalizeOptions(optionsInput);
const keywords = Object.values(options.keywords).map(escapeRegExp).join('|');
const nestingChar = escapeRegExp(options.tag.slice(0, 1));
const tag = escapeRegExp(options.tag);
const regex = new RegExp(`${tag}(${keywords})(?: *(.*))?\n`);
const escapeTag = new RegExp(escapeRegExp(`\\${options.tag}`), 'g');
// resolve th nesting level of an opening tag
// ::: -> 0, :::: -> 1, ::::: -> 2 ...
const nestingLevelRegex = new RegExp(
`^${tag}(?<nestingLevel>${nestingChar}*)`,
);
const regex = new RegExp(`${tag}${nestingChar}*(${keywords})(?: *(.*))?\n`);
const escapeTag = new RegExp(
escapeRegExp(`\\${options.tag}${options.tag.slice(0, 1)}*`),
'g',
);
// The tokenizer is called on blocks to determine if there is an admonition
// present and create tags for it
@ -77,6 +88,11 @@ const plugin: Plugin = function plugin(
];
const food = [];
const content = [];
// get the nesting level of the opening tag
const openingLevel =
nestingLevelRegex.exec(opening)!.groups!.nestingLevel!.length;
// used as a stack to keep track of nested admonitions
const nestingLevels: number[] = [openingLevel];
let newValue = value;
// consume lines until a closing tag
@ -88,12 +104,32 @@ const plugin: Plugin = function plugin(
next !== -1 ? newValue.slice(idx + 1, next) : newValue.slice(idx + 1);
food.push(line);
newValue = newValue.slice(idx + 1);
// the closing tag is NOT part of the content
if (line.startsWith(options.tag)) {
break;
const nesting = nestingLevelRegex.exec(line);
idx = newValue.indexOf(NEWLINE);
if (!nesting) {
content.push(line);
continue;
}
const tagLevel = nesting.groups!.nestingLevel!.length;
// first level
if (nestingLevels.length === 0) {
nestingLevels.push(tagLevel);
content.push(line);
continue;
}
const currentLevel = nestingLevels[nestingLevels.length - 1]!;
if (tagLevel < currentLevel) {
// entering a nested admonition block
nestingLevels.push(tagLevel);
} else if (tagLevel === currentLevel) {
// closing a nested admonition block
nestingLevels.pop();
// the closing tag is NOT part of the content
if (nestingLevels.length === 0) {
break;
}
}
content.push(line);
idx = newValue.indexOf(NEWLINE);
}
// consume the processed tag and replace escape sequences

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/migrate",
"version": "2.2.0",
"version": "2.3.0",
"description": "A CLI tool to migrate from older versions of Docusaurus.",
"license": "MIT",
"engines": {
@ -24,8 +24,8 @@
"dependencies": {
"@babel/core": "^7.18.6",
"@babel/preset-env": "^7.18.6",
"@docusaurus/logger": "2.2.0",
"@docusaurus/utils": "2.2.0",
"@docusaurus/logger": "2.3.0",
"@docusaurus/utils": "2.3.0",
"@mapbox/hast-util-to-jsx": "^2.0.0",
"color": "^4.2.3",
"commander": "^5.1.0",

View file

@ -120,7 +120,7 @@ exports[`migration CLI migrates complex website: write 1`] = `
]
}
],
"copyright": "Copyright © 2022 Facebook Inc.",
"copyright": "Copyright © 2023 Facebook Inc.",
"logo": {
"src": "img/docusaurus_monochrome.svg"
}
@ -303,7 +303,7 @@ exports[`migration CLI migrates missing versions: write 1`] = `
]
}
],
"copyright": "Copyright © 2022 Facebook Inc.",
"copyright": "Copyright © 2023 Facebook Inc.",
"logo": {
"src": "img/docusaurus_monochrome.svg"
}
@ -483,7 +483,7 @@ exports[`migration CLI migrates simple website: write 1`] = `
]
}
],
"copyright": "Copyright © 2022 Facebook Inc.",
"copyright": "Copyright © 2023 Facebook Inc.",
"logo": {
"src": "img/docusaurus_monochrome.svg"
}

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/module-type-aliases",
"version": "2.2.0",
"version": "2.3.0",
"description": "Docusaurus module type aliases.",
"types": "./src/index.d.ts",
"publishConfig": {
@ -13,7 +13,7 @@
},
"dependencies": {
"@docusaurus/react-loadable": "5.5.2",
"@docusaurus/types": "2.2.0",
"@docusaurus/types": "2.3.0",
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router-config": "*",

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-client-redirects",
"version": "2.2.0",
"version": "2.3.0",
"description": "Client redirects plugin for Docusaurus.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@ -18,18 +18,18 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "2.2.0",
"@docusaurus/logger": "2.2.0",
"@docusaurus/utils": "2.2.0",
"@docusaurus/utils-common": "2.2.0",
"@docusaurus/utils-validation": "2.2.0",
"@docusaurus/core": "2.3.0",
"@docusaurus/logger": "2.3.0",
"@docusaurus/utils": "2.3.0",
"@docusaurus/utils-common": "2.3.0",
"@docusaurus/utils-validation": "2.3.0",
"eta": "^1.12.3",
"fs-extra": "^10.1.0",
"lodash": "^4.17.21",
"tslib": "^2.4.0"
},
"devDependencies": {
"@docusaurus/types": "2.2.0"
"@docusaurus/types": "2.3.0"
},
"peerDependencies": {
"react": "^16.8.4 || ^17.0.0",

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-content-blog",
"version": "2.2.0",
"version": "2.3.0",
"description": "Blog plugin for Docusaurus.",
"main": "lib/index.js",
"types": "src/plugin-content-blog.d.ts",
@ -18,13 +18,13 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "2.2.0",
"@docusaurus/logger": "2.2.0",
"@docusaurus/mdx-loader": "2.2.0",
"@docusaurus/types": "2.2.0",
"@docusaurus/utils": "2.2.0",
"@docusaurus/utils-common": "2.2.0",
"@docusaurus/utils-validation": "2.2.0",
"@docusaurus/core": "2.3.0",
"@docusaurus/logger": "2.3.0",
"@docusaurus/mdx-loader": "2.3.0",
"@docusaurus/types": "2.3.0",
"@docusaurus/utils": "2.3.0",
"@docusaurus/utils-common": "2.3.0",
"@docusaurus/utils-validation": "2.3.0",
"cheerio": "^1.0.0-rc.12",
"feed": "^4.2.2",
"fs-extra": "^10.1.0",
@ -35,9 +35,6 @@
"utility-types": "^3.10.0",
"webpack": "^5.73.0"
},
"devDependencies": {
"escape-string-regexp": "^4.0.0"
},
"peerDependencies": {
"react": "^16.8.4 || ^17.0.0",
"react-dom": "^16.8.4 || ^17.0.0"

File diff suppressed because one or more lines are too long

View file

@ -143,4 +143,56 @@ describe.each(['atom', 'rss', 'json'])('%s', (feedType) => {
).toMatchSnapshot();
fsMock.mockClear();
});
it('filters to the first two entries', async () => {
const siteDir = path.join(__dirname, '__fixtures__', 'website');
const outDir = path.join(siteDir, 'build-snap');
const siteConfig = {
title: 'Hello',
baseUrl: '/myBaseUrl/',
url: 'https://docusaurus.io',
favicon: 'image/favicon.ico',
};
// Build is quite difficult to mock, so we built the blog beforehand and
// copied the output to the fixture...
await testGenerateFeeds(
{
siteDir,
siteConfig,
i18n: DefaultI18N,
outDir,
} as LoadContext,
{
path: 'blog',
routeBasePath: 'blog',
tagsBasePath: 'tags',
authorsMapPath: 'authors.yml',
include: DEFAULT_OPTIONS.include,
exclude: DEFAULT_OPTIONS.exclude,
feedOptions: {
type: [feedType],
copyright: 'Copyright',
createFeedItems: async (params) => {
const {blogPosts, defaultCreateFeedItems, ...rest} = params;
const blogPostsFiltered = blogPosts.filter(
(item, index) => index < 2,
);
return defaultCreateFeedItems({
blogPosts: blogPostsFiltered,
...rest,
});
},
},
readingTime: ({content, defaultReadingTime}) =>
defaultReadingTime({content}),
truncateMarker: /<!--\s*truncate\s*-->/,
} as PluginOptions,
);
expect(
fsMock.mock.calls.map((call) => call[1] as string),
).toMatchSnapshot();
fsMock.mockClear();
});
});

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import escapeStringRegexp from 'escape-string-regexp';
import {escapeRegexp} from '@docusaurus/utils';
import {validateBlogPostFrontMatter} from '../frontMatter';
import type {BlogPostFrontMatter} from '@docusaurus/plugin-content-blog';
@ -57,7 +57,7 @@ function testField(params: {
} catch (err) {
// eslint-disable-next-line jest/no-conditional-expect
expect((err as Error).message).toMatch(
new RegExp(escapeStringRegexp(message)),
new RegExp(escapeRegexp(message)),
);
}
});

View file

@ -8,7 +8,7 @@
import path from 'path';
import fs from 'fs-extra';
import logger from '@docusaurus/logger';
import {Feed, type Author as FeedAuthor, type Item as FeedItem} from 'feed';
import {Feed, type Author as FeedAuthor} from 'feed';
import {normalizeUrl, readOutputHTMLFile} from '@docusaurus/utils';
import {blogPostContainerID} from '@docusaurus/utils-common';
import {load as cheerioLoad} from 'cheerio';
@ -18,6 +18,7 @@ import type {
PluginOptions,
Author,
BlogPost,
BlogFeedItem,
} from '@docusaurus/plugin-content-blog';
async function generateBlogFeed({
@ -54,14 +55,39 @@ async function generateBlogFeed({
copyright: feedOptions.copyright,
});
const createFeedItems =
options.feedOptions.createFeedItems ?? defaultCreateFeedItems;
const feedItems = await createFeedItems({
blogPosts,
siteConfig,
outDir,
defaultCreateFeedItems,
});
feedItems.forEach(feed.addItem);
return feed;
}
async function defaultCreateFeedItems({
blogPosts,
siteConfig,
outDir,
}: {
blogPosts: BlogPost[];
siteConfig: DocusaurusConfig;
outDir: string;
}): Promise<BlogFeedItem[]> {
const {url: siteUrl} = siteConfig;
function toFeedAuthor(author: Author): FeedAuthor {
return {name: author.name, link: author.url, email: author.email};
}
await Promise.all(
return Promise.all(
blogPosts.map(async (post) => {
const {
id,
metadata: {
title: metadataTitle,
permalink,
@ -79,10 +105,11 @@ async function generateBlogFeed({
);
const $ = cheerioLoad(content);
const feedItem: FeedItem = {
const link = normalizeUrl([siteUrl, permalink]);
const feedItem: BlogFeedItem = {
title: metadataTitle,
id,
link: normalizeUrl([siteUrl, permalink]),
id: link,
link,
date,
description,
// Atom feed demands the "term", while other feeds use "name"
@ -99,9 +126,7 @@ async function generateBlogFeed({
return feedItem;
}),
).then((items) => items.forEach(feed.addItem));
return feed;
);
}
async function createBlogFeedFile({

View file

@ -124,6 +124,7 @@ const PluginOptionSchema = Joi.object<PluginOptions>({
.default(DEFAULT_OPTIONS.feedOptions.copyright),
}),
language: Joi.string(),
createFeedItems: Joi.function(),
}).default(DEFAULT_OPTIONS.feedOptions),
authorsMapPath: Joi.string().default(DEFAULT_OPTIONS.authorsMapPath),
readingTime: Joi.function().default(() => DEFAULT_OPTIONS.readingTime),

View file

@ -9,12 +9,19 @@ declare module '@docusaurus/plugin-content-blog' {
import type {LoadedMDXContent} from '@docusaurus/mdx-loader';
import type {MDXOptions} from '@docusaurus/mdx-loader';
import type {FrontMatterTag, Tag} from '@docusaurus/utils';
import type {Plugin, LoadContext} from '@docusaurus/types';
import type {DocusaurusConfig, Plugin, LoadContext} from '@docusaurus/types';
import type {Item as FeedItem} from 'feed';
import type {Overwrite} from 'utility-types';
export type Assets = {
/**
* If `metadata.image` is a collocated image path, this entry will be the
* If `metadata.yarn workspace website typecheck
4
yarn workspace v1.22.19yarn workspace website typecheck
4
yarn workspace v1.22.19yarn workspace website typecheck
4
yarn workspace v1.22.19image` is a collocated image path, this entry will be the
* bundler-generated image path. Otherwise, it's empty, and the image URL
* should be accessed through `frontMatter.image`.
*/
@ -255,6 +262,24 @@ declare module '@docusaurus/plugin-content-blog' {
copyright: string;
/** Language of the feed. */
language?: string;
/** Allow control over the construction of BlogFeedItems */
createFeedItems?: CreateFeedItemsFn;
};
type DefaultCreateFeedItemsParams = {
blogPosts: BlogPost[];
siteConfig: DocusaurusConfig;
outDir: string;
};
type CreateFeedItemsFn = (
params: CreateFeedItemsParams,
) => Promise<BlogFeedItem[]>;
type CreateFeedItemsParams = DefaultCreateFeedItemsParams & {
defaultCreateFeedItems: (
params: DefaultCreateFeedItemsParams,
) => Promise<BlogFeedItem[]>;
};
/**
@ -436,6 +461,8 @@ declare module '@docusaurus/plugin-content-blog' {
content: string;
};
export type BlogFeedItem = FeedItem;
export type BlogPaginatedMetadata = {
/** Title of the entire blog. */
readonly blogTitle: string;

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-content-docs",
"version": "2.2.0",
"version": "2.3.0",
"description": "Docs plugin for Docusaurus.",
"main": "lib/index.js",
"sideEffects": false,
@ -35,13 +35,13 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "2.2.0",
"@docusaurus/logger": "2.2.0",
"@docusaurus/mdx-loader": "2.2.0",
"@docusaurus/module-type-aliases": "2.2.0",
"@docusaurus/types": "2.2.0",
"@docusaurus/utils": "2.2.0",
"@docusaurus/utils-validation": "2.2.0",
"@docusaurus/core": "2.3.0",
"@docusaurus/logger": "2.3.0",
"@docusaurus/mdx-loader": "2.3.0",
"@docusaurus/module-type-aliases": "2.3.0",
"@docusaurus/types": "2.3.0",
"@docusaurus/utils": "2.3.0",
"@docusaurus/utils-validation": "2.3.0",
"@types/react-router-config": "^5.0.6",
"combine-promises": "^1.1.0",
"fs-extra": "^10.1.0",
@ -56,7 +56,6 @@
"@types/js-yaml": "^4.0.5",
"@types/picomatch": "^2.3.0",
"commander": "^5.1.0",
"escape-string-regexp": "^4.0.0",
"picomatch": "^2.3.1",
"shelljs": "^0.8.5"
},

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import escapeStringRegexp from 'escape-string-regexp';
import {escapeRegexp} from '@docusaurus/utils';
import {validateDocFrontMatter} from '../frontMatter';
import type {DocFrontMatter} from '@docusaurus/plugin-content-docs';
@ -57,7 +57,7 @@ function testField(params: {
} catch (err) {
// eslint-disable-next-line jest/no-conditional-expect
expect((err as Error).message).toMatch(
new RegExp(escapeStringRegexp(message)),
new RegExp(escapeRegexp(message)),
);
}
});

View file

@ -336,12 +336,13 @@ export default async function pluginContentDocs(
};
function createMDXLoaderRule(): RuleSetRule {
const contentDirs = versionsMetadata.flatMap(getContentPathList);
const contentDirs = versionsMetadata
.flatMap(getContentPathList)
// Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970
.map(addTrailingPathSeparator);
return {
test: /\.mdx?$/i,
include: contentDirs
// Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970
.map(addTrailingPathSeparator),
include: contentDirs,
use: [
getJSLoader({isServer}),
{

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-content-pages",
"version": "2.2.0",
"version": "2.3.0",
"description": "Pages plugin for Docusaurus.",
"main": "lib/index.js",
"types": "src/plugin-content-pages.d.ts",
@ -18,11 +18,11 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "2.2.0",
"@docusaurus/mdx-loader": "2.2.0",
"@docusaurus/types": "2.2.0",
"@docusaurus/utils": "2.2.0",
"@docusaurus/utils-validation": "2.2.0",
"@docusaurus/core": "2.3.0",
"@docusaurus/mdx-loader": "2.3.0",
"@docusaurus/types": "2.3.0",
"@docusaurus/utils": "2.3.0",
"@docusaurus/utils-validation": "2.3.0",
"fs-extra": "^10.1.0",
"tslib": "^2.4.0",
"webpack": "^5.73.0"

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-debug",
"version": "2.2.0",
"version": "2.3.0",
"description": "Debug plugin for Docusaurus.",
"main": "lib/index.js",
"types": "src/plugin-debug.d.ts",
@ -20,9 +20,9 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "2.2.0",
"@docusaurus/types": "2.2.0",
"@docusaurus/utils": "2.2.0",
"@docusaurus/core": "2.3.0",
"@docusaurus/types": "2.3.0",
"@docusaurus/utils": "2.3.0",
"fs-extra": "^10.1.0",
"react-json-view": "^1.21.3",
"tslib": "^2.4.0"

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-google-analytics",
"version": "2.2.0",
"version": "2.3.0",
"description": "Global analytics (analytics.js) plugin for Docusaurus.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@ -18,9 +18,9 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "2.2.0",
"@docusaurus/types": "2.2.0",
"@docusaurus/utils-validation": "2.2.0",
"@docusaurus/core": "2.3.0",
"@docusaurus/types": "2.3.0",
"@docusaurus/utils-validation": "2.3.0",
"tslib": "^2.4.0"
},
"peerDependencies": {

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-google-gtag",
"version": "2.2.0",
"version": "2.3.0",
"description": "Global Site Tag (gtag.js) plugin for Docusaurus.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@ -18,9 +18,9 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "2.2.0",
"@docusaurus/types": "2.2.0",
"@docusaurus/utils-validation": "2.2.0",
"@docusaurus/core": "2.3.0",
"@docusaurus/types": "2.3.0",
"@docusaurus/utils-validation": "2.3.0",
"tslib": "^2.4.0"
},
"peerDependencies": {

View file

@ -0,0 +1,3 @@
.tsbuildinfo*
tsconfig*
__tests__

View file

@ -0,0 +1,7 @@
# `@docusaurus/plugin-google-tag-manager`
Google Tag Manager plugin for Docusaurus.
## Usage
See [plugin-google-tag-manager documentation](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-google-tag-manager).

View file

@ -0,0 +1,33 @@
{
"name": "@docusaurus/plugin-google-tag-manager",
"version": "2.3.0",
"description": "Google Tag Manager (gtm.js) plugin for Docusaurus.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsc --build",
"watch": "tsc --build --watch"
},
"repository": {
"type": "git",
"url": "https://github.com/facebook/docusaurus.git",
"directory": "packages/docusaurus-plugin-google-tag-manager"
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "2.3.0",
"@docusaurus/types": "2.3.0",
"@docusaurus/utils-validation": "2.3.0",
"tslib": "^2.4.0"
},
"peerDependencies": {
"react": "^16.8.4 || ^17.0.0",
"react-dom": "^16.8.4 || ^17.0.0"
},
"engines": {
"node": ">=16.14"
}
}

View file

@ -0,0 +1,78 @@
/**
* 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 {Joi} from '@docusaurus/utils-validation';
import type {
LoadContext,
Plugin,
OptionValidationContext,
} from '@docusaurus/types';
import type {PluginOptions, Options} from './options';
export default function pluginGoogleAnalytics(
context: LoadContext,
options: PluginOptions,
): Plugin {
const {containerId} = options;
const isProd = process.env.NODE_ENV === 'production';
return {
name: 'docusaurus-plugin-google-tag-manager',
contentLoaded({actions}) {
actions.setGlobalData(options);
},
injectHtmlTags() {
if (!isProd) {
return {};
}
return {
preBodyTags: [
{
tagName: 'noscript',
innerHTML: `<iframe src="https://www.googletagmanager.com/ns.html?id=${containerId}" height="0" width="0" style="display:none;visibility:hidden"></iframe>`,
},
],
headTags: [
{
tagName: 'link',
attributes: {
rel: 'preconnect',
href: 'https://www.googletagmanager.com',
},
},
{
tagName: 'script',
innerHTML: `window.dataLayer = window.dataLayer || [];`,
},
{
tagName: 'script',
innerHTML: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','${containerId}');`,
},
],
};
},
};
}
const pluginOptionsSchema = Joi.object<PluginOptions>({
containerId: Joi.string().required(),
});
export function validateOptions({
validate,
options,
}: OptionValidationContext<Options, PluginOptions>): PluginOptions {
return validate(pluginOptionsSchema, options);
}
export type {PluginOptions, Options};

View file

@ -0,0 +1,12 @@
/**
* 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.
*/
export type PluginOptions = {
containerId: string;
};
export type Options = Partial<PluginOptions>;

View file

@ -0,0 +1,8 @@
/**
* 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/module-type-aliases" />

View file

@ -0,0 +1,15 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"noEmit": false,
"composite": true,
"incremental": true,
"tsBuildInfoFile": "./lib/.tsbuildinfo-client",
"module": "esnext",
"target": "esnext",
"rootDir": "src",
"outDir": "lib"
},
"include": ["src/*.d.ts"],
"exclude": ["**/__tests__/**"]
}

View file

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.json",
"references": [{"path": "./tsconfig.client.json"}],
"compilerOptions": {
"noEmit": false,
"incremental": true,
"tsBuildInfoFile": "./lib/.tsbuildinfo",
"rootDir": "src",
"outDir": "lib"
},
"include": ["src"],
"exclude": ["**/__tests__/**"]
}

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-ideal-image",
"version": "2.2.0",
"version": "2.3.0",
"description": "Docusaurus Plugin to generate an almost ideal image (responsive, lazy-loading, and low quality placeholder).",
"main": "lib/index.js",
"types": "src/plugin-ideal-image.d.ts",
@ -20,12 +20,12 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "2.2.0",
"@docusaurus/lqip-loader": "2.2.0",
"@docusaurus/core": "2.3.0",
"@docusaurus/lqip-loader": "2.3.0",
"@docusaurus/responsive-loader": "^1.7.0",
"@docusaurus/theme-translations": "2.2.0",
"@docusaurus/types": "2.2.0",
"@docusaurus/utils-validation": "2.2.0",
"@docusaurus/theme-translations": "2.3.0",
"@docusaurus/types": "2.3.0",
"@docusaurus/utils-validation": "2.3.0",
"@endiliey/react-ideal-image": "^0.0.11",
"react-waypoint": "^10.3.0",
"sharp": "^0.30.7",
@ -33,7 +33,7 @@
"webpack": "^5.73.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.2.0",
"@docusaurus/module-type-aliases": "2.3.0",
"fs-extra": "^10.1.0"
},
"peerDependencies": {

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-pwa",
"version": "2.2.0",
"version": "2.3.0",
"description": "Docusaurus Plugin to add PWA support.",
"main": "lib/index.js",
"types": "src/plugin-pwa.d.ts",
@ -22,12 +22,12 @@
"dependencies": {
"@babel/core": "^7.18.6",
"@babel/preset-env": "^7.18.6",
"@docusaurus/core": "2.2.0",
"@docusaurus/theme-common": "2.2.0",
"@docusaurus/theme-translations": "2.2.0",
"@docusaurus/types": "2.2.0",
"@docusaurus/utils": "2.2.0",
"@docusaurus/utils-validation": "2.2.0",
"@docusaurus/core": "2.3.0",
"@docusaurus/theme-common": "2.3.0",
"@docusaurus/theme-translations": "2.3.0",
"@docusaurus/types": "2.3.0",
"@docusaurus/utils": "2.3.0",
"@docusaurus/utils-validation": "2.3.0",
"babel-loader": "^8.2.5",
"clsx": "^1.2.1",
"core-js": "^3.23.3",
@ -40,7 +40,7 @@
"workbox-window": "^6.5.3"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.2.0",
"@docusaurus/module-type-aliases": "2.3.0",
"fs-extra": "^10.1.0"
},
"peerDependencies": {

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/plugin-sitemap",
"version": "2.2.0",
"version": "2.3.0",
"description": "Simple sitemap generation plugin for Docusaurus.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@ -18,12 +18,12 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "2.2.0",
"@docusaurus/logger": "2.2.0",
"@docusaurus/types": "2.2.0",
"@docusaurus/utils": "2.2.0",
"@docusaurus/utils-common": "2.2.0",
"@docusaurus/utils-validation": "2.2.0",
"@docusaurus/core": "2.3.0",
"@docusaurus/logger": "2.3.0",
"@docusaurus/types": "2.3.0",
"@docusaurus/utils": "2.3.0",
"@docusaurus/utils-common": "2.3.0",
"@docusaurus/utils-validation": "2.3.0",
"fs-extra": "^10.1.0",
"sitemap": "^7.1.1",
"tslib": "^2.4.0"

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/preset-classic",
"version": "2.2.0",
"version": "2.3.0",
"description": "Classic preset for Docusaurus.",
"main": "lib/index.js",
"types": "lib/index.d.ts",
@ -18,18 +18,19 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "2.2.0",
"@docusaurus/plugin-content-blog": "2.2.0",
"@docusaurus/plugin-content-docs": "2.2.0",
"@docusaurus/plugin-content-pages": "2.2.0",
"@docusaurus/plugin-debug": "2.2.0",
"@docusaurus/plugin-google-analytics": "2.2.0",
"@docusaurus/plugin-google-gtag": "2.2.0",
"@docusaurus/plugin-sitemap": "2.2.0",
"@docusaurus/theme-classic": "2.2.0",
"@docusaurus/theme-common": "2.2.0",
"@docusaurus/theme-search-algolia": "2.2.0",
"@docusaurus/types": "2.2.0"
"@docusaurus/core": "2.3.0",
"@docusaurus/plugin-content-blog": "2.3.0",
"@docusaurus/plugin-content-docs": "2.3.0",
"@docusaurus/plugin-content-pages": "2.3.0",
"@docusaurus/plugin-debug": "2.3.0",
"@docusaurus/plugin-google-analytics": "2.3.0",
"@docusaurus/plugin-google-gtag": "2.3.0",
"@docusaurus/plugin-google-tag-manager": "2.3.0",
"@docusaurus/plugin-sitemap": "2.3.0",
"@docusaurus/theme-classic": "2.3.0",
"@docusaurus/theme-common": "2.3.0",
"@docusaurus/theme-search-algolia": "2.3.0",
"@docusaurus/types": "2.3.0"
},
"peerDependencies": {
"react": "^16.8.4 || ^17.0.0",

View file

@ -40,6 +40,7 @@ export default function preset(
theme,
googleAnalytics,
gtag,
googleTagManager,
...rest
} = opts;
@ -80,6 +81,11 @@ export default function preset(
if (gtag) {
plugins.push(makePluginConfig('@docusaurus/plugin-google-gtag', gtag));
}
if (googleTagManager) {
plugins.push(
makePluginConfig('@docusaurus/plugin-google-gtag', googleTagManager),
);
}
if (isProd && sitemap !== false) {
plugins.push(makePluginConfig('@docusaurus/plugin-sitemap', sitemap));
}

View file

@ -11,6 +11,7 @@ import type {Options as PagesPluginOptions} from '@docusaurus/plugin-content-pag
import type {Options as SitemapPluginOptions} from '@docusaurus/plugin-sitemap';
import type {Options as GAPluginOptions} from '@docusaurus/plugin-google-analytics';
import type {Options as GtagPluginOptions} from '@docusaurus/plugin-google-gtag';
import type {Options as GTMPluginOptions} from '@docusaurus/plugin-google-tag-manager';
import type {Options as ThemeOptions} from '@docusaurus/theme-classic';
import type {ThemeConfig as BaseThemeConfig} from '@docusaurus/types';
import type {UserThemeConfig as ClassicThemeConfig} from '@docusaurus/theme-common';
@ -42,6 +43,7 @@ export type Options = {
* is present.
*/
gtag?: GtagPluginOptions;
googleTagManager?: GTMPluginOptions;
};
export type ThemeConfig = BaseThemeConfig &

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/remark-plugin-npm2yarn",
"version": "2.2.0",
"version": "2.3.0",
"description": "Remark plugin for converting npm commands to Yarn commands as tabs.",
"main": "lib/index.js",
"publishConfig": {

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/theme-classic",
"version": "2.2.0",
"version": "2.3.0",
"description": "Classic theme for Docusaurus",
"main": "lib/index.js",
"types": "src/theme-classic.d.ts",
@ -20,18 +20,18 @@
"copy:watch": "node ../../admin/scripts/copyUntypedFiles.js --watch"
},
"dependencies": {
"@docusaurus/core": "2.2.0",
"@docusaurus/mdx-loader": "2.2.0",
"@docusaurus/module-type-aliases": "2.2.0",
"@docusaurus/plugin-content-blog": "2.2.0",
"@docusaurus/plugin-content-docs": "2.2.0",
"@docusaurus/plugin-content-pages": "2.2.0",
"@docusaurus/theme-common": "2.2.0",
"@docusaurus/theme-translations": "2.2.0",
"@docusaurus/types": "2.2.0",
"@docusaurus/utils": "2.2.0",
"@docusaurus/utils-common": "2.2.0",
"@docusaurus/utils-validation": "2.2.0",
"@docusaurus/core": "2.3.0",
"@docusaurus/mdx-loader": "2.3.0",
"@docusaurus/module-type-aliases": "2.3.0",
"@docusaurus/plugin-content-blog": "2.3.0",
"@docusaurus/plugin-content-docs": "2.3.0",
"@docusaurus/plugin-content-pages": "2.3.0",
"@docusaurus/theme-common": "2.3.0",
"@docusaurus/theme-translations": "2.3.0",
"@docusaurus/types": "2.3.0",
"@docusaurus/utils": "2.3.0",
"@docusaurus/utils-common": "2.3.0",
"@docusaurus/utils-validation": "2.3.0",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"copy-text-to-clipboard": "^3.0.1",

View file

@ -62,7 +62,7 @@ describe('themeConfig', () => {
textColor: '#000',
isCloseable: true,
},
image: 'img/docusaurus-soc.png',
image: 'img/docusaurus-social-card.jpg',
navbar: {
style: 'primary',
hideOnScroll: true,

View file

@ -20,6 +20,14 @@ export default function getSwizzleConfig(): SwizzleConfig {
description:
'The component used to render multi-line code blocks, generally used in Markdown files.',
},
'CodeBlock/Content': {
actions: {
eject: 'unsafe',
wrap: 'forbidden',
},
description:
'The folder containing components responsible for rendering different types of CodeBlock content.',
},
ColorModeToggle: {
actions: {
eject: 'safe',
@ -28,6 +36,14 @@ export default function getSwizzleConfig(): SwizzleConfig {
description:
'The color mode toggle to switch between light and dark mode.',
},
'DocBreadcrumbs/Items': {
actions: {
eject: 'unsafe',
wrap: 'forbidden', // Can't wrap a folder
},
description:
'The components responsible for rendering the breadcrumb items',
},
DocCardList: {
actions: {
eject: 'safe',
@ -36,6 +52,17 @@ export default function getSwizzleConfig(): SwizzleConfig {
description:
'The component responsible for rendering a list of sidebar items cards.\nNotable used on the category generated-index pages.',
},
'DocItem/TOC': {
actions: {
// Forbidden because it's a parent folder, makes the CLI crash atm
// TODO the CLI should rather support --eject
// Subfolders can be swizzled
eject: 'forbidden',
wrap: 'forbidden',
},
description:
'The DocItem TOC is not directly swizzle-able, but you can swizzle its sub-components.',
},
DocSidebar: {
actions: {
eject: 'unsafe', // Too much technical code in sidebar, not very safe atm
@ -101,6 +128,17 @@ export default function getSwizzleConfig(): SwizzleConfig {
},
description: 'The footer logo',
},
Icon: {
actions: {
// Forbidden because it's a parent folder, makes the CLI crash atm
// TODO the CLI should rather support --eject
// Subfolders can be swizzled
eject: 'forbidden',
wrap: 'forbidden',
},
description:
'The Icon folder is not directly swizzle-able, but you can swizzle its sub-components.',
},
'Icon/Arrow': {
actions: {
eject: 'safe',
@ -220,7 +258,7 @@ export default function getSwizzleConfig(): SwizzleConfig {
wrap: 'forbidden',
},
description:
'The Navbar item components mapping. Can be ejected to add custom navbar item types. See https://github.com/facebook/docusaurus/issues/7227.',
'The Navbar item components mapping. Can be ejected to add custom navbar item types.\nSee https://github.com/facebook/docusaurus/issues/7227.',
},
NotFound: {
actions: {

View file

@ -1123,38 +1123,17 @@ declare module '@theme/Mermaid' {
}
declare module '@theme/TabItem' {
import type {ReactNode} from 'react';
import type {TabItemProps} from '@docusaurus/theme-common/internal';
export interface Props {
readonly children: ReactNode;
readonly value: string;
readonly default?: boolean;
readonly label?: string;
readonly hidden?: boolean;
readonly className?: string;
readonly attributes?: {[key: string]: unknown};
}
export interface Props extends TabItemProps {}
export default function TabItem(props: Props): JSX.Element;
}
declare module '@theme/Tabs' {
import type {ReactElement} from 'react';
import type {Props as TabItemProps} from '@theme/TabItem';
import type {TabsProps} from '@docusaurus/theme-common/internal';
export interface Props {
readonly lazy?: boolean;
readonly block?: boolean;
readonly children: readonly ReactElement<TabItemProps>[];
readonly defaultValue?: string | null;
readonly values?: readonly {
value: string;
label?: string;
attributes?: {[key: string]: unknown};
}[];
readonly groupId?: string;
readonly className?: string;
}
export interface Props extends TabsProps {}
export default function Tabs(props: Props): JSX.Element;
}
@ -1392,3 +1371,7 @@ declare module '@theme/prism-include-languages' {
PrismObject: typeof PrismNamespace,
): void;
}
declare module '@theme/DocBreadcrumbs/Items/Home' {
export default function HomeBreadcrumbItem(): JSX.Element;
}

View file

@ -0,0 +1,33 @@
/**
* 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 Link from '@docusaurus/Link';
import useBaseUrl from '@docusaurus/useBaseUrl';
import {translate} from '@docusaurus/Translate';
import IconHome from '@theme/Icon/Home';
import styles from './styles.module.css';
export default function HomeBreadcrumbItem(): JSX.Element {
const homeHref = useBaseUrl('/');
return (
<li className="breadcrumbs__item">
<Link
aria-label={translate({
id: 'theme.docs.breadcrumbs.home',
message: 'Home page',
description: 'The ARIA label for the home page in the breadcrumbs',
})}
className="breadcrumbs__link"
href={homeHref}>
<IconHome className={styles.breadcrumbHomeIcon} />
</Link>
</li>
);
}

View file

@ -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.
*/
.breadcrumbHomeIcon {
position: relative;
top: 1px;
vertical-align: top;
height: 1.1rem;
width: 1.1rem;
}

View file

@ -13,9 +13,8 @@ import {
useHomePageRoute,
} from '@docusaurus/theme-common/internal';
import Link from '@docusaurus/Link';
import useBaseUrl from '@docusaurus/useBaseUrl';
import {translate} from '@docusaurus/Translate';
import IconHome from '@theme/Icon/Home';
import HomeBreadcrumbItem from '@theme/DocBreadcrumbs/Items/Home';
import styles from './styles.module.css';
@ -79,24 +78,6 @@ function BreadcrumbsItem({
);
}
function HomeBreadcrumbItem() {
const homeHref = useBaseUrl('/');
return (
<li className="breadcrumbs__item">
<Link
aria-label={translate({
id: 'theme.docs.breadcrumbs.home',
message: 'Home page',
description: 'The ARIA label for the home page in the breadcrumbs',
})}
className={clsx('breadcrumbs__link', styles.breadcrumbsItemLink)}
href={homeHref}>
<IconHome className={styles.breadcrumbHomeIcon} />
</Link>
</li>
);
}
export default function DocBreadcrumbs(): JSX.Element | null {
const breadcrumbs = useSidebarBreadcrumbs();
const homePageRoute = useHomePageRoute();

View file

@ -9,11 +9,3 @@
--ifm-breadcrumb-size-multiplier: 0.8;
margin-bottom: 0.8rem;
}
.breadcrumbHomeIcon {
position: relative;
top: 1px;
vertical-align: top;
height: 1.1rem;
width: 1.1rem;
}

View file

@ -7,10 +7,11 @@
@media (min-width: 997px) {
.expandButton {
position: sticky;
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 100%;
max-height: 100vh;
display: flex;
align-items: center;
justify-content: center;

View file

@ -60,15 +60,20 @@ export default function DocPageLayoutSidebar({
}
}}>
<ResetOnSidebarChange>
<DocSidebar
sidebar={sidebar}
path={pathname}
onCollapse={toggleSidebar}
isHidden={hiddenSidebar}
/>
<div
className={clsx(
styles.sidebarViewport,
hiddenSidebar && styles.sidebarViewportHidden,
)}>
<DocSidebar
sidebar={sidebar}
path={pathname}
onCollapse={toggleSidebar}
isHidden={hiddenSidebar}
/>
{hiddenSidebar && <ExpandButton toggleSidebar={toggleSidebar} />}
</div>
</ResetOnSidebarChange>
{hiddenSidebar && <ExpandButton toggleSidebar={toggleSidebar} />}
</aside>
);
}

View file

@ -29,4 +29,11 @@
width: var(--doc-sidebar-hidden-width);
cursor: pointer;
}
.sidebarViewport {
top: 0;
position: sticky;
height: 100%;
max-height: 100vh;
}
}

View file

@ -12,4 +12,5 @@
.docsWrapper {
display: flex;
flex: 1 0 auto;
}

View file

@ -43,4 +43,5 @@
.collapseSidebarButton {
display: none;
margin: 0;
}

View file

@ -12,6 +12,7 @@ import {
useAnnouncementBar,
useScrollPosition,
} from '@docusaurus/theme-common/internal';
import {translate} from '@docusaurus/Translate';
import DocSidebarItems from '@theme/DocSidebarItems';
import type {Props} from '@theme/DocSidebar/Desktop/Content';
@ -41,6 +42,11 @@ export default function DocSidebarDesktopContent({
return (
<nav
aria-label={translate({
id: 'theme.docs.sidebar.navAriaLabel',
message: 'Docs sidebar',
description: 'The ARIA label for the sidebar navigation',
})}
className={clsx(
'menu thin-scrollbar',
styles.menu,

View file

@ -9,13 +9,9 @@
.sidebar {
display: flex;
flex-direction: column;
max-height: 100vh;
height: 100%;
position: sticky;
top: 0;
padding-top: var(--ifm-navbar-height);
width: var(--doc-sidebar-width);
transition: opacity 50ms ease;
}
.sidebarWithHideableNavbar {
@ -24,8 +20,6 @@
.sidebarHidden {
opacity: 0;
height: 0;
overflow: hidden;
visibility: hidden;
}

View file

@ -9,6 +9,7 @@ import React from 'react';
import clsx from 'clsx';
import {translate} from '@docusaurus/Translate';
import {useThemeConfig} from '@docusaurus/theme-common';
import Link from '@docusaurus/Link';
import type {Props} from '@theme/Heading';
import styles from './styles.module.css';
@ -22,6 +23,17 @@ export default function Heading({as: As, id, ...props}: Props): JSX.Element {
return <As {...props} id={undefined} />;
}
const anchorTitle = translate(
{
id: 'theme.common.headingLinkTitle',
message: 'Direct link to {heading}',
description: 'Title for link to heading',
},
{
heading: typeof props.children === 'string' ? props.children : id,
},
);
return (
<As
{...props}
@ -30,19 +42,17 @@ export default function Heading({as: As, id, ...props}: Props): JSX.Element {
hideOnScroll
? styles.anchorWithHideOnScrollNavbar
: styles.anchorWithStickyNavbar,
props.className,
)}
id={id}>
{props.children}
<a
<Link
className="hash-link"
href={`#${id}`}
title={translate({
id: 'theme.common.headingLinkTitle',
message: 'Direct link to heading',
description: 'Title for link to heading',
})}>
to={`#${id}`}
aria-label={anchorTitle}
title={anchorTitle}>
&#8203;
</a>
</Link>
</As>
);
}

View file

@ -9,7 +9,6 @@ import React from 'react';
import {composeProviders} from '@docusaurus/theme-common';
import {
ColorModeProvider,
TabGroupChoiceProvider,
AnnouncementBarProvider,
DocsPreferredVersionContextProvider,
ScrollControllerProvider,
@ -21,7 +20,6 @@ import type {Props} from '@theme/Layout/Provider';
const Provider = composeProviders([
ColorModeProvider,
AnnouncementBarProvider,
TabGroupChoiceProvider,
ScrollControllerProvider,
DocsPreferredVersionContextProvider,
PluginHtmlClassNameProvider,

View file

@ -12,6 +12,8 @@ body {
.mainWrapper {
flex: 1 0 auto;
display: flex;
flex-direction: column;
}
/* Docusaurus-specific utility class */

View file

@ -12,6 +12,7 @@ import {
useHideableNavbar,
useNavbarMobileSidebar,
} from '@docusaurus/theme-common/internal';
import {translate} from '@docusaurus/Translate';
import NavbarMobileSidebar from '@theme/Navbar/MobileSidebar';
import type {Props} from '@theme/Navbar/Layout';
@ -36,6 +37,11 @@ export default function NavbarLayout({children}: Props): JSX.Element {
return (
<nav
ref={navbarRef}
aria-label={translate({
id: 'theme.NavBar.navAriaLabel',
message: 'Main',
description: 'The ARIA label for the main navigation',
})}
className={clsx(
'navbar',
'navbar--fixed-top',

View file

@ -5,23 +5,37 @@
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import React, {type ReactNode} from 'react';
import renderer from 'react-test-renderer';
import {
TabGroupChoiceProvider,
ScrollControllerProvider,
} from '@docusaurus/theme-common/internal';
import {ScrollControllerProvider} from '@docusaurus/theme-common/internal';
import {StaticRouter} from 'react-router-dom';
import Tabs from '../index';
import TabItem from '../../TabItem';
function TestProviders({
children,
pathname = '/',
}: {
children: ReactNode;
pathname?: string;
}) {
return (
<StaticRouter location={{pathname}}>
<ScrollControllerProvider>{children}</ScrollControllerProvider>
</StaticRouter>
);
}
describe('Tabs', () => {
it('rejects bad Tabs child', () => {
expect(() => {
renderer.create(
<Tabs>
<div>Naughty</div>
<TabItem value="good">Good</TabItem>
</Tabs>,
<TestProviders>
<Tabs>
<div>Naughty</div>
<TabItem value="good">Good</TabItem>
</Tabs>
</TestProviders>,
);
}).toThrowErrorMatchingInlineSnapshot(
`"Docusaurus error: Bad <Tabs> child <div>: all children of the <Tabs> component should be <TabItem>, and every <TabItem> should have a unique "value" prop."`,
@ -30,10 +44,12 @@ describe('Tabs', () => {
it('rejects bad Tabs defaultValue', () => {
expect(() => {
renderer.create(
<Tabs defaultValue="bad">
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2">Tab 2</TabItem>
</Tabs>,
<TestProviders>
<Tabs defaultValue="bad">
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2">Tab 2</TabItem>
</Tabs>
</TestProviders>,
);
}).toThrowErrorMatchingInlineSnapshot(
`"Docusaurus error: The <Tabs> has a defaultValue "bad" but none of its children has the corresponding value. Available values are: v1, v2. If you intend to show no default tab, use defaultValue={null} instead."`,
@ -42,14 +58,16 @@ describe('Tabs', () => {
it('rejects duplicate values', () => {
expect(() => {
renderer.create(
<Tabs>
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2">Tab 2</TabItem>
<TabItem value="v3">Tab 3</TabItem>
<TabItem value="v4">Tab 4</TabItem>
<TabItem value="v1">Tab 5</TabItem>
<TabItem value="v2">Tab 6</TabItem>
</Tabs>,
<TestProviders>
<Tabs>
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2">Tab 2</TabItem>
<TabItem value="v3">Tab 3</TabItem>
<TabItem value="v4">Tab 4</TabItem>
<TabItem value="v1">Tab 5</TabItem>
<TabItem value="v2">Tab 6</TabItem>
</Tabs>
</TestProviders>,
);
}).toThrowErrorMatchingInlineSnapshot(
`"Docusaurus error: Duplicate values "v1, v2" found in <Tabs>. Every value needs to be unique."`,
@ -58,54 +76,52 @@ describe('Tabs', () => {
it('accepts valid Tabs config', () => {
expect(() => {
renderer.create(
<ScrollControllerProvider>
<TabGroupChoiceProvider>
<Tabs>
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2">Tab 2</TabItem>
</Tabs>
<Tabs>
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2" default>
Tab 2
</TabItem>
</Tabs>
<Tabs defaultValue="v1">
<TabItem value="v1" label="V1">
Tab 1
</TabItem>
<TabItem value="v2" label="V2">
Tab 2
</TabItem>
</Tabs>
<Tabs
defaultValue="v1"
values={[
{value: 'v1', label: 'V1'},
{value: 'v2', label: 'V2'},
]}>
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2">Tab 2</TabItem>
</Tabs>
<Tabs
defaultValue={null}
values={[
{value: 'v1', label: 'V1'},
{value: 'v2', label: 'V2'},
]}>
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2">Tab 2</TabItem>
</Tabs>
<Tabs defaultValue={null}>
<TabItem value="v1" label="V1">
Tab 1
</TabItem>
<TabItem value="v2" label="V2">
Tab 2
</TabItem>
</Tabs>
</TabGroupChoiceProvider>
</ScrollControllerProvider>,
<TestProviders>
<Tabs>
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2">Tab 2</TabItem>
</Tabs>
<Tabs>
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2" default>
Tab 2
</TabItem>
</Tabs>
<Tabs defaultValue="v1">
<TabItem value="v1" label="V1">
Tab 1
</TabItem>
<TabItem value="v2" label="V2">
Tab 2
</TabItem>
</Tabs>
<Tabs
defaultValue="v1"
values={[
{value: 'v1', label: 'V1'},
{value: 'v2', label: 'V2'},
]}>
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2">Tab 2</TabItem>
</Tabs>
<Tabs
defaultValue={null}
values={[
{value: 'v1', label: 'V1'},
{value: 'v2', label: 'V2'},
]}>
<TabItem value="v1">Tab 1</TabItem>
<TabItem value="v2">Tab 2</TabItem>
</Tabs>
<Tabs defaultValue={null}>
<TabItem value="v1" label="V1">
Tab 1
</TabItem>
<TabItem value="v2" label="V2">
Tab 2
</TabItem>
</Tabs>
</TestProviders>,
);
}).not.toThrow(); // TODO Better Jest infrastructure to mock the Layout
});
@ -114,22 +130,60 @@ describe('Tabs', () => {
expect(() => {
const tabs = ['Apple', 'Banana', 'Carrot'];
renderer.create(
<ScrollControllerProvider>
<TabGroupChoiceProvider>
<Tabs
<TestProviders>
<Tabs
// @ts-expect-error: for an edge-case that we didn't write types for
values={tabs.map((t, idx) => ({label: t, value: idx}))}
// @ts-expect-error: for an edge-case that we didn't write types for
defaultValue={0}>
{tabs.map((t, idx) => (
// @ts-expect-error: for an edge-case that we didn't write types for
values={tabs.map((t, idx) => ({label: t, value: idx}))}
// @ts-expect-error: for an edge-case that we didn't write types for
defaultValue={0}>
{tabs.map((t, idx) => (
// @ts-expect-error: for an edge-case that we didn't write types for
<TabItem key={idx} value={idx}>
{t}
</TabItem>
))}
</Tabs>
</TabGroupChoiceProvider>
</ScrollControllerProvider>,
<TabItem key={idx} value={idx}>
{t}
</TabItem>
))}
</Tabs>
</TestProviders>,
);
}).not.toThrow();
});
it('rejects if querystring is true, but groupId falsy', () => {
expect(() => {
renderer.create(
<TestProviders>
<Tabs queryString>
<TabItem value="val1">Val1</TabItem>
<TabItem value="val2">Val2</TabItem>
</Tabs>
</TestProviders>,
);
}).toThrow(
'Docusaurus error: The <Tabs> component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".',
);
});
it('accept querystring=true when groupId is defined', () => {
expect(() => {
renderer.create(
<TestProviders>
<Tabs queryString groupId="my-group-id">
<TabItem value="val1">Val1</TabItem>
<TabItem value="val2">Val2</TabItem>
</Tabs>
</TestProviders>,
);
}).not.toThrow();
});
it('accept querystring as string, but groupId falsy', () => {
expect(() => {
renderer.create(
<TestProviders>
<Tabs queryString="qsKey">
<TabItem value="val1">Val1</TabItem>
<TabItem value="val2">Val2</TabItem>
</Tabs>
</TestProviders>,
);
}).not.toThrow();
});

View file

@ -5,104 +5,27 @@
* LICENSE file in the root directory of this source tree.
*/
import React, {
useState,
cloneElement,
isValidElement,
type ReactElement,
} from 'react';
import React, {cloneElement} from 'react';
import clsx from 'clsx';
import useIsBrowser from '@docusaurus/useIsBrowser';
import {duplicates} from '@docusaurus/theme-common';
import {
useScrollPositionBlocker,
useTabGroupChoice,
useTabs,
} from '@docusaurus/theme-common/internal';
import useIsBrowser from '@docusaurus/useIsBrowser';
import type {Props} from '@theme/Tabs';
import type {Props as TabItemProps} from '@theme/TabItem';
import styles from './styles.module.css';
// A very rough duck type, but good enough to guard against mistakes while
// allowing customization
function isTabItem(
comp: ReactElement<object>,
): comp is ReactElement<TabItemProps> {
return 'value' in comp.props;
}
function TabsComponent(props: Props): JSX.Element {
const {
lazy,
block,
defaultValue: defaultValueProp,
values: valuesProp,
groupId,
className,
} = props;
const children = React.Children.map(props.children, (child) => {
if (isValidElement(child) && isTabItem(child)) {
return child;
}
// child.type.name will give non-sensical values in prod because of
// minification, but we assume it won't throw in prod.
throw new Error(
`Docusaurus error: Bad <Tabs> child <${
// @ts-expect-error: guarding against unexpected cases
typeof child.type === 'string' ? child.type : child.type.name
}>: all children of the <Tabs> component should be <TabItem>, and every <TabItem> should have a unique "value" prop.`,
);
});
const values =
valuesProp ??
// Only pick keys that we recognize. MDX would inject some keys by default
children.map(({props: {value, label, attributes}}) => ({
value,
label,
attributes,
}));
const dup = duplicates(values, (a, b) => a.value === b.value);
if (dup.length > 0) {
throw new Error(
`Docusaurus error: Duplicate values "${dup
.map((a) => a.value)
.join(', ')}" found in <Tabs>. Every value needs to be unique.`,
);
}
// When defaultValueProp is null, don't show a default tab
const defaultValue =
defaultValueProp === null
? defaultValueProp
: defaultValueProp ??
children.find((child) => child.props.default)?.props.value ??
children[0]!.props.value;
if (defaultValue !== null && !values.some((a) => a.value === defaultValue)) {
throw new Error(
`Docusaurus error: The <Tabs> has a defaultValue "${defaultValue}" but none of its children has the corresponding value. Available values are: ${values
.map((a) => a.value)
.join(
', ',
)}. If you intend to show no default tab, use defaultValue={null} instead.`,
);
}
const {tabGroupChoices, setTabGroupChoices} = useTabGroupChoice();
const [selectedValue, setSelectedValue] = useState(defaultValue);
function TabList({
className,
block,
selectedValue,
selectValue,
tabValues,
}: Props & ReturnType<typeof useTabs>) {
const tabRefs: (HTMLLIElement | null)[] = [];
const {blockElementScrollPositionUntilNextRender} =
useScrollPositionBlocker();
if (groupId != null) {
const relevantTabGroupChoice = tabGroupChoices[groupId];
if (
relevantTabGroupChoice != null &&
relevantTabGroupChoice !== selectedValue &&
values.some((value) => value.value === relevantTabGroupChoice)
) {
setSelectedValue(relevantTabGroupChoice);
}
}
const handleTabChange = (
event:
| React.FocusEvent<HTMLLIElement>
@ -111,15 +34,11 @@ function TabsComponent(props: Props): JSX.Element {
) => {
const newTab = event.currentTarget;
const newTabIndex = tabRefs.indexOf(newTab);
const newTabValue = values[newTabIndex]!.value;
const newTabValue = tabValues[newTabIndex]!.value;
if (newTabValue !== selectedValue) {
blockElementScrollPositionUntilNextRender(newTab);
setSelectedValue(newTabValue);
if (groupId != null) {
setTabGroupChoices(groupId, String(newTabValue));
}
selectValue(newTabValue);
}
};
@ -149,61 +68,79 @@ function TabsComponent(props: Props): JSX.Element {
};
return (
<div className={clsx('tabs-container', styles.tabList)}>
<ul
role="tablist"
aria-orientation="horizontal"
className={clsx(
'tabs',
{
'tabs--block': block,
},
className,
)}>
{values.map(({value, label, attributes}) => (
<li
role="tab"
tabIndex={selectedValue === value ? 0 : -1}
aria-selected={selectedValue === value}
key={value}
ref={(tabControl) => tabRefs.push(tabControl)}
onKeyDown={handleKeydown}
onClick={handleTabChange}
{...attributes}
className={clsx(
'tabs__item',
styles.tabItem,
attributes?.className as string,
{
'tabs__item--active': selectedValue === value,
},
)}>
{label ?? value}
</li>
))}
</ul>
<ul
role="tablist"
aria-orientation="horizontal"
className={clsx(
'tabs',
{
'tabs--block': block,
},
className,
)}>
{tabValues.map(({value, label, attributes}) => (
<li
// TODO extract TabListItem
role="tab"
tabIndex={selectedValue === value ? 0 : -1}
aria-selected={selectedValue === value}
key={value}
ref={(tabControl) => tabRefs.push(tabControl)}
onKeyDown={handleKeydown}
onClick={handleTabChange}
{...attributes}
className={clsx(
'tabs__item',
styles.tabItem,
attributes?.className as string,
{
'tabs__item--active': selectedValue === value,
},
)}>
{label ?? value}
</li>
))}
</ul>
);
}
{lazy ? (
cloneElement(
children.filter(
(tabItem) => tabItem.props.value === selectedValue,
)[0]!,
{className: 'margin-top--md'},
)
) : (
<div className="margin-top--md">
{children.map((tabItem, i) =>
cloneElement(tabItem, {
key: i,
hidden: tabItem.props.value !== selectedValue,
}),
)}
</div>
function TabContent({
lazy,
children,
selectedValue,
}: Props & ReturnType<typeof useTabs>) {
if (lazy) {
const selectedTabItem = children.find(
(tabItem) => tabItem.props.value === selectedValue,
);
if (!selectedTabItem) {
// fail-safe or fail-fast? not sure what's best here
return null;
}
return cloneElement(selectedTabItem, {className: 'margin-top--md'});
}
return (
<div className="margin-top--md">
{children.map((tabItem, i) =>
cloneElement(tabItem, {
key: i,
hidden: tabItem.props.value !== selectedValue,
}),
)}
</div>
);
}
function TabsComponent(props: Props): JSX.Element {
const tabs = useTabs(props);
return (
<div className={clsx('tabs-container', styles.tabList)}>
<TabList {...props} {...tabs} />
<TabContent {...props} {...tabs} />
</div>
);
}
export default function Tabs(props: Props): JSX.Element {
const isBrowser = useIsBrowser();
return (

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/theme-common",
"version": "2.2.0",
"version": "2.3.0",
"description": "Common code for Docusaurus themes.",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
@ -30,12 +30,12 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/mdx-loader": "2.2.0",
"@docusaurus/module-type-aliases": "2.2.0",
"@docusaurus/plugin-content-blog": "2.2.0",
"@docusaurus/plugin-content-docs": "2.2.0",
"@docusaurus/plugin-content-pages": "2.2.0",
"@docusaurus/utils": "2.2.0",
"@docusaurus/mdx-loader": "2.3.0",
"@docusaurus/module-type-aliases": "2.3.0",
"@docusaurus/plugin-content-blog": "2.3.0",
"@docusaurus/plugin-content-docs": "2.3.0",
"@docusaurus/plugin-content-pages": "2.3.0",
"@docusaurus/utils": "2.3.0",
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router-config": "*",
@ -43,11 +43,12 @@
"parse-numeric-range": "^1.3.0",
"prism-react-renderer": "^1.3.5",
"tslib": "^2.4.0",
"use-sync-external-store": "^1.2.0",
"utility-types": "^3.10.0"
},
"devDependencies": {
"@docusaurus/core": "2.2.0",
"@docusaurus/types": "2.2.0",
"@docusaurus/core": "2.3.0",
"@docusaurus/types": "2.3.0",
"fs-extra": "^10.1.0",
"lodash": "^4.17.21"
},

View file

@ -1,85 +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, {
useState,
useCallback,
useEffect,
useMemo,
useContext,
type ReactNode,
} from 'react';
import {createStorageSlot, listStorageKeys} from '../utils/storageUtils';
import {ReactContextError} from '../utils/reactUtils';
const TAB_CHOICE_PREFIX = 'docusaurus.tab.';
type ContextValue = {
/** A map from `groupId` to the `value` of the saved choice. */
readonly tabGroupChoices: {readonly [groupId: string]: string};
/** Set the new choice value of a group. */
readonly setTabGroupChoices: (groupId: string, newChoice: string) => void;
};
const Context = React.createContext<ContextValue | undefined>(undefined);
function useContextValue(): ContextValue {
const [tabGroupChoices, setChoices] = useState<{
readonly [groupId: string]: string;
}>({});
const setChoiceSyncWithLocalStorage = useCallback(
(groupId: string, newChoice: string) => {
createStorageSlot(`${TAB_CHOICE_PREFIX}${groupId}`).set(newChoice);
},
[],
);
useEffect(() => {
try {
const localStorageChoices: {[groupId: string]: string} = {};
listStorageKeys().forEach((storageKey) => {
if (storageKey.startsWith(TAB_CHOICE_PREFIX)) {
const groupId = storageKey.substring(TAB_CHOICE_PREFIX.length);
localStorageChoices[groupId] = createStorageSlot(storageKey).get()!;
}
});
setChoices(localStorageChoices);
} catch (err) {
console.error(err);
}
}, []);
const setTabGroupChoices = useCallback(
(groupId: string, newChoice: string) => {
setChoices((oldChoices) => ({...oldChoices, [groupId]: newChoice}));
setChoiceSyncWithLocalStorage(groupId, newChoice);
},
[setChoiceSyncWithLocalStorage],
);
return useMemo(
() => ({tabGroupChoices, setTabGroupChoices}),
[tabGroupChoices, setTabGroupChoices],
);
}
export function TabGroupChoiceProvider({
children,
}: {
children: ReactNode;
}): JSX.Element {
const value = useContextValue();
return <Context.Provider value={value}>{children}</Context.Provider>;
}
export function useTabGroupChoice(): ContextValue {
const context = useContext(Context);
if (context == null) {
throw new ReactContextError('TabGroupChoiceProvider');
}
return context;
}

View file

@ -8,6 +8,7 @@
import {useCallback, useEffect, useState} from 'react';
import {useHistory} from '@docusaurus/router';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import type {ThemeConfig as AlgoliaThemeConfig} from '@docusaurus/theme-search-algolia';
const SEARCH_PARAM_QUERY = 'q';
@ -31,8 +32,11 @@ export function useSearchPage(): {
} {
const history = useHistory();
const {
siteConfig: {baseUrl},
siteConfig: {baseUrl, themeConfig},
} = useDocusaurusContext();
const {
algolia: {searchPagePath},
} = themeConfig as AlgoliaThemeConfig;
const [searchQuery, setSearchQueryState] = useState('');
@ -65,10 +69,11 @@ export function useSearchPage(): {
const generateSearchPageLink = useCallback(
(targetSearchQuery: string) =>
// Refer to https://github.com/facebook/docusaurus/pull/2838
`${baseUrl}search?${SEARCH_PARAM_QUERY}=${encodeURIComponent(
targetSearchQuery,
)}`,
[baseUrl],
// Note: if searchPagePath is falsy, useSearchPage() will not be called
`${baseUrl}${
searchPagePath as string
}?${SEARCH_PARAM_QUERY}=${encodeURIComponent(targetSearchQuery)}`,
[baseUrl, searchPagePath],
);
return {

View file

@ -24,7 +24,11 @@ export {
type ColorModeConfig,
} from './utils/useThemeConfig';
export {createStorageSlot, listStorageKeys} from './utils/storageUtils';
export {
createStorageSlot,
useStorageSlot,
listStorageKeys,
} from './utils/storageUtils';
export {useContextualSearchFilters} from './utils/searchUtils';

View file

@ -42,10 +42,8 @@ export {
useAnnouncementBar,
} from './contexts/announcementBar';
export {
useTabGroupChoice,
TabGroupChoiceProvider,
} from './contexts/tabGroupChoice';
export {useTabs} from './utils/tabsUtils';
export type {TabValue, TabsProps, TabItemProps} from './utils/tabsUtils';
export {useNavbarMobileSidebar} from './contexts/navbarMobileSidebar';
export {useNavbarSecondaryMenu} from './contexts/navbarSecondaryMenu/display';
@ -82,7 +80,11 @@ export {useLocationChange} from './utils/useLocationChange';
export {useLocalPathname} from './utils/useLocalPathname';
export {useHistoryPopHandler} from './utils/historyUtils';
export {
useHistoryPopHandler,
useHistorySelector,
useQueryStringValue,
} from './utils/historyUtils';
export {
useFilteredAndTreeifiedTOC,

View file

@ -7,8 +7,11 @@
import {useEffect} from 'react';
import {useHistory} from '@docusaurus/router';
// @ts-expect-error: TODO temporary until React 18 upgrade
import {useSyncExternalStore} from 'use-sync-external-store/shim';
import {useEvent} from './reactUtils';
import type {Location, Action} from 'history';
import type {History, Location, Action} from 'history';
type HistoryBlockHandler = (location: Location, action: Action) => void | false;
@ -43,3 +46,28 @@ export function useHistoryPopHandler(handler: HistoryBlockHandler): void {
return undefined;
});
}
/**
* Permits to efficiently subscribe to a slice of the history
* See https://thisweekinreact.com/articles/useSyncExternalStore-the-underrated-react-api
* @param selector
*/
export function useHistorySelector<Value>(
selector: (history: History<unknown>) => Value,
): Value {
const history = useHistory();
return useSyncExternalStore(history.listen, () => selector(history));
}
/**
* Permits to efficiently subscribe to a specific querystring value
* @param key
*/
export function useQueryStringValue(key: string | null): string | null {
return useHistorySelector((history) => {
if (key === null) {
return null;
}
return new URLSearchParams(history.location.search).get(key);
});
}

View file

@ -5,12 +5,46 @@
* LICENSE file in the root directory of this source tree.
*/
import {useCallback, useRef} from 'react';
// @ts-expect-error: TODO temp error until React 18 upgrade
import {useSyncExternalStore} from 'use-sync-external-store/shim';
const StorageTypes = ['localStorage', 'sessionStorage', 'none'] as const;
export type StorageType = typeof StorageTypes[number];
const DefaultStorageType: StorageType = 'localStorage';
// window.addEventListener('storage') only works for different windows...
// so for current window we have to dispatch the event manually
// Now we can listen for both cross-window / current-window storage changes!
// see https://stackoverflow.com/a/71177640/82609
// see https://stackoverflow.com/questions/26974084/listen-for-changes-with-localstorage-on-the-same-window
function dispatchChangeEvent({
key,
oldValue,
newValue,
storage,
}: {
key: string;
oldValue: string | null;
newValue: string | null;
storage: Storage;
}) {
const event = document.createEvent('StorageEvent');
event.initStorageEvent(
'storage',
false,
false,
key,
oldValue,
newValue,
window.location.href,
storage,
);
window.dispatchEvent(event);
}
/**
* Will return `null` if browser storage is unavailable (like running Docusaurus
* in an iframe). This should NOT be called in SSR.
@ -58,12 +92,14 @@ export type StorageSlot = {
get: () => string | null;
set: (value: string) => void;
del: () => void;
listen: (onChange: (event: StorageEvent) => void) => () => void;
};
const NoopStorageSlot: StorageSlot = {
get: () => null,
set: () => {},
del: () => {},
listen: () => () => {},
};
// Fail-fast, as storage APIs should not be used during the SSR process
@ -78,6 +114,7 @@ Please only call storage APIs in effects and event handlers.`);
get: throwError,
set: throwError,
del: throwError,
listen: throwError,
};
}
@ -98,39 +135,103 @@ export function createStorageSlot(
if (typeof window === 'undefined') {
return createServerStorageSlot(key);
}
const browserStorage = getBrowserStorage(options?.persistence);
if (browserStorage === null) {
const storage = getBrowserStorage(options?.persistence);
if (storage === null) {
return NoopStorageSlot;
}
return {
get: () => {
try {
return browserStorage.getItem(key);
return storage.getItem(key);
} catch (err) {
console.error(`Docusaurus storage error, can't get key=${key}`, err);
return null;
}
},
set: (value) => {
set: (newValue) => {
try {
browserStorage.setItem(key, value);
const oldValue = storage.getItem(key);
storage.setItem(key, newValue);
dispatchChangeEvent({
key,
oldValue,
newValue,
storage,
});
} catch (err) {
console.error(
`Docusaurus storage error, can't set ${key}=${value}`,
`Docusaurus storage error, can't set ${key}=${newValue}`,
err,
);
}
},
del: () => {
try {
browserStorage.removeItem(key);
const oldValue = storage.getItem(key);
storage.removeItem(key);
dispatchChangeEvent({key, oldValue, newValue: null, storage});
} catch (err) {
console.error(`Docusaurus storage error, can't delete key=${key}`, err);
}
},
listen: (onChange) => {
try {
const listener = (event: StorageEvent) => {
if (event.storageArea === storage && event.key === key) {
onChange(event);
}
};
window.addEventListener('storage', listener);
return () => window.removeEventListener('storage', listener);
} catch (err) {
console.error(
`Docusaurus storage error, can't listen for changes of key=${key}`,
err,
);
return () => {};
}
},
};
}
export function useStorageSlot(
key: string | null,
options?: {persistence?: StorageType},
): [string | null, StorageSlot] {
// Not ideal but good enough: assumes storage slot config is constant
const storageSlot = useRef(() => {
if (key === null) {
return NoopStorageSlot;
}
return createStorageSlot(key, options);
}).current();
const listen: StorageSlot['listen'] = useCallback(
(onChange) => {
// Do not try to add a listener during SSR
if (typeof window === 'undefined') {
return () => {};
}
return storageSlot.listen(onChange);
},
[storageSlot],
);
const currentValue = useSyncExternalStore(
listen,
() => {
// TODO this check should be useless after React 18
if (typeof window === 'undefined') {
return null;
}
return storageSlot.get();
},
() => null,
);
return [currentValue, storageSlot];
}
/**
* Returns a list of all the keys currently stored in browser storage,
* or an empty list if browser storage can't be accessed.

View file

@ -0,0 +1,266 @@
/**
* 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, {
isValidElement,
useCallback,
useEffect,
useState,
useMemo,
type ReactNode,
type ReactElement,
} from 'react';
import {useHistory} from '@docusaurus/router';
import {useQueryStringValue} from '@docusaurus/theme-common/internal';
import {duplicates, useStorageSlot} from '../index';
/**
* TabValue is the "config" of a given Tab
* Provided through <Tabs> "values" prop or through the children <TabItem> props
*/
export interface TabValue {
readonly value: string;
readonly label?: string;
readonly attributes?: {[key: string]: unknown};
readonly default?: boolean;
}
export interface TabsProps {
readonly lazy?: boolean;
readonly block?: boolean;
readonly children: readonly ReactElement<TabItemProps>[];
readonly defaultValue?: string | null;
readonly values?: readonly TabValue[];
readonly groupId?: string;
readonly className?: string;
readonly queryString?: string | boolean;
}
export interface TabItemProps {
readonly children: ReactNode;
readonly value: string;
readonly default?: boolean;
readonly label?: string;
readonly hidden?: boolean;
readonly className?: string;
readonly attributes?: {[key: string]: unknown};
}
// A very rough duck type, but good enough to guard against mistakes while
// allowing customization
function isTabItem(
comp: ReactElement<object>,
): comp is ReactElement<TabItemProps> {
return 'value' in comp.props;
}
function ensureValidChildren(children: TabsProps['children']) {
return React.Children.map(children, (child) => {
if (isValidElement(child) && isTabItem(child)) {
return child;
}
// child.type.name will give non-sensical values in prod because of
// minification, but we assume it won't throw in prod.
throw new Error(
`Docusaurus error: Bad <Tabs> child <${
// @ts-expect-error: guarding against unexpected cases
typeof child.type === 'string' ? child.type : child.type.name
}>: all children of the <Tabs> component should be <TabItem>, and every <TabItem> should have a unique "value" prop.`,
);
});
}
function extractChildrenTabValues(children: TabsProps['children']): TabValue[] {
return ensureValidChildren(children).map(
({props: {value, label, attributes, default: isDefault}}) => ({
value,
label,
attributes,
default: isDefault,
}),
);
}
function ensureNoDuplicateValue(values: readonly TabValue[]) {
const dup = duplicates(values, (a, b) => a.value === b.value);
if (dup.length > 0) {
throw new Error(
`Docusaurus error: Duplicate values "${dup
.map((a) => a.value)
.join(', ')}" found in <Tabs>. Every value needs to be unique.`,
);
}
}
function useTabValues(
props: Pick<TabsProps, 'values' | 'children'>,
): readonly TabValue[] {
const {values: valuesProp, children} = props;
return useMemo(() => {
const values = valuesProp ?? extractChildrenTabValues(children);
ensureNoDuplicateValue(values);
return values;
}, [valuesProp, children]);
}
function isValidValue({
value,
tabValues,
}: {
value: string | null | undefined;
tabValues: readonly TabValue[];
}) {
return tabValues.some((a) => a.value === value);
}
function getInitialStateValue({
defaultValue,
tabValues,
}: {
defaultValue: TabsProps['defaultValue'];
tabValues: readonly TabValue[];
}): string {
if (tabValues.length === 0) {
throw new Error(
'Docusaurus error: the <Tabs> component requires at least one <TabItem> children component',
);
}
if (defaultValue) {
// Warn user about passing incorrect defaultValue as prop.
if (!isValidValue({value: defaultValue, tabValues})) {
throw new Error(
`Docusaurus error: The <Tabs> has a defaultValue "${defaultValue}" but none of its children has the corresponding value. Available values are: ${tabValues
.map((a) => a.value)
.join(
', ',
)}. If you intend to show no default tab, use defaultValue={null} instead.`,
);
}
return defaultValue;
}
const defaultTabValue =
tabValues.find((tabValue) => tabValue.default) ?? tabValues[0];
if (!defaultTabValue) {
throw new Error('Unexpected error: 0 tabValues');
}
return defaultTabValue.value;
}
function getStorageKey(groupId: string | undefined) {
if (!groupId) {
return null;
}
return `docusaurus.tab.${groupId}`;
}
function getQueryStringKey({
queryString = false,
groupId,
}: Pick<TabsProps, 'queryString' | 'groupId'>) {
if (typeof queryString === 'string') {
return queryString;
}
if (queryString === false) {
return null;
}
if (queryString === true && !groupId) {
throw new Error(
`Docusaurus error: The <Tabs> component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".`,
);
}
return groupId ?? null;
}
function useTabQueryString({
queryString = false,
groupId,
}: Pick<TabsProps, 'queryString' | 'groupId'>) {
const history = useHistory();
const key = getQueryStringKey({queryString, groupId});
const value = useQueryStringValue(key);
const setValue = useCallback(
(newValue: string) => {
if (!key) {
return; // no-op
}
const searchParams = new URLSearchParams(history.location.search);
searchParams.set(key, newValue);
history.replace({...history.location, search: searchParams.toString()});
},
[key, history],
);
return [value, setValue] as const;
}
function useTabStorage({groupId}: Pick<TabsProps, 'groupId'>) {
const key = getStorageKey(groupId);
const [value, storageSlot] = useStorageSlot(key);
const setValue = useCallback(
(newValue: string) => {
if (!key) {
return; // no-op
}
storageSlot.set(newValue);
},
[key, storageSlot],
);
return [value, setValue] as const;
}
export function useTabs(props: TabsProps): {
selectedValue: string;
selectValue: (value: string) => void;
tabValues: readonly TabValue[];
} {
const {defaultValue, queryString = false, groupId} = props;
const tabValues = useTabValues(props);
const [selectedValue, setSelectedValue] = useState(() =>
getInitialStateValue({defaultValue, tabValues}),
);
const [queryStringValue, setQueryString] = useTabQueryString({
queryString,
groupId,
});
const [storageValue, setStorageValue] = useTabStorage({
groupId,
});
// We sync valid querystring/storage value to state on change + hydration
const valueToSync = (() => {
const value = queryStringValue ?? storageValue;
if (!isValidValue({value, tabValues})) {
return null;
}
return value;
})();
useEffect(() => {
if (valueToSync) {
setSelectedValue(valueToSync);
}
}, [valueToSync]);
const selectValue = useCallback(
(newValue: string) => {
if (!isValidValue({value: newValue, tabValues})) {
throw new Error(`Can't select invalid tab value=${newValue}`);
}
setSelectedValue(newValue);
setQueryString(newValue);
setStorageValue(newValue);
},
[setQueryString, setStorageValue, tabValues],
);
return {selectedValue, selectValue, tabValues};
}

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/theme-live-codeblock",
"version": "2.2.0",
"version": "2.3.0",
"description": "Docusaurus live code block component.",
"main": "lib/index.js",
"types": "src/theme-live-codeblock.d.ts",
@ -23,10 +23,10 @@
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "2.2.0",
"@docusaurus/theme-common": "2.2.0",
"@docusaurus/theme-translations": "2.2.0",
"@docusaurus/utils-validation": "2.2.0",
"@docusaurus/core": "2.3.0",
"@docusaurus/theme-common": "2.3.0",
"@docusaurus/theme-translations": "2.3.0",
"@docusaurus/utils-validation": "2.3.0",
"@philpl/buble": "^0.19.7",
"clsx": "^1.2.1",
"fs-extra": "^10.1.0",
@ -34,7 +34,7 @@
"tslib": "^2.4.0"
},
"devDependencies": {
"@docusaurus/types": "2.2.0",
"@docusaurus/types": "2.3.0",
"@types/buble": "^0.20.1"
},
"peerDependencies": {

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/theme-mermaid",
"version": "2.2.0",
"version": "2.3.0",
"description": "Mermaid components for Docusaurus.",
"main": "lib/index.js",
"types": "src/theme-mermaid.d.ts",
@ -33,18 +33,17 @@
"copy:watch": "node ../../admin/scripts/copyUntypedFiles.js --watch"
},
"dependencies": {
"@docusaurus/core": "2.2.0",
"@docusaurus/module-type-aliases": "2.2.0",
"@docusaurus/theme-common": "2.2.0",
"@docusaurus/types": "2.2.0",
"@docusaurus/utils-validation": "2.2.0",
"@docusaurus/core": "2.3.0",
"@docusaurus/module-type-aliases": "2.3.0",
"@docusaurus/theme-common": "2.3.0",
"@docusaurus/types": "2.3.0",
"@docusaurus/utils-validation": "2.3.0",
"@mdx-js/react": "^1.6.22",
"mermaid": "^9.1.1",
"mermaid": "^9.2.2",
"tslib": "^2.4.0"
},
"devDependencies": {
"@types/mdx-js__react": "^1.5.5",
"@types/mermaid": "^8.2.9",
"react-test-renderer": "^17.0.2"
},
"peerDependencies": {

View file

@ -7,8 +7,7 @@
import {useMemo} from 'react';
import {useColorMode, useThemeConfig} from '@docusaurus/theme-common';
import mermaid from 'mermaid';
import type mermaidAPI from 'mermaid/mermaidAPI';
import mermaid, {type MermaidConfig} from 'mermaid';
import type {ThemeConfig} from '@docusaurus/theme-mermaid';
// Stable className to allow users to easily target with CSS
@ -18,7 +17,7 @@ export function useMermaidThemeConfig(): ThemeConfig['mermaid'] {
return (useThemeConfig() as unknown as ThemeConfig).mermaid;
}
export function useMermaidConfig(): mermaidAPI.Config {
export function useMermaidConfig(): MermaidConfig {
const {colorMode} = useColorMode();
const mermaidThemeConfig = useMermaidThemeConfig();
@ -33,7 +32,7 @@ export function useMermaidConfig(): mermaidAPI.Config {
export function useMermaidSvg(
txt: string,
mermaidConfigParam?: mermaidAPI.Config,
mermaidConfigParam?: MermaidConfig,
): string {
/*
For flexibility, we allow the hook to receive a custom Mermaid config

View file

@ -7,14 +7,13 @@
import {Joi} from '@docusaurus/utils-validation';
import type {ThemeConfig} from '@docusaurus/theme-mermaid';
import type mermaidAPI from 'mermaid/mermaidAPI';
import type {ThemeConfigValidationContext} from '@docusaurus/types';
export const DEFAULT_THEME_CONFIG: ThemeConfig = {
mermaid: {
theme: {
dark: 'dark' as mermaidAPI.Theme,
light: 'default' as mermaidAPI.Theme,
dark: 'dark',
light: 'default',
},
options: {},
},

View file

@ -10,6 +10,6 @@
"module": "esnext",
"target": "esnext"
},
"include": ["src/theme", "src/*.d.ts"],
"include": ["src/client", "src/theme", "src/*.d.ts"],
"exclude": ["**/__tests__/**"]
}

View file

@ -10,5 +10,5 @@
"outDir": "lib"
},
"include": ["src"],
"exclude": ["src/theme", "**/__tests__/**"]
"exclude": ["src/client", "src/theme", "**/__tests__/**"]
}

View file

@ -1,6 +1,6 @@
{
"name": "@docusaurus/theme-search-algolia",
"version": "2.2.0",
"version": "2.3.0",
"description": "Algolia search component for Docusaurus.",
"main": "lib/index.js",
"sideEffects": [
@ -34,13 +34,13 @@
},
"dependencies": {
"@docsearch/react": "^3.1.1",
"@docusaurus/core": "2.2.0",
"@docusaurus/logger": "2.2.0",
"@docusaurus/plugin-content-docs": "2.2.0",
"@docusaurus/theme-common": "2.2.0",
"@docusaurus/theme-translations": "2.2.0",
"@docusaurus/utils": "2.2.0",
"@docusaurus/utils-validation": "2.2.0",
"@docusaurus/core": "2.3.0",
"@docusaurus/logger": "2.3.0",
"@docusaurus/plugin-content-docs": "2.3.0",
"@docusaurus/theme-common": "2.3.0",
"@docusaurus/theme-translations": "2.3.0",
"@docusaurus/utils": "2.3.0",
"@docusaurus/utils-validation": "2.3.0",
"algoliasearch": "^4.13.1",
"algoliasearch-helper": "^3.10.0",
"clsx": "^1.2.1",
@ -51,7 +51,7 @@
"utility-types": "^3.10.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.2.0"
"@docusaurus/module-type-aliases": "2.3.0"
},
"peerDependencies": {
"react": "^16.8.4 || ^17.0.0",

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {validateThemeConfig, DEFAULT_CONFIG} from '../validateThemeConfig';
import {DEFAULT_CONFIG, validateThemeConfig} from '../validateThemeConfig';
import type {Joi} from '@docusaurus/utils-validation';
function testValidateThemeConfig(themeConfig: {[key: string]: unknown}) {
@ -121,6 +121,53 @@ describe('validateThemeConfig', () => {
});
});
describe('replaceSearchResultPathname', () => {
it('escapes from string', () => {
const algolia = {
appId: 'BH4D9OD16A',
indexName: 'index',
apiKey: 'apiKey',
replaceSearchResultPathname: {
from: '/docs/some-\\special-.[regexp]{chars*}',
to: '/abc',
},
};
expect(testValidateThemeConfig({algolia})).toEqual({
algolia: {
...DEFAULT_CONFIG,
...algolia,
replaceSearchResultPathname: {
from: '/docs/some\\x2d\\\\special\\x2d\\.\\[regexp\\]\\{chars\\*\\}',
to: '/abc',
},
},
});
});
it('converts from regexp to string', () => {
const algolia = {
appId: 'BH4D9OD16A',
indexName: 'index',
apiKey: 'apiKey',
replaceSearchResultPathname: {
from: /^\/docs\/(?:1\.0|next)/,
to: '/abc',
},
};
expect(testValidateThemeConfig({algolia})).toEqual({
algolia: {
...DEFAULT_CONFIG,
...algolia,
replaceSearchResultPathname: {
from: '^\\/docs\\/(?:1\\.0|next)',
to: '/abc',
},
},
});
});
});
it('searchParameters.facetFilters search config', () => {
const algolia = {
appId: 'BH4D9OD16A',

View file

@ -5,4 +5,6 @@
* LICENSE file in the root directory of this source tree.
*/
export {useAlgoliaThemeConfig} from './useAlgoliaThemeConfig';
export {useAlgoliaContextualFacetFilters} from './useAlgoliaContextualFacetFilters';
export {useSearchResultUrlProcessor} from './useSearchResultUrlProcessor';

View file

@ -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 useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import type {ThemeConfig} from '@docusaurus/theme-search-algolia';
export function useAlgoliaThemeConfig(): ThemeConfig {
const {
siteConfig: {themeConfig},
} = useDocusaurusContext();
return themeConfig as ThemeConfig;
}

View file

@ -0,0 +1,54 @@
/**
* 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 {useCallback} from 'react';
import {isRegexpStringMatch} from '@docusaurus/theme-common';
import {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
import {useAlgoliaThemeConfig} from './useAlgoliaThemeConfig';
import type {ThemeConfig} from '@docusaurus/theme-search-algolia';
function replacePathname(
pathname: string,
replaceSearchResultPathname: ThemeConfig['algolia']['replaceSearchResultPathname'],
): string {
return replaceSearchResultPathname
? pathname.replaceAll(
new RegExp(replaceSearchResultPathname.from, 'g'),
replaceSearchResultPathname.to,
)
: pathname;
}
/**
* Process the search result url from Algolia to its final form, ready to be
* navigated to or used as a link
*/
export function useSearchResultUrlProcessor(): (url: string) => string {
const {withBaseUrl} = useBaseUrlUtils();
const {
algolia: {externalUrlRegex, replaceSearchResultPathname},
} = useAlgoliaThemeConfig();
return useCallback(
(url: string) => {
const parsedURL = new URL(url);
// Algolia contains an external domain => navigate to URL
if (isRegexpStringMatch(externalUrlRegex, parsedURL.href)) {
return url;
}
// Otherwise => transform to relative URL for SPA navigation
const relativeUrl = `${parsedURL.pathname + parsedURL.hash}`;
return withBaseUrl(
replacePathname(relativeUrl, replaceSearchResultPathname),
);
},
[withBaseUrl, externalUrlRegex, replaceSearchResultPathname],
);
}

View file

@ -17,13 +17,23 @@ declare module '@docusaurus/theme-search-algolia' {
indexName: string;
searchParameters: {[key: string]: unknown};
searchPagePath: string | false | null;
replaceSearchResultPathname?: {
from: string;
to: string;
};
};
};
export type UserThemeConfig = DeepPartial<ThemeConfig>;
}
declare module '@docusaurus/theme-search-algolia/client' {
import type {ThemeConfig} from '@docusaurus/theme-search-algolia';
export function useAlgoliaThemeConfig(): ThemeConfig;
export function useAlgoliaContextualFacetFilters(): [string, string[]];
export function useSearchResultUrlProcessor(): (url: string) => string;
}
declare module '@theme/SearchPage' {

View file

@ -5,20 +5,23 @@
* LICENSE file in the root directory of this source tree.
*/
import React, {useState, useRef, useCallback, useMemo} from 'react';
import {createPortal} from 'react-dom';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import {useHistory} from '@docusaurus/router';
import {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
import Link from '@docusaurus/Link';
import React, {useCallback, useMemo, useRef, useState} from 'react';
import {DocSearchButton, useDocSearchKeyboardEvents} from '@docsearch/react';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import {useHistory} from '@docusaurus/router';
import {isRegexpStringMatch} from '@docusaurus/theme-common';
import {useSearchPage} from '@docusaurus/theme-common/internal';
import {DocSearchButton, useDocSearchKeyboardEvents} from '@docsearch/react';
import {useAlgoliaContextualFacetFilters} from '@docusaurus/theme-search-algolia/client';
import {
useAlgoliaContextualFacetFilters,
useSearchResultUrlProcessor,
} from '@docusaurus/theme-search-algolia/client';
import Translate from '@docusaurus/Translate';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import {createPortal} from 'react-dom';
import translations from '@theme/SearchTranslations';
import type {AutocompleteState} from '@algolia/autocomplete-core';
import type {
DocSearchModal as DocSearchModalType,
DocSearchModalProps,
@ -28,7 +31,6 @@ import type {
StoredDocSearchHit,
} from '@docsearch/react/dist/esm/types';
import type {SearchClient} from 'algoliasearch/lite';
import type {AutocompleteState} from '@algolia/autocomplete-core';
type DocSearchProps = Omit<
DocSearchModalProps,
@ -88,6 +90,7 @@ function DocSearch({
...props
}: DocSearchProps) {
const {siteMetadata} = useDocusaurusContext();
const processSearchResultUrl = useSearchResultUrlProcessor();
const contextualSearchFacetFilters =
useAlgoliaContextualFacetFilters() as FacetFilters;
@ -107,7 +110,6 @@ function DocSearch({
facetFilters,
};
const {withBaseUrl} = useBaseUrlUtils();
const history = useHistory();
const searchContainer = useRef<HTMLDivElement | null>(null);
const searchButtonRef = useRef<HTMLButtonElement>(null);
@ -172,20 +174,14 @@ function DocSearch({
const transformItems = useRef<DocSearchModalProps['transformItems']>(
(items) =>
items.map((item) => {
// If Algolia contains a external domain, we should navigate without
// relative URL
if (isRegexpStringMatch(externalUrlRegex, item.url)) {
return item;
}
// We transform the absolute URL into a relative URL.
const url = new URL(item.url);
return {
...item,
url: withBaseUrl(`${url.pathname}${url.hash}`),
};
}),
props.transformItems
? // Custom transformItems
props.transformItems(items)
: // Default transformItems
items.map((item) => ({
...item,
url: processSearchResultUrl(item.url),
})),
).current;
const resultsFooterComponent: DocSearchProps['resultsFooterComponent'] =

View file

@ -7,32 +7,34 @@
/* eslint-disable jsx-a11y/no-autofocus */
import React, {useEffect, useState, useReducer, useRef} from 'react';
import React, {useEffect, useReducer, useRef, useState} from 'react';
import clsx from 'clsx';
import algoliaSearch from 'algoliasearch/lite';
import algoliaSearchHelper from 'algoliasearch-helper';
import algoliaSearch from 'algoliasearch/lite';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import {useAllDocsData} from '@docusaurus/plugin-content-docs/client';
import {
HtmlClassNameProvider,
usePluralForm,
isRegexpStringMatch,
useEvent,
usePluralForm,
} from '@docusaurus/theme-common';
import {
useTitleFormatter,
useSearchPage,
useTitleFormatter,
} from '@docusaurus/theme-common/internal';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import {useAllDocsData} from '@docusaurus/plugin-content-docs/client';
import Translate, {translate} from '@docusaurus/Translate';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import {
useAlgoliaThemeConfig,
useSearchResultUrlProcessor,
} from '@docusaurus/theme-search-algolia/client';
import Layout from '@theme/Layout';
import styles from './styles.module.css';
import type {ThemeConfig} from '@docusaurus/theme-search-algolia';
// Very simple pluralization: probably good enough for now
function useDocumentsFoundPlural() {
@ -156,12 +158,12 @@ type ResultDispatcher =
function SearchPageContent(): JSX.Element {
const {
siteConfig: {themeConfig},
i18n: {currentLocale},
} = useDocusaurusContext();
const {
algolia: {appId, apiKey, indexName, externalUrlRegex},
} = themeConfig as ThemeConfig;
algolia: {appId, apiKey, indexName},
} = useAlgoliaThemeConfig();
const processSearchResultUrl = useSearchResultUrlProcessor();
const documentsFoundPlural = useDocumentsFoundPlural();
const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers();
@ -244,16 +246,12 @@ function SearchPageContent(): JSX.Element {
_highlightResult: {hierarchy: {[key: string]: {value: string}}};
_snippetResult: {content?: {value: string}};
}) => {
const parsedURL = new URL(url);
const titles = Object.keys(hierarchy).map((key) =>
sanitizeValue(hierarchy[key]!.value),
);
return {
title: titles.pop()!,
url: isRegexpStringMatch(externalUrlRegex, parsedURL.href)
? parsedURL.href
: parsedURL.pathname + parsedURL.hash,
url: processSearchResultUrl(url),
summary: snippet.content
? `${sanitizeValue(snippet.content.value)}...`
: '',

View file

@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {escapeRegexp} from '@docusaurus/utils';
import {Joi} from '@docusaurus/utils-validation';
import type {
ThemeConfig,
@ -39,6 +40,19 @@ export const Schema = Joi.object<ThemeConfig>({
.try(Joi.boolean().invalid(true), Joi.string())
.allow(null)
.default(DEFAULT_CONFIG.searchPagePath),
replaceSearchResultPathname: Joi.object({
from: Joi.custom((from) => {
if (typeof from === 'string') {
return escapeRegexp(from);
} else if (from instanceof RegExp) {
return from.source;
}
throw new Error(
`it should be a RegExp or a string, but received ${from}`,
);
}).required(),
to: Joi.string().required(),
}).optional(),
})
.label('themeConfig.algolia')
.required()

View file

@ -8,6 +8,7 @@
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "تبديل فئة الشريط الجاني القابل للاغلاق '{label}'",
"theme.ErrorPageContent.title": "هذه الصفحة لا تستجيب.",
"theme.ErrorPageContent.tryAgain": "المحاولة مجددا",
"theme.NavBar.navAriaLabel": "Main",
"theme.NotFound.p1": "لم نتمكن من العثور على ما كنت تبحث عنه.",
"theme.NotFound.p2": "يرجى الاتصال بمالك الموقع الذي ربطك بعنوان URL الأصلي وإخباره بأن الارتباط الخاص به معطل.",
"theme.NotFound.title": "الصفحة غير موجودة",
@ -35,7 +36,7 @@
"theme.colorToggle.ariaLabel.mode.dark": "الوضع الداكن",
"theme.colorToggle.ariaLabel.mode.light": "الوضع الفاتح",
"theme.common.editThisPage": "تعديل هذه الصفحة",
"theme.common.headingLinkTitle": "ارتباط مباشر بالعنوان",
"theme.common.headingLinkTitle": "ارتباط مباشر بالعنوان {heading}",
"theme.common.skipToMainContent": "انتقل إلى المحتوى الرئيسي",
"theme.docs.DocCard.categoryDescription": "{count} مواد",
"theme.docs.breadcrumbs.home": "الرئيسية",
@ -48,6 +49,7 @@
"theme.docs.sidebar.collapseButtonTitle": "طي الشريط الجانبي",
"theme.docs.sidebar.expandButtonAriaLabel": "توسيع الشريط الجانبي",
"theme.docs.sidebar.expandButtonTitle": "توسيع الشريط الجانبي",
"theme.docs.sidebar.navAriaLabel": "Docs sidebar",
"theme.docs.sidebar.toggleSidebarButtonAriaLabel": "Toggle navigation bar",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} مستند موسوم بـ \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "مستند موسوم واحد|{count} مستندات موسومة",

View file

@ -16,7 +16,9 @@
"theme.ErrorPageContent.title": "This page crashed.",
"theme.ErrorPageContent.title___DESCRIPTION": "The title of the fallback page when the page crashed",
"theme.ErrorPageContent.tryAgain": "Try again",
"theme.ErrorPageContent.tryAgain___DESCRIPTION": "The label of the button to try again when the page crashed",
"theme.ErrorPageContent.tryAgain___DESCRIPTION": "The label of the button to try again rendering when the React error boundary captures an error",
"theme.NavBar.navAriaLabel": "Main",
"theme.NavBar.navAriaLabel___DESCRIPTION": "The ARIA label for the main navigation",
"theme.NotFound.p1": "We could not find what you were looking for.",
"theme.NotFound.p1___DESCRIPTION": "The first paragraph of the 404 page",
"theme.NotFound.p2": "Please contact the owner of the site that linked you to the original URL and let them know their link is broken.",
@ -71,7 +73,7 @@
"theme.colorToggle.ariaLabel.mode.light___DESCRIPTION": "The name for the light color mode",
"theme.common.editThisPage": "Edit this page",
"theme.common.editThisPage___DESCRIPTION": "The link label to edit the current page",
"theme.common.headingLinkTitle": "Direct link to heading",
"theme.common.headingLinkTitle": "Direct link to {heading}",
"theme.common.headingLinkTitle___DESCRIPTION": "Title for link to heading",
"theme.common.skipToMainContent": "Skip to main content",
"theme.common.skipToMainContent___DESCRIPTION": "The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation",
@ -97,6 +99,8 @@
"theme.docs.sidebar.expandButtonAriaLabel___DESCRIPTION": "The ARIA label and title attribute for expand button of doc sidebar",
"theme.docs.sidebar.expandButtonTitle": "Expand sidebar",
"theme.docs.sidebar.expandButtonTitle___DESCRIPTION": "The ARIA label and title attribute for expand button of doc sidebar",
"theme.docs.sidebar.navAriaLabel": "Docs sidebar",
"theme.docs.sidebar.navAriaLabel___DESCRIPTION": "The ARIA label for the sidebar navigation",
"theme.docs.sidebar.toggleSidebarButtonAriaLabel": "Toggle navigation bar",
"theme.docs.sidebar.toggleSidebarButtonAriaLabel___DESCRIPTION": "The ARIA label for hamburger menu button of mobile navigation",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",

View file

@ -8,6 +8,7 @@
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
"theme.ErrorPageContent.title": "This page crashed.",
"theme.ErrorPageContent.tryAgain": "Try again",
"theme.NavBar.navAriaLabel": "Main",
"theme.NotFound.p1": "আপনি যা খুঁজছিলেন তা আমরা খুঁজে পাইনি।",
"theme.NotFound.p2": "দয়া করে সাইটের মালিকের সাথে যোগাযোগ করুন যা আপনাকে মূল URL এর সাথে যুক্ত করেছে এবং তাদের লিঙ্কটি ভাঙ্গা রয়েছে তা তাদের জানান।",
"theme.NotFound.title": "পেজটি খুঁজে পাওয়া যায়নি",
@ -35,7 +36,7 @@
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
"theme.colorToggle.ariaLabel.mode.light": "light mode",
"theme.common.editThisPage": "এই পেজটি এডিট করুন",
"theme.common.headingLinkTitle": "হেডিং এর সঙ্গে সরাসরি লিংকড",
"theme.common.headingLinkTitle": "{heading} এর সঙ্গে সরাসরি লিংকড",
"theme.common.skipToMainContent": "স্কিপ করে মূল কন্টেন্ট এ যান",
"theme.docs.DocCard.categoryDescription": "{count} items",
"theme.docs.breadcrumbs.home": "Home page",
@ -48,6 +49,7 @@
"theme.docs.sidebar.collapseButtonTitle": "সাইডবারটি সঙ্কুচিত করুন",
"theme.docs.sidebar.expandButtonAriaLabel": "সাইডবারটি প্রসারিত করুন",
"theme.docs.sidebar.expandButtonTitle": "সাইডবারটি প্রসারিত করুন",
"theme.docs.sidebar.navAriaLabel": "Docs sidebar",
"theme.docs.sidebar.toggleSidebarButtonAriaLabel": "Toggle navigation bar",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",

View file

@ -8,6 +8,7 @@
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
"theme.ErrorPageContent.title": "This page crashed.",
"theme.ErrorPageContent.tryAgain": "Try again",
"theme.NavBar.navAriaLabel": "Main",
"theme.NotFound.p1": "Nepodařilo se nám najít co jste hledal(a).",
"theme.NotFound.p2": "Kontaktujte prosím vlastníka webu, který vás odkázal na původní URL a upozorněte ho, že jejich odkaz nefunguje.",
"theme.NotFound.title": "Stránka nenalezena",
@ -35,7 +36,7 @@
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
"theme.colorToggle.ariaLabel.mode.light": "light mode",
"theme.common.editThisPage": "Upravit tuto stránku",
"theme.common.headingLinkTitle": "Přímý odkaz na nadpis",
"theme.common.headingLinkTitle": "Přímý odkaz na {heading}",
"theme.common.skipToMainContent": "Přeskočit na hlavní obsah",
"theme.docs.DocCard.categoryDescription": "{count} items",
"theme.docs.breadcrumbs.home": "Home page",
@ -48,6 +49,7 @@
"theme.docs.sidebar.collapseButtonTitle": "Zavřít postranní lištu",
"theme.docs.sidebar.expandButtonAriaLabel": "Otevřít postranní lištu",
"theme.docs.sidebar.expandButtonTitle": "Otevřít postranní lištu",
"theme.docs.sidebar.navAriaLabel": "Docs sidebar",
"theme.docs.sidebar.toggleSidebarButtonAriaLabel": "Toggle navigation bar",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",

View file

@ -8,6 +8,7 @@
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Toggle the collapsible sidebar category '{label}'",
"theme.ErrorPageContent.title": "This page crashed.",
"theme.ErrorPageContent.tryAgain": "Try again",
"theme.NavBar.navAriaLabel": "Main",
"theme.NotFound.p1": "Vi kunne ikke finde det, du søgte.",
"theme.NotFound.p2": "Venligst kontakt ejeren til webstedet, som førte dig frem denne URL, og informer dem om at linket ikke virker.",
"theme.NotFound.title": "Siden blev ikke fundet",
@ -35,7 +36,7 @@
"theme.colorToggle.ariaLabel.mode.dark": "dark mode",
"theme.colorToggle.ariaLabel.mode.light": "light mode",
"theme.common.editThisPage": "Rediger denne side",
"theme.common.headingLinkTitle": "Direkte link til overskrift",
"theme.common.headingLinkTitle": "Direkte link til {heading}",
"theme.common.skipToMainContent": "Hop til hovedindhold",
"theme.docs.DocCard.categoryDescription": "{count} items",
"theme.docs.breadcrumbs.home": "Home page",
@ -48,6 +49,7 @@
"theme.docs.sidebar.collapseButtonTitle": "Sammenlæg sidenavigation",
"theme.docs.sidebar.expandButtonAriaLabel": "Udvid sidenavigation",
"theme.docs.sidebar.expandButtonTitle": "Udvid sidenavigation",
"theme.docs.sidebar.navAriaLabel": "Docs sidebar",
"theme.docs.sidebar.toggleSidebarButtonAriaLabel": "Toggle navigation bar",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",

View file

@ -8,6 +8,7 @@
"theme.DocSidebarItem.toggleCollapsedCategoryAriaLabel": "Umschalten der Seitenleiste mit einklappbarer Kategorie '{label}'",
"theme.ErrorPageContent.title": "Die Seite ist abgestürzt.",
"theme.ErrorPageContent.tryAgain": "Nochmal versuchen",
"theme.NavBar.navAriaLabel": "Main",
"theme.NotFound.p1": "Wir konnten nicht finden, wonach Sie gesucht haben.",
"theme.NotFound.p2": "Bitte kontaktieren Sie den Besitzer der Seite, die Sie mit der ursprünglichen URL verlinkt hat, und teilen Sie ihm mit, dass der Link nicht mehr funktioniert.",
"theme.NotFound.title": "Seite nicht gefunden",
@ -35,7 +36,7 @@
"theme.colorToggle.ariaLabel.mode.dark": "dunkler Modus",
"theme.colorToggle.ariaLabel.mode.light": "heller Modus",
"theme.common.editThisPage": "Diese Seite bearbeiten",
"theme.common.headingLinkTitle": "Direkter Link zur Überschrift",
"theme.common.headingLinkTitle": "Direkter Link zur {heading}",
"theme.common.skipToMainContent": "Zum Hauptinhalt springen",
"theme.docs.DocCard.categoryDescription": "{count} Einträge",
"theme.docs.breadcrumbs.home": "Home page",
@ -48,6 +49,7 @@
"theme.docs.sidebar.collapseButtonTitle": "Seitenleiste einklappen",
"theme.docs.sidebar.expandButtonAriaLabel": "Seitenleiste ausklappen",
"theme.docs.sidebar.expandButtonTitle": "Seitenleiste ausklappen",
"theme.docs.sidebar.navAriaLabel": "Docs sidebar",
"theme.docs.sidebar.toggleSidebarButtonAriaLabel": "Toggle navigation bar",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} mit \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "Ein doc getaggt|{count} docs getaggt",

Some files were not shown because too many files have changed in this diff Show more