mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-28 17:57:48 +02:00
chore: clean up ESLint config, enable a few rules (#6514)
* chore: clean up ESLint config, enable a few rules * enable max-len for comments * fix build
This commit is contained in:
parent
b8ccb869f1
commit
aa446b7a9c
167 changed files with 1157 additions and 960 deletions
226
.eslintrc.js
226
.eslintrc.js
|
@ -22,15 +22,14 @@ module.exports = {
|
|||
allowImportExportEverywhere: true,
|
||||
},
|
||||
globals: {
|
||||
testStylelintRule: true,
|
||||
JSX: true,
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:jest/recommended',
|
||||
'airbnb',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'prettier',
|
||||
],
|
||||
settings: {
|
||||
|
@ -41,111 +40,37 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
reportUnusedDisableDirectives: true,
|
||||
plugins: ['react-hooks', 'header', 'jest'],
|
||||
plugins: ['react-hooks', 'header', 'jest', '@typescript-eslint'],
|
||||
rules: {
|
||||
'react-hooks/rules-of-hooks': ERROR,
|
||||
'react-hooks/exhaustive-deps': ERROR,
|
||||
'class-methods-use-this': OFF, // It's a way of allowing private variables.
|
||||
'func-names': OFF,
|
||||
// Ignore certain webpack alias because it can't be resolved
|
||||
'import/no-unresolved': [
|
||||
ERROR,
|
||||
{
|
||||
ignore: ['^@theme', '^@docusaurus', '^@generated', '^@site'],
|
||||
},
|
||||
],
|
||||
'import/extensions': OFF,
|
||||
'no-restricted-exports': OFF,
|
||||
'header/header': [
|
||||
ERROR,
|
||||
'block',
|
||||
[
|
||||
'*',
|
||||
' * 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.',
|
||||
' ',
|
||||
],
|
||||
],
|
||||
'jsx-a11y/click-events-have-key-events': WARNING,
|
||||
'jsx-a11y/no-noninteractive-element-interactions': WARNING,
|
||||
'jsx-a11y/html-has-lang': OFF,
|
||||
'no-console': OFF,
|
||||
'no-else-return': OFF,
|
||||
'no-param-reassign': [WARNING, {props: false}],
|
||||
'no-underscore-dangle': OFF,
|
||||
curly: [WARNING, 'all'],
|
||||
'react/jsx-filename-extension': OFF,
|
||||
'react/no-array-index-key': OFF, // Sometimes its ok, e.g. non-changing data.
|
||||
'react/prop-types': OFF,
|
||||
'react/destructuring-assignment': OFF, // Too many lines.
|
||||
'react/prefer-stateless-function': WARNING,
|
||||
'react/jsx-props-no-spreading': OFF,
|
||||
'react/require-default-props': [ERROR, {ignoreFunctionalComponents: true}],
|
||||
'react/function-component-definition': [
|
||||
WARNING,
|
||||
{
|
||||
namedComponents: 'function-declaration',
|
||||
unnamedComponents: 'arrow-function',
|
||||
},
|
||||
],
|
||||
'react/no-unstable-nested-components': [WARNING, {allowAsProps: true}],
|
||||
'@typescript-eslint/no-inferrable-types': OFF,
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
WARNING,
|
||||
{disallowTypeAnnotations: false},
|
||||
],
|
||||
'import/order': OFF,
|
||||
'import/prefer-default-export': OFF,
|
||||
'lines-between-class-members': OFF,
|
||||
'no-lonely-if': WARNING,
|
||||
'no-use-before-define': OFF,
|
||||
'@typescript-eslint/no-use-before-define': [
|
||||
ERROR,
|
||||
{functions: false, classes: false, variables: true},
|
||||
],
|
||||
'no-unused-vars': OFF,
|
||||
'no-nested-ternary': WARNING,
|
||||
'@typescript-eslint/no-empty-function': OFF,
|
||||
'@typescript-eslint/no-non-null-assertion': OFF,
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
ERROR,
|
||||
{argsIgnorePattern: '^_', ignoreRestSiblings: true},
|
||||
],
|
||||
'@typescript-eslint/explicit-module-boundary-types': WARNING,
|
||||
'@typescript-eslint/ban-ts-comment': [
|
||||
ERROR,
|
||||
{'ts-expect-error': 'allow-with-description'},
|
||||
],
|
||||
'import/no-extraneous-dependencies': ERROR,
|
||||
'no-useless-escape': WARNING,
|
||||
'prefer-template': WARNING,
|
||||
'no-template-curly-in-string': WARNING,
|
||||
'array-callback-return': WARNING,
|
||||
camelcase: WARNING,
|
||||
'no-restricted-syntax': WARNING,
|
||||
'no-unused-expressions': [WARNING, {allowTaggedTemplates: true}],
|
||||
'class-methods-use-this': OFF, // It's a way of allowing private variables.
|
||||
curly: [WARNING, 'all'],
|
||||
'global-require': WARNING,
|
||||
'prefer-destructuring': WARNING,
|
||||
yoda: WARNING,
|
||||
'no-await-in-loop': OFF,
|
||||
'no-control-regex': WARNING,
|
||||
'no-empty': [WARNING, {allowEmptyCatch: true}],
|
||||
'no-prototype-builtins': WARNING,
|
||||
'no-case-declarations': WARNING,
|
||||
'no-undef': OFF,
|
||||
'no-shadow': OFF,
|
||||
'@typescript-eslint/no-shadow': ERROR,
|
||||
'no-redeclare': OFF,
|
||||
'@typescript-eslint/no-redeclare': ERROR,
|
||||
'@typescript-eslint/no-empty-interface': [
|
||||
ERROR,
|
||||
'lines-between-class-members': OFF,
|
||||
'max-len': [
|
||||
WARNING,
|
||||
{
|
||||
allowSingleExtends: true,
|
||||
code: Infinity, // Code width is already enforced by Prettier
|
||||
tabWidth: 2,
|
||||
comments: 80,
|
||||
ignoreUrls: true,
|
||||
ignorePattern: '(eslint-disable|@)',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/method-signature-style': ERROR,
|
||||
'no-await-in-loop': OFF,
|
||||
'no-case-declarations': WARNING,
|
||||
'no-console': OFF,
|
||||
'no-control-regex': WARNING,
|
||||
'no-else-return': [WARNING, {allowElseIf: true}],
|
||||
'no-empty': [WARNING, {allowEmptyCatch: true}],
|
||||
'no-lonely-if': WARNING,
|
||||
'no-nested-ternary': WARNING,
|
||||
'no-param-reassign': [WARNING, {props: false}],
|
||||
'no-prototype-builtins': WARNING,
|
||||
'no-restricted-exports': OFF,
|
||||
'no-useless-escape': WARNING,
|
||||
'no-template-curly-in-string': WARNING,
|
||||
'no-restricted-imports': [
|
||||
ERROR,
|
||||
{
|
||||
|
@ -153,7 +78,9 @@ module.exports = {
|
|||
{
|
||||
name: 'lodash',
|
||||
importNames: [
|
||||
// 'compact', // TODO: TS doesn't make Boolean a narrowing function yet, so filter(Boolean) is problematic type-wise
|
||||
// TODO: TS doesn't make Boolean a narrowing function yet,
|
||||
// so filter(Boolean) is problematic type-wise
|
||||
// 'compact',
|
||||
'filter',
|
||||
'flatten',
|
||||
'flatMap',
|
||||
|
@ -170,16 +97,104 @@ module.exports = {
|
|||
],
|
||||
},
|
||||
],
|
||||
'no-restricted-syntax': WARNING,
|
||||
'no-unused-expressions': [WARNING, {allowTaggedTemplates: true}],
|
||||
'prefer-destructuring': WARNING,
|
||||
'prefer-template': WARNING,
|
||||
yoda: WARNING,
|
||||
|
||||
'header/header': [
|
||||
ERROR,
|
||||
'block',
|
||||
[
|
||||
'*',
|
||||
' * 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/extensions': OFF,
|
||||
// Ignore certain webpack aliases because they can't be resolved
|
||||
'import/no-unresolved': [
|
||||
ERROR,
|
||||
{
|
||||
ignore: ['^@theme', '^@docusaurus', '^@generated', '^@site'],
|
||||
},
|
||||
],
|
||||
'import/order': OFF,
|
||||
'import/prefer-default-export': OFF,
|
||||
|
||||
'jest/prefer-expect-resolves': WARNING,
|
||||
'jest/expect-expect': OFF,
|
||||
'jest/valid-title': OFF,
|
||||
|
||||
'jsx-a11y/click-events-have-key-events': WARNING,
|
||||
'jsx-a11y/no-noninteractive-element-interactions': WARNING,
|
||||
'jsx-a11y/html-has-lang': OFF,
|
||||
|
||||
'react-hooks/rules-of-hooks': ERROR,
|
||||
'react-hooks/exhaustive-deps': ERROR,
|
||||
|
||||
// Sometimes we do need the props as a whole, e.g. when spreading
|
||||
'react/destructuring-assignment': OFF,
|
||||
'react/function-component-definition': [
|
||||
WARNING,
|
||||
{
|
||||
namedComponents: 'function-declaration',
|
||||
unnamedComponents: 'arrow-function',
|
||||
},
|
||||
],
|
||||
'react/jsx-filename-extension': OFF,
|
||||
'react/jsx-props-no-spreading': OFF,
|
||||
'react/no-array-index-key': OFF, // We build a static site, and nearly all components don't change.
|
||||
'react/no-unstable-nested-components': [WARNING, {allowAsProps: true}],
|
||||
'react/prefer-stateless-function': WARNING,
|
||||
'react/prop-types': OFF,
|
||||
'react/require-default-props': [ERROR, {ignoreFunctionalComponents: true}],
|
||||
|
||||
'@typescript-eslint/ban-ts-comment': [
|
||||
ERROR,
|
||||
{'ts-expect-error': 'allow-with-description'},
|
||||
],
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
WARNING,
|
||||
{disallowTypeAnnotations: false},
|
||||
],
|
||||
'@typescript-eslint/explicit-module-boundary-types': WARNING,
|
||||
'@typescript-eslint/method-signature-style': ERROR,
|
||||
'@typescript-eslint/no-empty-function': OFF,
|
||||
'@typescript-eslint/no-empty-interface': [
|
||||
ERROR,
|
||||
{
|
||||
allowSingleExtends: true,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-inferrable-types': OFF,
|
||||
'no-use-before-define': OFF,
|
||||
'@typescript-eslint/no-use-before-define': [
|
||||
ERROR,
|
||||
{functions: false, classes: false, variables: true},
|
||||
],
|
||||
'@typescript-eslint/no-non-null-assertion': OFF,
|
||||
'no-redeclare': OFF,
|
||||
'@typescript-eslint/no-redeclare': ERROR,
|
||||
'no-shadow': OFF,
|
||||
'@typescript-eslint/no-shadow': ERROR,
|
||||
'no-unused-vars': OFF,
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
ERROR,
|
||||
{argsIgnorePattern: '^_', ignoreRestSiblings: true},
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'packages/docusaurus-theme-*/src/theme/**/*.js',
|
||||
'packages/docusaurus-theme-*/src/theme/**/*.ts',
|
||||
'packages/docusaurus-theme-*/src/theme/**/*.tsx',
|
||||
'packages/docusaurus-*/src/theme/**/*.js',
|
||||
'packages/docusaurus-*/src/theme/**/*.ts',
|
||||
'packages/docusaurus-*/src/theme/**/*.tsx',
|
||||
],
|
||||
rules: {
|
||||
'import/no-named-export': ERROR,
|
||||
|
@ -206,6 +221,7 @@ module.exports = {
|
|||
{
|
||||
files: ['*.ts', '*.tsx'],
|
||||
rules: {
|
||||
'no-undef': OFF,
|
||||
'import/no-import-module-exports': OFF,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -62,9 +62,11 @@ describe('packages', () => {
|
|||
.filter((packageJsonFile) => packageJsonFile.content.name.startsWith('@'))
|
||||
.forEach((packageJsonFile) => {
|
||||
if (packageJsonFile) {
|
||||
// Unfortunately jest custom message do not exist in loops, so using an exception instead to show failing package file
|
||||
// Unfortunately jest custom message do not exist in loops,
|
||||
// so using an exception instead to show failing package file
|
||||
// (see https://github.com/facebook/jest/issues/3293)
|
||||
// expect(packageJsonFile.content.publishConfig?.access).toEqual('public');
|
||||
// expect(packageJsonFile.content.publishConfig?.access)
|
||||
// .toEqual('public');
|
||||
if (packageJsonFile.content.publishConfig?.access !== 'public') {
|
||||
throw new Error(
|
||||
`Package ${packageJsonFile.file} does not have publishConfig.access: 'public'`,
|
||||
|
|
|
@ -10,7 +10,9 @@ import type {HandlerEvent, HandlerResponse} from '@netlify/functions';
|
|||
const CookieName = 'DocusaurusPlaygroundName';
|
||||
|
||||
const PlaygroundConfigs = {
|
||||
// codesandbox: 'https://codesandbox.io/s/docusaurus', // Do not use this one, see https://github.com/codesandbox/codesandbox-client/issues/5683#issuecomment-1023252459
|
||||
// Do not use this one, see
|
||||
// https://github.com/codesandbox/codesandbox-client/issues/5683#issuecomment-1023252459
|
||||
// codesandbox: 'https://codesandbox.io/s/docusaurus',
|
||||
codesandbox:
|
||||
'https://codesandbox.io/s/github/facebook/docusaurus/tree/main/examples/classic',
|
||||
|
||||
|
@ -69,14 +71,11 @@ export function readPlaygroundName(
|
|||
: {};
|
||||
const playgroundName: string | undefined = parsedCookie[CookieName];
|
||||
|
||||
if (playgroundName) {
|
||||
if (isValidPlaygroundName(playgroundName)) {
|
||||
return playgroundName;
|
||||
} else {
|
||||
console.error(
|
||||
`playgroundName found in cookie was invalid: ${playgroundName}`,
|
||||
);
|
||||
}
|
||||
if (!isValidPlaygroundName(playgroundName)) {
|
||||
console.error(
|
||||
`playgroundName found in cookie was invalid: ${playgroundName}`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
return playgroundName;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,6 @@ import type {Handler} from '@netlify/functions';
|
|||
|
||||
import {createPlaygroundResponse} from '../functionUtils/playgroundUtils';
|
||||
|
||||
export const handler: Handler = async function (_event, _context) {
|
||||
export const handler: Handler = async function handler(_event, _context) {
|
||||
return createPlaygroundResponse('codesandbox');
|
||||
};
|
||||
|
|
|
@ -9,6 +9,6 @@ import type {Handler} from '@netlify/functions';
|
|||
|
||||
import {createPlaygroundResponse} from '../functionUtils/playgroundUtils';
|
||||
|
||||
export const handler: Handler = async function (_event, _context) {
|
||||
export const handler: Handler = async function handler(_event, _context) {
|
||||
return createPlaygroundResponse('stackblitz');
|
||||
};
|
||||
|
|
|
@ -26,16 +26,16 @@ async function generateTemplateExample(template) {
|
|||
`generating ${template} template for codesandbox in the examples folder...`,
|
||||
);
|
||||
|
||||
// run the docusaurus script to bootstrap the template in the examples folder
|
||||
// run the docusaurus script to create the template in the examples folder
|
||||
const command = template.endsWith('-typescript')
|
||||
? template.replace('-typescript', ' -- --typescript')
|
||||
: template;
|
||||
shell.exec(
|
||||
// /!\ we use the published init script on purpose,
|
||||
// because using the local init script is too early and could generate upcoming/unavailable config options
|
||||
// remember CodeSandbox templates will use the published version, not the repo version
|
||||
// because using the local init script is too early and could generate
|
||||
// upcoming/unavailable config options. Remember CodeSandbox templates
|
||||
// will use the published version, not the repo version
|
||||
`npm init docusaurus@latest examples/${template} ${command}`,
|
||||
// `node ./packages/docusaurus-init/bin/index.js init examples/${template} ${template}`,
|
||||
);
|
||||
|
||||
// read the content of the package.json
|
||||
|
@ -49,10 +49,10 @@ async function generateTemplateExample(template) {
|
|||
// these example projects are not meant to be published to npm
|
||||
templatePackageJson.private = true;
|
||||
|
||||
// make sure package.json name is not "examples-classic"
|
||||
// the package.json name appear in CodeSandbox UI so let's display a good name!
|
||||
// unfortunately we can't use uppercase or spaces
|
||||
// see also https://github.com/codesandbox/codesandbox-client/pull/5136#issuecomment-763521662
|
||||
// Make sure package.json name is not "examples-classic". The package.json
|
||||
// name appears in CodeSandbox UI so let's display a good name!
|
||||
// Unfortunately we can't use uppercase or spaces... See also
|
||||
// https://github.com/codesandbox/codesandbox-client/pull/5136#issuecomment-763521662
|
||||
templatePackageJson.name =
|
||||
template === 'classic' ? 'docusaurus' : `docusaurus-${template}`;
|
||||
templatePackageJson.description =
|
||||
|
@ -98,12 +98,13 @@ async function generateTemplateExample(template) {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Starters are repositories/branches that only contains a newly initialized Docusaurus site
|
||||
Those are useful for users to inspect (may be more convenient than "examples/classic)
|
||||
Also some tools like Netlify deploy button currently require using the main branch of a dedicated repo
|
||||
See https://github.com/jamstack/jamstack.org/pull/609
|
||||
Button visible here: https://jamstack.org/generators/
|
||||
/**
|
||||
* Starters are repositories/branches that only contains a newly initialized
|
||||
* Docusaurus site. Those are useful for users to inspect (may be more
|
||||
* convenient than "examples/classic) Also some tools like Netlify deploy button
|
||||
* currently require using the main branch of a dedicated repo.
|
||||
* See https://github.com/jamstack/jamstack.org/pull/609
|
||||
* Button visible here: https://jamstack.org/generators/
|
||||
*/
|
||||
function updateStarters() {
|
||||
function forcePushGitSubtree({subfolder, remote, remoteBranch}) {
|
||||
|
|
|
@ -39,7 +39,8 @@ await Promise.all(
|
|||
}),
|
||||
);
|
||||
|
||||
// You should also run optimizt `find website/src/data/showcase -type f -name '*.png'`.
|
||||
// This is not included here because @funboxteam/optimizt doesn't seem to play well with M1
|
||||
// so I had to run this in a Rosetta terminal.
|
||||
// You should also run
|
||||
// optimizt `find website/src/data/showcase -type f -name '*.png'`.
|
||||
// This is not included here because @funboxteam/optimizt doesn't seem to play
|
||||
// well with M1 so I had to run this in a Rosetta terminal.
|
||||
// TODO integrate this as part of the script
|
||||
|
|
|
@ -40,7 +40,7 @@ export default {
|
|||
'@docusaurus/core/lib/client/exports/$1',
|
||||
// Maybe point to a fixture?
|
||||
'@generated/.*': '<rootDir>/jest/emptyModule.js',
|
||||
// TODO maybe use "projects" + multiple configs if we plan to add tests to another theme?
|
||||
// TODO use "projects" + multiple configs if we work on another theme?
|
||||
'@theme/(.*)': '@docusaurus/theme-classic/src/theme/$1',
|
||||
'@site/(.*)': 'website/$1',
|
||||
|
||||
|
|
|
@ -81,7 +81,8 @@ async function copyTemplate(
|
|||
) {
|
||||
await fs.copy(path.resolve(templatesDir, 'shared'), dest);
|
||||
|
||||
// TypeScript variants will copy duplicate resources like CSS & config from base template
|
||||
// TypeScript variants will copy duplicate resources like CSS & config from
|
||||
// base template
|
||||
const tsBaseTemplate = getTypeScriptBaseTemplate(template);
|
||||
if (tsBaseTemplate) {
|
||||
const tsBaseTemplatePath = path.resolve(templatesDir, tsBaseTemplate);
|
||||
|
@ -94,7 +95,8 @@ async function copyTemplate(
|
|||
}
|
||||
|
||||
await fs.copy(path.resolve(templatesDir, template), dest, {
|
||||
// Symlinks don't exist in published NPM packages anymore, so this is only to prevent errors during local testing
|
||||
// Symlinks don't exist in published NPM packages anymore, so this is only
|
||||
// to prevent errors during local testing
|
||||
filter: (filePath) => !fs.lstatSync(filePath).isSymbolicLink(),
|
||||
});
|
||||
}
|
||||
|
@ -278,7 +280,8 @@ export default async function init(
|
|||
shell.exec(useYarn ? 'yarn' : 'npm install --color always', {
|
||||
env: {
|
||||
...process.env,
|
||||
// Force coloring the output, since the command is invoked by shelljs, which is not the interactive shell
|
||||
// Force coloring the output, since the command is invoked by shelljs,
|
||||
// which is not the interactive shell
|
||||
...(supportsColor.stdout ? {FORCE_COLOR: '1'} : {}),
|
||||
},
|
||||
}).code !== 0
|
||||
|
|
|
@ -6,12 +6,17 @@
|
|||
*/
|
||||
|
||||
/**
|
||||
* This PostCSS plugin will remove duplicate/same custom properties (which are actually overridden ones) **only** from `:root` selector.
|
||||
* This PostCSS plugin will remove duplicate/same custom properties (which are
|
||||
* actually overridden ones) **only** from `:root` selector.
|
||||
*
|
||||
* Depending on the presence of an `!important` rule in value of custom property, the following actions will happens:
|
||||
* Depending on the presence of an `!important` rule in value of custom
|
||||
* property, the following actions will happen:
|
||||
*
|
||||
* - If the same custom properties do **not** have an `!important` rule, then all of them will be removed except for the last one (which will actually be applied).
|
||||
* - If the same custom properties have at least one `!important` rule, then only those properties that do not have this rule will be removed.
|
||||
* - If the same custom properties do **not** have an `!important` rule, then
|
||||
* all of them will be removed except for the last one (which will actually be
|
||||
* applied).
|
||||
* - If the same custom properties have at least one `!important` rule, then
|
||||
* only those properties that do not have this rule will be removed.
|
||||
* @returns {import('postcss').Plugin}
|
||||
*/
|
||||
module.exports = function creator() {
|
||||
|
|
|
@ -23,7 +23,7 @@ function interpolate(
|
|||
values.forEach((value, idx) => {
|
||||
const flag = msgs[idx].match(/[a-z]+=$/);
|
||||
res += msgs[idx].replace(/[a-z]+=$/, '');
|
||||
const format = (function () {
|
||||
const format = (() => {
|
||||
if (!flag) {
|
||||
return (a: string | number) => a;
|
||||
}
|
||||
|
|
|
@ -49,9 +49,12 @@ type Options = RemarkAndRehypePluginOptions & {
|
|||
filepath: string;
|
||||
};
|
||||
|
||||
// When this throws, it generally means that there's no metadata file associated with this MDX document
|
||||
// It can happen when using MDX partials (usually starting with _)
|
||||
// That's why it's important to provide the "isMDXPartial" function in config
|
||||
/**
|
||||
* When this throws, it generally means that there's no metadata file associated
|
||||
* with this MDX document. It can happen when using MDX partials (usually
|
||||
* starting with _). That's why it's important to provide the `isMDXPartial`
|
||||
* function in config
|
||||
*/
|
||||
async function readMetadataPath(metadataPath: string) {
|
||||
try {
|
||||
return await readFile(metadataPath, 'utf8');
|
||||
|
@ -62,11 +65,14 @@ async function readMetadataPath(metadataPath: string) {
|
|||
}
|
||||
}
|
||||
|
||||
// Converts assets an object with Webpack require calls code
|
||||
// This is useful for mdx files to reference co-located assets using relative paths
|
||||
// Those assets should enter the Webpack assets pipeline and be hashed
|
||||
// For now, we only handle that for images and paths starting with ./
|
||||
// {image: "./myImage.png"} => {image: require("./myImage.png")}
|
||||
/**
|
||||
* Converts assets an object with Webpack require calls code.
|
||||
* This is useful for mdx files to reference co-located assets using relative
|
||||
* paths. Those assets should enter the Webpack assets pipeline and be hashed.
|
||||
* For now, we only handle that for images and paths starting with `./`:
|
||||
*
|
||||
* `{image: "./myImage.png"}` => `{image: require("./myImage.png")}`
|
||||
*/
|
||||
function createAssetsExportCode(assets: Record<string, unknown>) {
|
||||
if (Object.keys(assets).length === 0) {
|
||||
return 'undefined';
|
||||
|
@ -148,7 +154,7 @@ export default async function mdxLoader(
|
|||
filepath: filePath,
|
||||
};
|
||||
|
||||
let result;
|
||||
let result: string;
|
||||
try {
|
||||
result = await mdx(content, options);
|
||||
} catch (err) {
|
||||
|
@ -156,7 +162,7 @@ export default async function mdxLoader(
|
|||
}
|
||||
|
||||
// MDX partials are MDX files starting with _ or in a folder starting with _
|
||||
// Partial are not expected to have an associated metadata file or front matter
|
||||
// Partial are not expected to have associated metadata files or front matter
|
||||
const isMDXPartial = options.isMDXPartial && options.isMDXPartial(filePath);
|
||||
if (isMDXPartial && hasFrontMatter) {
|
||||
const errorMessage = `Docusaurus MDX partial files should not contain FrontMatter.
|
||||
|
@ -168,9 +174,8 @@ ${JSON.stringify(frontMatter, null, 2)}`;
|
|||
const shouldError = process.env.NODE_ENV === 'test' || process.env.CI;
|
||||
if (shouldError) {
|
||||
return callback(new Error(errorMessage));
|
||||
} else {
|
||||
logger.warn(errorMessage);
|
||||
}
|
||||
logger.warn(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ describe('headings plugin', () => {
|
|||
|
||||
test('should not overwrite `data` on headings', () => {
|
||||
const result = process('# Normal\n', [
|
||||
function () {
|
||||
() => {
|
||||
function transform(tree) {
|
||||
tree.children[0].data = {foo: 'bar'};
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ describe('headings plugin', () => {
|
|||
|
||||
test('should not overwrite `data.hProperties` on headings', () => {
|
||||
const result = process('# Normal\n', [
|
||||
function () {
|
||||
() => {
|
||||
function transform(tree) {
|
||||
tree.children[0].data = {hProperties: {className: ['foo']}};
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ describe('headings plugin', () => {
|
|||
'## Something also',
|
||||
].join('\n\n'),
|
||||
[
|
||||
function () {
|
||||
() => {
|
||||
function transform(tree) {
|
||||
tree.children[1].data = {hProperties: {id: 'here'}};
|
||||
tree.children[3].data = {hProperties: {id: 'something'}};
|
||||
|
|
|
@ -44,7 +44,8 @@ function headings(): Transformer {
|
|||
|
||||
if (parsedHeading.id) {
|
||||
// When there's an id, it is always in the last child node
|
||||
// Sometimes heading is in multiple "parts" (** syntax creates a child node):
|
||||
// Sometimes heading is in multiple "parts" (** syntax creates a child
|
||||
// node):
|
||||
// ## part1 *part2* part3 {#id}
|
||||
const lastNode = headingNode.children[
|
||||
headingNode.children.length - 1
|
||||
|
|
|
@ -46,9 +46,9 @@ export default function search(node: Node): TOCItem[] {
|
|||
});
|
||||
});
|
||||
|
||||
// Keep track of which previous index would be the current heading's direct parent.
|
||||
// Each entry <i> is the last index of the `headings` array at heading level <i>.
|
||||
// We will modify these indices as we iterate through all headings.
|
||||
// Keep track of which previous index would be the current heading's direct
|
||||
// parent. Each entry <i> is the last index of the `headings` array at heading
|
||||
// level <i>. We will modify these indices as we iterate through all headings.
|
||||
// e.g. if an ### H3 was last seen at index 2, then prevIndexForLevel[3] === 2
|
||||
// indices 0 and 1 will remain unused.
|
||||
const prevIndexForLevel = Array(7).fill(-1);
|
||||
|
|
|
@ -116,14 +116,10 @@ async function getImageAbsolutePath(
|
|||
}
|
||||
return imageFilePath;
|
||||
}
|
||||
// We try to convert image urls without protocol to images with require calls
|
||||
// going through webpack ensures that image assets exist at build time
|
||||
else {
|
||||
// relative paths are resolved against the source file's folder
|
||||
const imageFilePath = path.join(path.dirname(filePath), imagePath);
|
||||
await ensureImageFileExist(imageFilePath, filePath);
|
||||
return imageFilePath;
|
||||
}
|
||||
// relative paths are resolved against the source file's folder
|
||||
const imageFilePath = path.join(path.dirname(filePath), imagePath);
|
||||
await ensureImageFileExist(imageFilePath, filePath);
|
||||
return imageFilePath;
|
||||
}
|
||||
|
||||
async function processImageNode(node: Image, context: Context) {
|
||||
|
@ -137,16 +133,16 @@ async function processImageNode(node: Image, context: Context) {
|
|||
|
||||
const parsedUrl = url.parse(node.url);
|
||||
if (parsedUrl.protocol || !parsedUrl.pathname) {
|
||||
// pathname:// is an escape hatch,
|
||||
// in case user does not want his images to be converted to require calls going through webpack loader
|
||||
// we don't have to document this for now,
|
||||
// it's mostly to make next release less risky (2.0.0-alpha.59)
|
||||
// pathname:// is an escape hatch, in case user does not want her images to
|
||||
// be converted to require calls going through webpack loader
|
||||
if (parsedUrl.protocol === 'pathname:') {
|
||||
node.url = node.url.replace('pathname://', '');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// We try to convert image urls without protocol to images with require calls
|
||||
// going through webpack ensures that image assets exist at build time
|
||||
const imagePath = await getImageAbsolutePath(parsedUrl.pathname, context);
|
||||
await toImageRequireNode(node, imagePath, context.filePath);
|
||||
}
|
||||
|
|
|
@ -10,9 +10,10 @@ import type {Transformer, Processor} from 'unified';
|
|||
import type {Code, Parent} from 'mdast';
|
||||
|
||||
// This plugin is mostly to help integrating Docusaurus with translation systems
|
||||
// that do not support well MDX embedded JSX syntax (like Crowdin)
|
||||
// We wrap the JSX syntax in code blocks so that translation tools don't mess-up with the markup
|
||||
// But the JSX inside such code blocks should still be evaluated as JSX
|
||||
// that do not support well MDX embedded JSX syntax (like Crowdin).
|
||||
// We wrap the JSX syntax in code blocks so that translation tools don't mess up
|
||||
// with the markup, but the JSX inside such code blocks should still be
|
||||
// evaluated as JSX
|
||||
// See https://github.com/facebook/docusaurus/pull/4278
|
||||
function plugin(this: Processor): Transformer {
|
||||
const transformer: Transformer = (root) => {
|
||||
|
|
|
@ -61,7 +61,8 @@ function sanitizedFileContent(
|
|||
return sanitizedData;
|
||||
}
|
||||
|
||||
// TODO refactor this new type should be used everywhere instead of passing many params to each method
|
||||
// TODO refactor this new type should be used everywhere instead of passing many
|
||||
// params to each method
|
||||
type MigrationContext = {
|
||||
siteDir: string;
|
||||
newDir: string;
|
||||
|
|
|
@ -179,13 +179,13 @@ declare module '@docusaurus/Interpolate' {
|
|||
Value extends ReactNode,
|
||||
> = Record<ExtractInterpolatePlaceholders<Str>, Value>;
|
||||
|
||||
// TS function overload: if all the values are plain strings, then interpolate returns a simple string
|
||||
// If all the values are plain strings, interpolate returns a simple string
|
||||
export function interpolate<Str extends string>(
|
||||
text: Str,
|
||||
values?: InterpolateValues<Str, string | number>,
|
||||
): string;
|
||||
|
||||
// If values contain any ReactNode, then the return is a ReactNode
|
||||
// If values contain any ReactNode, the return is a ReactNode
|
||||
export function interpolate<Str extends string, Value extends ReactNode>(
|
||||
text: Str,
|
||||
values?: InterpolateValues<Str, Value>,
|
||||
|
|
|
@ -114,7 +114,9 @@ describe('toRedirectFilesMetadata', () => {
|
|||
);
|
||||
|
||||
expect(redirectFiles.map((f) => f.fileAbsolutePath)).toEqual([
|
||||
// path.join(pluginContext.outDir, '/abc.html/index.html'), // Can't be used because /abc.html already exists, and file/folder can't share same name on Unix!
|
||||
// Can't be used because /abc.html already exists, and file/folder can't
|
||||
// share same name on Unix!
|
||||
// path.join(pluginContext.outDir, '/abc.html/index.html'),
|
||||
path.join(pluginContext.outDir, '/abc.html.html'), // Weird but on purpose!
|
||||
path.join(pluginContext.outDir, '/def/index.html'),
|
||||
path.join(pluginContext.outDir, '/xyz/index.html'),
|
||||
|
|
|
@ -39,9 +39,12 @@ export default function collectRedirects(
|
|||
}
|
||||
|
||||
// If users wants to redirect to=/abc and they enable trailingSlash=true then
|
||||
// => we don't want to reject the to=/abc (as only /abc/ is an existing/valid path now)
|
||||
// => we want to redirect to=/abc/ without the user having to change all its redirect plugin options
|
||||
// It should be easy to toggle siteConfig.trailingSlash option without having to change other configs
|
||||
// => we don't want to reject the to=/abc (as only /abc/ is an existing/valid
|
||||
// path now)
|
||||
// => we want to redirect to=/abc/ without the user having to change all its
|
||||
// redirect plugin options
|
||||
// It should be easy to toggle siteConfig.trailingSlash option without having to
|
||||
// change other configs
|
||||
function applyRedirectsTrailingSlash(
|
||||
redirects: RedirectMetadata[],
|
||||
params: ApplyTrailingSlashParams,
|
||||
|
|
|
@ -81,19 +81,12 @@ export function createFromExtensionsRedirects(
|
|||
if (path === '' || path === '/' || alreadyEndsWithAnExtension(path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// /path => /path.html
|
||||
// /path/ => /path.html/
|
||||
function getFrom(ext: string) {
|
||||
if (path.endsWith('/')) {
|
||||
return addTrailingSlash(`${removeTrailingSlash(path)}.${ext}`);
|
||||
} else {
|
||||
return `${path}.${ext}`;
|
||||
}
|
||||
}
|
||||
|
||||
return extensions.map((ext) => ({
|
||||
from: getFrom(ext),
|
||||
// /path => /path.html
|
||||
// /path/ => /path.html/
|
||||
from: path.endsWith('/')
|
||||
? addTrailingSlash(`${removeTrailingSlash(path)}.${ext}`)
|
||||
: `${path}.${ext}`,
|
||||
to: path,
|
||||
}));
|
||||
};
|
||||
|
|
|
@ -25,7 +25,8 @@ export function createToUrl(baseUrl: string, to: string): string {
|
|||
}
|
||||
|
||||
// Create redirect file path
|
||||
// Make sure this path has lower precedence over the original file path when served by host providers!
|
||||
// Make sure this path has lower precedence over the original file path when
|
||||
// served by host providers!
|
||||
// Otherwise it can produce infinite redirect loops!
|
||||
//
|
||||
// See https://github.com/facebook/docusaurus/issues/5055
|
||||
|
@ -39,17 +40,19 @@ function getRedirectFilePath(
|
|||
const filePath = path.dirname(fromPath);
|
||||
// Edge case for https://github.com/facebook/docusaurus/pull/5102
|
||||
// If the redirect source path is /xyz, with file /xyz.html
|
||||
// We can't write the redirect file at /xyz.html/index.html because for Unix FS, a file/folder can't have the same name "xyz.html"
|
||||
// The only possible solution for a redirect file is thus /xyz.html.html (I know, looks suspicious)
|
||||
// We can't write the redirect file at /xyz.html/index.html because for Unix
|
||||
// FS, a file/folder can't have the same name "xyz.html"
|
||||
// The only possible solution for a redirect file is thus /xyz.html.html (I
|
||||
// know, looks suspicious)
|
||||
if (trailingSlash === false && fileName.endsWith('.html')) {
|
||||
return path.join(filePath, `${fileName}.html`);
|
||||
}
|
||||
// If the target path is /xyz, with file /xyz/index.html, we don't want the redirect file to be /xyz.html
|
||||
// otherwise it would be picked in priority and the redirect file would redirect to itself
|
||||
// We prefer the redirect file to be /xyz.html/index.html, served with lower priority for most static hosting tools
|
||||
else {
|
||||
return path.join(filePath, `${fileName}/index.html`);
|
||||
}
|
||||
// If the target path is /xyz, with file /xyz/index.html, we don't want the
|
||||
// redirect file to be /xyz.html, otherwise it would be picked in priority and
|
||||
// the redirect file would redirect to itself. We prefer the redirect file to
|
||||
// be /xyz.html/index.html, served with lower priority for most static hosting
|
||||
// tools
|
||||
return path.join(filePath, `${fileName}/index.html`);
|
||||
}
|
||||
|
||||
export function toRedirectFilesMetadata(
|
||||
|
|
|
@ -83,9 +83,9 @@ function normalizeFrontMatterAuthors(
|
|||
authorInput: string | BlogPostFrontMatterAuthor,
|
||||
): BlogPostFrontMatterAuthor {
|
||||
if (typeof authorInput === 'string') {
|
||||
// Technically, we could allow users to provide an author's name here
|
||||
// IMHO it's better to only support keys here
|
||||
// Reason: a typo in a key would fallback to becoming a name and may end-up un-noticed
|
||||
// Technically, we could allow users to provide an author's name here, but
|
||||
// we only support keys, otherwise, a typo in a key would fallback to
|
||||
// becoming a name and may end up unnoticed
|
||||
return {key: authorInput};
|
||||
}
|
||||
return authorInput;
|
||||
|
@ -137,7 +137,8 @@ export function getBlogPostAuthors(params: AuthorsParam): Author[] {
|
|||
const authors = getFrontMatterAuthors(params);
|
||||
|
||||
if (authorLegacy) {
|
||||
// Technically, we could allow mixing legacy/authors front matter, but do we really want to?
|
||||
// Technically, we could allow mixing legacy/authors front matter, but do we
|
||||
// really want to?
|
||||
if (authors.length > 0) {
|
||||
throw new Error(
|
||||
`To declare blog post authors, use the 'authors' front matter in priority.
|
||||
|
|
|
@ -82,11 +82,10 @@ export function parseBlogFileName(
|
|||
const slugDate = dateString.replace(/-/g, '/');
|
||||
const slug = `/${slugDate}/${folder}${text}`;
|
||||
return {date, text, slug};
|
||||
} else {
|
||||
const text = blogSourceRelative.replace(/(\/index)?\.mdx?$/, '');
|
||||
const slug = `/${text}`;
|
||||
return {date: undefined, text, slug};
|
||||
}
|
||||
const text = blogSourceRelative.replace(/(\/index)?\.mdx?$/, '');
|
||||
const slug = `/${text}`;
|
||||
return {date: undefined, text, slug};
|
||||
}
|
||||
|
||||
function formatBlogPostDate(locale: string, date: Date): string {
|
||||
|
|
|
@ -322,7 +322,8 @@ export default async function pluginContentBlog(
|
|||
modules: {
|
||||
sidebar: aliasedSource(sidebarProp),
|
||||
items: items.map((postID) =>
|
||||
// To tell routes.js this is an import and not a nested object to recurse.
|
||||
// To tell routes.js this is an import and not a nested object
|
||||
// to recurse.
|
||||
({
|
||||
content: {
|
||||
__import: true,
|
||||
|
@ -485,7 +486,8 @@ export default async function pluginContentBlog(
|
|||
// Blog posts title are rendered separately
|
||||
removeContentTitle: true,
|
||||
|
||||
// Assets allow to convert some relative images paths to require() calls
|
||||
// Assets allow to convert some relative images paths to
|
||||
// require() calls
|
||||
createAssets: ({
|
||||
frontMatter,
|
||||
metadata,
|
||||
|
|
|
@ -95,7 +95,8 @@ Entries created:
|
|||
},
|
||||
|
||||
expectSnapshot: () => {
|
||||
// Sort the route config like in src/server/plugins/index.ts for consistent snapshot ordering
|
||||
// Sort the route config like in src/server/plugins/index.ts for
|
||||
// consistent snapshot ordering
|
||||
sortConfig(routeConfigs);
|
||||
expect(routeConfigs).not.toEqual([]);
|
||||
expect(routeConfigs).toMatchSnapshot('route config');
|
||||
|
@ -249,7 +250,8 @@ describe('simple website', () => {
|
|||
.spyOn(cliDocs, 'cliDocsVersionCommand')
|
||||
.mockImplementation();
|
||||
const cli = new commander.Command();
|
||||
// @ts-expect-error: in actual usage, we pass the static commander instead of the new command
|
||||
// @ts-expect-error: in actual usage, we pass the static commander instead
|
||||
// of the new command
|
||||
plugin.extendCli!(cli);
|
||||
cli.parse(['node', 'test', 'docs:version', '1.0.0']);
|
||||
expect(mock).toHaveBeenCalledTimes(1);
|
||||
|
@ -373,7 +375,8 @@ describe('versioned website', () => {
|
|||
.spyOn(cliDocs, 'cliDocsVersionCommand')
|
||||
.mockImplementation();
|
||||
const cli = new commander.Command();
|
||||
// @ts-expect-error: in actual usage, we pass the static commander instead of the new command
|
||||
// @ts-expect-error: in actual usage, we pass the static commander instead
|
||||
// of the new command
|
||||
plugin.extendCli!(cli);
|
||||
cli.parse(['node', 'test', 'docs:version', '2.0.0']);
|
||||
expect(mock).toHaveBeenCalledTimes(1);
|
||||
|
@ -522,7 +525,8 @@ describe('versioned website (community)', () => {
|
|||
.spyOn(cliDocs, 'cliDocsVersionCommand')
|
||||
.mockImplementation();
|
||||
const cli = new commander.Command();
|
||||
// @ts-expect-error: in actual usage, we pass the static commander instead of the new command
|
||||
// @ts-expect-error: in actual usage, we pass the static commander instead
|
||||
// of the new command
|
||||
plugin.extendCli!(cli);
|
||||
cli.parse(['node', 'test', `docs:version:${pluginId}`, '2.0.0']);
|
||||
expect(mock).toHaveBeenCalledTimes(1);
|
||||
|
@ -726,7 +730,8 @@ describe('site with partial autogenerated sidebars', () => {
|
|||
const {content} = await loadSite();
|
||||
const version = content.loadedVersions[0];
|
||||
|
||||
// Only looking at the docs of the autogen sidebar, others metadata should not be affected
|
||||
// Only looking at the docs of the autogen sidebar, others metadata should
|
||||
// not be affected
|
||||
|
||||
expect(getDocById(version, 'API/api-end')).toMatchSnapshot();
|
||||
expect(getDocById(version, 'API/api-overview')).toMatchSnapshot();
|
||||
|
@ -803,7 +808,8 @@ describe('site with custom sidebar items generator', () => {
|
|||
const generatorArg: SidebarItemsGeneratorOptionArgs =
|
||||
customSidebarItemsGeneratorMock.mock.calls[0][0];
|
||||
|
||||
// Make test pass even if docs are in different order and paths are absolutes
|
||||
// Make test pass even if docs are in different order and paths are
|
||||
// absolutes
|
||||
function makeDeterministic(
|
||||
arg: SidebarItemsGeneratorOptionArgs,
|
||||
): SidebarItemsGeneratorOptionArgs {
|
||||
|
|
|
@ -32,10 +32,12 @@ function createVersionedSidebarFile({
|
|||
version: string;
|
||||
}) {
|
||||
// Load current sidebar and create a new versioned sidebars file (if needed).
|
||||
// Note: we don't need the sidebars file to be normalized: it's ok to let plugin option changes to impact older, versioned sidebars
|
||||
// Note: we don't need the sidebars file to be normalized: it's ok to let
|
||||
// plugin option changes to impact older, versioned sidebars
|
||||
const sidebars = loadSidebarsFile(sidebarPath);
|
||||
|
||||
// Do not create a useless versioned sidebars file if sidebars file is empty or sidebars are disabled/false)
|
||||
// Do not create a useless versioned sidebars file if sidebars file is empty
|
||||
// or sidebars are disabled/false)
|
||||
const shouldCreateVersionedSidebarFile = Object.keys(sidebars).length > 0;
|
||||
|
||||
if (shouldCreateVersionedSidebarFile) {
|
||||
|
|
|
@ -28,7 +28,7 @@ export function getActivePlugin(
|
|||
options: GetActivePluginOptions = {},
|
||||
): ActivePlugin | undefined {
|
||||
const activeEntry = Object.entries(allPluginDatas)
|
||||
// A quick route sorting: '/android/foo' should match '/android' instead of '/'
|
||||
// Route sorting: '/android/foo' should match '/android' instead of '/'
|
||||
.sort((a, b) => b[1].path.localeCompare(a[1].path))
|
||||
.find(
|
||||
([, pluginData]) =>
|
||||
|
@ -67,7 +67,7 @@ export const getActiveVersion = (
|
|||
): GlobalVersion | undefined => {
|
||||
const lastVersion = getLatestVersion(data);
|
||||
// Last version is a route like /docs/*,
|
||||
// we need to try to match it last or it would match /docs/version-1.0/* as well
|
||||
// we need to match it last or it would match /docs/version-1.0/* as well
|
||||
const orderedVersionsMetadata = [
|
||||
...data.versions.filter((version) => version !== lastVersion),
|
||||
lastVersion,
|
||||
|
|
|
@ -27,12 +27,13 @@ import type {
|
|||
GetActivePluginOptions,
|
||||
} from '@docusaurus/plugin-content-docs/client';
|
||||
|
||||
// Important to use a constant object to avoid React useEffect executions etc...,
|
||||
// Important to use a constant object to avoid React useEffect executions etc.
|
||||
// see https://github.com/facebook/docusaurus/issues/5089
|
||||
const StableEmptyObject = {};
|
||||
|
||||
// Not using useAllPluginInstancesData() because in blog-only mode, docs hooks are still used by the theme
|
||||
// We need a fail-safe fallback when the docs plugin is not in use
|
||||
// Not using useAllPluginInstancesData() because in blog-only mode, docs hooks
|
||||
// are still used by the theme. We need a fail-safe fallback when the docs
|
||||
// plugin is not in use
|
||||
export const useAllDocsData = (): Record<string, GlobalPluginData> =>
|
||||
// useAllPluginInstancesData('docusaurus-plugin-content-docs');
|
||||
useGlobalData()['docusaurus-plugin-content-docs'] ?? StableEmptyObject;
|
||||
|
|
|
@ -139,7 +139,8 @@ function doProcessDocMetadata({
|
|||
const {
|
||||
custom_edit_url: customEditURL,
|
||||
|
||||
// Strip number prefixes by default (01-MyFolder/01-MyDoc.md => MyFolder/MyDoc) by default,
|
||||
// Strip number prefixes by default
|
||||
// (01-MyFolder/01-MyDoc.md => MyFolder/MyDoc)
|
||||
// but allow to disable this behavior with front matter
|
||||
parse_number_prefixes: parseNumberPrefixes = true,
|
||||
} = frontMatter;
|
||||
|
@ -164,7 +165,8 @@ function doProcessDocMetadata({
|
|||
throw new Error(`Document id "${baseID}" cannot include slash.`);
|
||||
}
|
||||
|
||||
// For autogenerated sidebars, sidebar position can come from filename number prefix or front matter
|
||||
// For autogenerated sidebars, sidebar position can come from filename number
|
||||
// prefix or front matter
|
||||
const sidebarPosition: number | undefined =
|
||||
frontMatter.sidebar_position ?? numberPrefix;
|
||||
|
||||
|
@ -205,8 +207,9 @@ function doProcessDocMetadata({
|
|||
numberPrefixParser: options.numberPrefixParser,
|
||||
});
|
||||
|
||||
// Note: the title is used by default for page title, sidebar label, pagination buttons...
|
||||
// frontMatter.title should be used in priority over contentTitle (because it can contain markdown/JSX syntax)
|
||||
// Note: the title is used by default for page title, sidebar label,
|
||||
// pagination buttons... frontMatter.title should be used in priority over
|
||||
// contentTitle (because it can contain markdown/JSX syntax)
|
||||
const title: string = frontMatter.title ?? contentTitle ?? baseID;
|
||||
|
||||
const description: string = frontMatter.description ?? excerpt ?? '';
|
||||
|
@ -233,9 +236,8 @@ function doProcessDocMetadata({
|
|||
? versionMetadata.versionEditUrlLocalized
|
||||
: versionMetadata.versionEditUrl;
|
||||
return getEditUrl(relativeFilePath, baseVersionEditUrl);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Assign all of object properties during instantiation (if possible) for
|
||||
|
@ -361,9 +363,8 @@ export function getMainDocId({
|
|||
doc.id === firstDocIdOfFirstSidebar ||
|
||||
doc.unversionedId === firstDocIdOfFirstSidebar,
|
||||
)!;
|
||||
} else {
|
||||
return docs[0];
|
||||
}
|
||||
return docs[0];
|
||||
}
|
||||
|
||||
return getMainDoc().unversionedId;
|
||||
|
@ -407,7 +408,8 @@ export function toCategoryIndexMatcherParam({
|
|||
}
|
||||
|
||||
/**
|
||||
* guides/sidebar/autogenerated.md -> 'autogenerated', '.md', ['sidebar', 'guides']
|
||||
* `guides/sidebar/autogenerated.md` ->
|
||||
* `'autogenerated', '.md', ['sidebar', 'guides']`
|
||||
*/
|
||||
export function splitPath(str: string): {
|
||||
/**
|
||||
|
@ -428,15 +430,17 @@ export function splitPath(str: string): {
|
|||
}
|
||||
|
||||
// Return both doc ids
|
||||
// TODO legacy retro-compatibility due to old versioned sidebars using versioned doc ids
|
||||
// ("id" should be removed & "versionedId" should be renamed to "id")
|
||||
// TODO legacy retro-compatibility due to old versioned sidebars using
|
||||
// versioned doc ids ("id" should be removed & "versionedId" should be renamed
|
||||
// to "id")
|
||||
export function getDocIds(doc: DocMetadataBase): [string, string] {
|
||||
return [doc.unversionedId, doc.id];
|
||||
}
|
||||
|
||||
// docs are indexed by both versioned and unversioned ids at the same time
|
||||
// TODO legacy retro-compatibility due to old versioned sidebars using versioned doc ids
|
||||
// ("id" should be removed & "versionedId" should be renamed to "id")
|
||||
// TODO legacy retro-compatibility due to old versioned sidebars using
|
||||
// versioned doc ids ("id" should be removed & "versionedId" should be renamed
|
||||
// to "id")
|
||||
export function createDocsByIdIndex<
|
||||
Doc extends {id: string; unversionedId: string},
|
||||
>(docs: Doc[]): Record<string, Doc> {
|
||||
|
|
|
@ -8,15 +8,16 @@
|
|||
import type {NumberPrefixParser} from '@docusaurus/plugin-content-docs';
|
||||
|
||||
// Best-effort to avoid parsing some patterns as number prefix
|
||||
const IgnoredPrefixPatterns = (function () {
|
||||
const IgnoredPrefixPatterns = (() => {
|
||||
// ignore common date-like patterns: https://github.com/facebook/docusaurus/issues/4640
|
||||
const DateLikePrefixRegex =
|
||||
/^((\d{2}|\d{4})[-_.]\d{2}([-_.](\d{2}|\d{4}))?)(.*)$/;
|
||||
|
||||
// ignore common versioning patterns: https://github.com/facebook/docusaurus/issues/4653
|
||||
// note: we could try to parse float numbers in filenames but that is probably not worth it
|
||||
// as a version such as "8.0" can be interpreted as both a version and a float
|
||||
// User can configure his own NumberPrefixParser if he wants 8.0 to be interpreted as a float
|
||||
// note: we could try to parse float numbers in filenames but that is
|
||||
// probably not worth it as a version such as "8.0" can be interpreted as both
|
||||
// a version and a float. User can configure her own NumberPrefixParser if
|
||||
// she wants 8.0 to be interpreted as a float
|
||||
const VersionLikePrefixRegex = /^(\d+[-_.]\d+)(.*)$/;
|
||||
|
||||
return new RegExp(
|
||||
|
|
|
@ -148,8 +148,9 @@ export function validateOptions({
|
|||
let options = userOptions;
|
||||
|
||||
if (options.sidebarCollapsible === false) {
|
||||
// When sidebarCollapsible=false and sidebarCollapsed=undefined, we don't want to have the inconsistency warning
|
||||
// We let options.sidebarCollapsible become the default value for options.sidebarCollapsed
|
||||
// When sidebarCollapsible=false and sidebarCollapsed=undefined, we don't
|
||||
// want to have the inconsistency warning. We let options.sidebarCollapsible
|
||||
// become the default value for options.sidebarCollapsed
|
||||
if (typeof options.sidebarCollapsed === 'undefined') {
|
||||
options = {
|
||||
...options,
|
||||
|
|
|
@ -45,7 +45,8 @@ declare module '@docusaurus/plugin-content-docs' {
|
|||
sidebarPath?: string | false | undefined;
|
||||
};
|
||||
|
||||
// TODO support custom version banner? {type: "error", content: "html content"}
|
||||
// TODO support custom version banner?
|
||||
// {type: "error", content: "html content"}
|
||||
export type VersionBanner = 'unreleased' | 'unmaintained';
|
||||
export type VersionOptions = {
|
||||
path?: string;
|
||||
|
|
|
@ -73,7 +73,8 @@ export async function createCategoryGeneratedIndexRoutes({
|
|||
modules: {
|
||||
categoryGeneratedIndex: aliasedSource(propData),
|
||||
},
|
||||
// Same as doc, this sidebar route attribute permits to associate this subpage to the given sidebar
|
||||
// Same as doc, this sidebar route attribute permits to associate this
|
||||
// subpage to the given sidebar
|
||||
...(sidebar && {sidebar}),
|
||||
};
|
||||
}
|
||||
|
@ -109,7 +110,8 @@ export async function createDocRoutes({
|
|||
content: metadataItem.source,
|
||||
},
|
||||
// Because the parent (DocPage) comp need to access it easily
|
||||
// This permits to render the sidebar once without unmount/remount when navigating (and preserve sidebar state)
|
||||
// This permits to render the sidebar once without unmount/remount when
|
||||
// navigating (and preserve sidebar state)
|
||||
...(metadataItem.sidebar && {
|
||||
sidebar: metadataItem.sidebar,
|
||||
}),
|
||||
|
|
|
@ -205,7 +205,8 @@ describe('processSidebars', () => {
|
|||
link: {
|
||||
type: 'generated-index',
|
||||
slug: 'generated-cat-index-slug',
|
||||
// @ts-expect-error: TODO undefined should be allowed here, typing error needing refactor
|
||||
// @ts-expect-error: TODO undefined should be allowed here,
|
||||
// typing error needing refactor
|
||||
permalink: undefined,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -47,7 +47,8 @@ export type CategoryMetadataFile = {
|
|||
className?: string;
|
||||
link?: SidebarItemCategoryLinkConfig | null;
|
||||
|
||||
// TODO should we allow "items" here? how would this work? would an "autogenerated" type be allowed?
|
||||
// TODO should we allow "items" here? how would this work? would an
|
||||
// "autogenerated" type be allowed?
|
||||
// This mkdocs plugin do something like that: https://github.com/lukasgeiter/mkdocs-awesome-pages-plugin/
|
||||
// cf comment: https://github.com/facebook/docusaurus/issues/3464#issuecomment-784765199
|
||||
};
|
||||
|
@ -56,16 +57,20 @@ type WithPosition<T> = T & {position?: number};
|
|||
|
||||
/**
|
||||
* A representation of the fs structure. For each object entry:
|
||||
* If it's a folder, the key is the directory name, and value is the directory content;
|
||||
* If it's a doc file, the key is the doc id prefixed with '$doc$/', and value is null
|
||||
* If it's a folder, the key is the directory name, and value is the directory
|
||||
* content; If it's a doc file, the key is the doc id prefixed with '$doc$/',
|
||||
* and value is null
|
||||
*/
|
||||
type Dir = {
|
||||
[item: string]: Dir | null;
|
||||
};
|
||||
|
||||
// TODO I now believe we should read all the category metadata files ahead of time: we may need this metadata to customize docs metadata
|
||||
// Example use-case being able to disable number prefix parsing at the folder level, or customize the default route path segment for an intermediate directory...
|
||||
// TODO later if there is `CategoryFolder/with-category-name-doc.md`, we may want to read the metadata as yaml on it
|
||||
// TODO I now believe we should read all the category metadata files ahead of
|
||||
// time: we may need this metadata to customize docs metadata
|
||||
// Example use-case being able to disable number prefix parsing at the folder
|
||||
// level, or customize the default base slug for an intermediate directory
|
||||
// TODO later if there is `CategoryFolder/with-category-name-doc.md`, we may
|
||||
// want to read the metadata as yaml on it
|
||||
// see https://github.com/facebook/docusaurus/issues/3464#issuecomment-818670449
|
||||
async function readCategoryMetadataFile(
|
||||
categoryDirPath: string,
|
||||
|
@ -142,7 +147,8 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
|||
* Step 2. Turn the linear file list into a tree structure.
|
||||
*/
|
||||
function treeify(docs: SidebarItemsGeneratorDoc[]): Dir {
|
||||
// Get the category breadcrumb of a doc (relative to the dir of the autogenerated sidebar item)
|
||||
// Get the category breadcrumb of a doc (relative to the dir of the
|
||||
// autogenerated sidebar item)
|
||||
// autogenDir=a/b and docDir=a/b/c/d => returns [c, d]
|
||||
// autogenDir=a/b and docDir=a/b => returns []
|
||||
// TODO: try to use path.relative()
|
||||
|
@ -169,7 +175,7 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
|||
}
|
||||
|
||||
/**
|
||||
* Step 3. Recursively transform the tree-like file structure to sidebar items.
|
||||
* Step 3. Recursively transform the tree-like structure to sidebar items.
|
||||
* (From a record to an array of items, akin to normalizing shorthand)
|
||||
*/
|
||||
function generateSidebar(fsModel: Dir): Promise<WithPosition<SidebarItem>[]> {
|
||||
|
@ -182,7 +188,8 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
|||
type: 'doc',
|
||||
id,
|
||||
position,
|
||||
// We don't want these fields to magically appear in the generated sidebar
|
||||
// We don't want these fields to magically appear in the generated
|
||||
// sidebar
|
||||
...(label !== undefined && {label}),
|
||||
...(className !== undefined && {className}),
|
||||
};
|
||||
|
@ -225,13 +232,12 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
|||
if (link !== undefined) {
|
||||
if (link && link.type === 'doc') {
|
||||
return findDocByLocalId(link.id)?.id || getDoc(link.id).id;
|
||||
} else {
|
||||
// We don't continue for other link types on purpose!
|
||||
// IE if user decide to use type "generated-index", we should not pick a README.md file as the linked doc
|
||||
return undefined;
|
||||
}
|
||||
// If a link is explicitly specified, we won't apply conventions
|
||||
return undefined;
|
||||
}
|
||||
// Apply default convention to pick index.md, README.md or <categoryName>.md as the category doc
|
||||
// Apply default convention to pick index.md, README.md or
|
||||
// <categoryName>.md as the category doc
|
||||
return findConventionalCategoryDocLink()?.id;
|
||||
}
|
||||
|
||||
|
@ -279,10 +285,11 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
|||
}
|
||||
|
||||
/**
|
||||
* Step 4. Recursively sort the categories/docs + remove the "position" attribute from final output.
|
||||
* Note: the "position" is only used to sort "inside" a sidebar slice. It is not
|
||||
* used to sort across multiple consecutive sidebar slices (ie a whole Category
|
||||
* composed of multiple autogenerated items)
|
||||
* Step 4. Recursively sort the categories/docs + remove the "position"
|
||||
* attribute from final output. Note: the "position" is only used to sort
|
||||
* "inside" a sidebar slice. It is not used to sort across multiple
|
||||
* consecutive sidebar slices (i.e. a whole category composed of multiple
|
||||
* autogenerated items)
|
||||
*/
|
||||
function sortItems(sidebarItems: WithPosition<SidebarItem>[]): SidebarItem[] {
|
||||
const processedSidebarItems = sidebarItems.map((item) => {
|
||||
|
@ -298,7 +305,6 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
|
|||
return sortedSidebarItems.map(({position, ...item}) => item);
|
||||
}
|
||||
// TODO: the whole code is designed for pipeline operator
|
||||
// return getAutogenDocs() |> treeify |> await generateSidebar(^) |> sortItems;
|
||||
const docs = getAutogenDocs();
|
||||
const fsModel = treeify(docs);
|
||||
const sidebarWithPosition = await generateSidebar(fsModel);
|
||||
|
|
|
@ -60,7 +60,8 @@ function toSidebarItemsGeneratorVersion(
|
|||
return pick(version, ['versionName', 'contentPath']);
|
||||
}
|
||||
|
||||
// Handle the generation of autogenerated sidebar items and other post-processing checks
|
||||
// Handle the generation of autogenerated sidebar items and other
|
||||
// post-processing checks
|
||||
async function processSidebar(
|
||||
unprocessedSidebar: NormalizedSidebar,
|
||||
params: SidebarProcessorParams,
|
||||
|
@ -91,7 +92,8 @@ async function processSidebar(
|
|||
async function processAutoGeneratedItem(
|
||||
item: SidebarItemAutogenerated,
|
||||
): Promise<SidebarItem[]> {
|
||||
// TODO the returned type can't be trusted in practice (generator can be user-provided)
|
||||
// TODO the returned type can't be trusted in practice (generator can be
|
||||
// user-provided)
|
||||
const generatedItems = await sidebarItemsGenerator({
|
||||
item,
|
||||
numberPrefixParser,
|
||||
|
@ -106,7 +108,8 @@ async function processSidebar(
|
|||
normalizeItem(generatedItem, {...params, ...sidebarOptions}),
|
||||
);
|
||||
|
||||
// Process again... weird but sidebar item generated might generate some auto-generated items?
|
||||
// Process again... weird but sidebar item generated might generate some
|
||||
// auto-generated items?
|
||||
return processItems(generatedItemsNormalized);
|
||||
}
|
||||
|
||||
|
|
|
@ -205,7 +205,8 @@ export type SidebarItemsGenerator = (
|
|||
Promise<SidebarItem[]>;
|
||||
// Promise<SidebarItemConfig[]>;
|
||||
|
||||
// Also inject the default generator to conveniently wrap/enhance/sort the default sidebar gen logic
|
||||
// Also inject the default generator to conveniently wrap/enhance/sort the
|
||||
// default sidebar gen logic
|
||||
// see https://github.com/facebook/docusaurus/issues/4640#issuecomment-822292320
|
||||
export type SidebarItemsGeneratorOptionArgs = {
|
||||
defaultSidebarItemsGenerator: SidebarItemsGenerator;
|
||||
|
|
|
@ -16,7 +16,6 @@ import type {
|
|||
SidebarCategoriesShorthand,
|
||||
SidebarItemConfig,
|
||||
SidebarItemCategoryWithGeneratedIndex,
|
||||
SidebarItemCategoryWithLink,
|
||||
SidebarNavigationItem,
|
||||
} from './types';
|
||||
|
||||
|
@ -46,8 +45,11 @@ export function transformSidebarItems(
|
|||
return sidebar.map(transformRecursive);
|
||||
}
|
||||
|
||||
// Flatten sidebar items into a single flat array (containing categories/docs on the same level)
|
||||
// /!\ order matters (useful for next/prev nav), top categories appear before their child elements
|
||||
/**
|
||||
* Flatten sidebar items into a single flat array (containing categories/docs on
|
||||
* the same level). Order matters (useful for next/prev nav), top categories
|
||||
* appear before their child elements
|
||||
*/
|
||||
function flattenSidebarItems(items: SidebarItem[]): SidebarItem[] {
|
||||
function flattenRecursive(item: SidebarItem): SidebarItem[] {
|
||||
return item.type === 'category'
|
||||
|
@ -196,34 +198,33 @@ export function createSidebarsUtils(sidebars: Sidebars): SidebarsUtils {
|
|||
sidebarName = getSidebarNameByDocId(docId);
|
||||
}
|
||||
|
||||
if (sidebarName) {
|
||||
if (!sidebarNameToNavigationItems[sidebarName]) {
|
||||
throw new Error(
|
||||
`Doc with ID ${docId} wants to display sidebar ${sidebarName} but a sidebar with this name doesn't exist`,
|
||||
);
|
||||
}
|
||||
const navigationItems = sidebarNameToNavigationItems[sidebarName];
|
||||
const currentItemIndex = navigationItems.findIndex((item) => {
|
||||
if (item.type === 'doc') {
|
||||
return item.id === docId;
|
||||
}
|
||||
if (item.type === 'category' && item.link.type === 'doc') {
|
||||
return item.link.id === docId;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (currentItemIndex === -1) {
|
||||
return {sidebarName, next: undefined, previous: undefined};
|
||||
}
|
||||
|
||||
const {previous, next} = getElementsAround(
|
||||
navigationItems,
|
||||
currentItemIndex,
|
||||
);
|
||||
return {sidebarName, previous, next};
|
||||
} else {
|
||||
if (!sidebarName) {
|
||||
return emptySidebarNavigation();
|
||||
}
|
||||
if (!sidebarNameToNavigationItems[sidebarName]) {
|
||||
throw new Error(
|
||||
`Doc with ID ${docId} wants to display sidebar ${sidebarName} but a sidebar with this name doesn't exist`,
|
||||
);
|
||||
}
|
||||
const navigationItems = sidebarNameToNavigationItems[sidebarName];
|
||||
const currentItemIndex = navigationItems.findIndex((item) => {
|
||||
if (item.type === 'doc') {
|
||||
return item.id === docId;
|
||||
}
|
||||
if (item.type === 'category' && item.link.type === 'doc') {
|
||||
return item.link.id === docId;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (currentItemIndex === -1) {
|
||||
return {sidebarName, next: undefined, previous: undefined};
|
||||
}
|
||||
|
||||
const {previous, next} = getElementsAround(
|
||||
navigationItems,
|
||||
currentItemIndex,
|
||||
);
|
||||
return {sidebarName, previous, next};
|
||||
}
|
||||
|
||||
function getCategoryGeneratedIndexList(): SidebarItemCategoryWithGeneratedIndex[] {
|
||||
|
@ -237,8 +238,10 @@ export function createSidebarsUtils(sidebars: Sidebars): SidebarsUtils {
|
|||
});
|
||||
}
|
||||
|
||||
// We identity the category generated index by its permalink (should be unique)
|
||||
// More reliable than using object identity
|
||||
/**
|
||||
* We identity the category generated index by its permalink (should be
|
||||
* unique). More reliable than using object identity
|
||||
*/
|
||||
function getCategoryGeneratedIndexNavigation(
|
||||
categoryGeneratedIndexPermalink: string,
|
||||
): SidebarNavigation {
|
||||
|
@ -257,19 +260,18 @@ export function createSidebarsUtils(sidebars: Sidebars): SidebarsUtils {
|
|||
navigationItems.find(isCurrentCategoryGeneratedIndexItem),
|
||||
)?.[0];
|
||||
|
||||
if (sidebarName) {
|
||||
const navigationItems = sidebarNameToNavigationItems[sidebarName];
|
||||
const currentItemIndex = navigationItems.findIndex(
|
||||
isCurrentCategoryGeneratedIndexItem,
|
||||
);
|
||||
const {previous, next} = getElementsAround(
|
||||
navigationItems,
|
||||
currentItemIndex,
|
||||
);
|
||||
return {sidebarName, previous, next};
|
||||
} else {
|
||||
if (!sidebarName) {
|
||||
return emptySidebarNavigation();
|
||||
}
|
||||
const navigationItems = sidebarNameToNavigationItems[sidebarName];
|
||||
const currentItemIndex = navigationItems.findIndex(
|
||||
isCurrentCategoryGeneratedIndexItem,
|
||||
);
|
||||
const {previous, next} = getElementsAround(
|
||||
navigationItems,
|
||||
currentItemIndex,
|
||||
);
|
||||
return {sidebarName, previous, next};
|
||||
}
|
||||
|
||||
function checkSidebarsDocIds(validDocIds: string[], sidebarFilePath: string) {
|
||||
|
@ -322,11 +324,10 @@ Available document ids are:
|
|||
slug: item.link.slug,
|
||||
label: item.label,
|
||||
};
|
||||
} else {
|
||||
const firstSubItem = getFirstLink(item.items);
|
||||
if (firstSubItem) {
|
||||
return firstSubItem;
|
||||
}
|
||||
}
|
||||
const firstSubItem = getFirstLink(item.items);
|
||||
if (firstSubItem) {
|
||||
return firstSubItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -371,18 +372,6 @@ export function toNavigationLink(
|
|||
return doc;
|
||||
}
|
||||
|
||||
function handleCategory(category: SidebarItemCategoryWithLink): DocNavLink {
|
||||
if (category.link.type === 'doc') {
|
||||
return toDocNavigationLink(getDocById(category.link.id));
|
||||
} else if (category.link.type === 'generated-index') {
|
||||
return {
|
||||
title: category.label,
|
||||
permalink: category.link.permalink,
|
||||
};
|
||||
} else {
|
||||
throw new Error('unexpected category link type');
|
||||
}
|
||||
}
|
||||
if (!navigationItem) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -390,8 +379,15 @@ export function toNavigationLink(
|
|||
if (navigationItem.type === 'doc') {
|
||||
return toDocNavigationLink(getDocById(navigationItem.id));
|
||||
} else if (navigationItem.type === 'category') {
|
||||
return handleCategory(navigationItem);
|
||||
} else {
|
||||
throw new Error('unexpected navigation item');
|
||||
if (navigationItem.link.type === 'doc') {
|
||||
return toDocNavigationLink(getDocById(navigationItem.link.id));
|
||||
} else if (navigationItem.link.type === 'generated-index') {
|
||||
return {
|
||||
title: navigationItem.label,
|
||||
permalink: navigationItem.link.permalink,
|
||||
};
|
||||
}
|
||||
throw new Error('unexpected category link type');
|
||||
}
|
||||
throw new Error('unexpected navigation item');
|
||||
}
|
||||
|
|
|
@ -23,7 +23,8 @@ import {isCategoriesShorthand} from './utils';
|
|||
import type {CategoryMetadataFile} from './generator';
|
||||
|
||||
// NOTE: we don't add any default values during validation on purpose!
|
||||
// Config types are exposed to users for typechecking and we use the same type in normalization
|
||||
// Config types are exposed to users for typechecking and we use the same type
|
||||
// in normalization
|
||||
|
||||
const sidebarItemBaseSchema = Joi.object<SidebarItemBase>({
|
||||
className: Joi.string(),
|
||||
|
@ -71,7 +72,8 @@ const sidebarItemCategoryLinkSchema = Joi.object<SidebarItemCategoryLink>()
|
|||
then: Joi.object<SidebarItemCategoryLinkGeneratedIndex>({
|
||||
type: 'generated-index',
|
||||
slug: Joi.string().optional(),
|
||||
// permalink: Joi.string().optional(), // No, this one is not in the user config, only in the normalized version
|
||||
// This one is not in the user config, only in the normalized version
|
||||
// permalink: Joi.string().optional(),
|
||||
title: Joi.string().optional(),
|
||||
description: Joi.string().optional(),
|
||||
image: Joi.string().optional(),
|
||||
|
@ -132,7 +134,8 @@ function validateSidebarItem(item: unknown): asserts item is SidebarItemConfig {
|
|||
return;
|
||||
}
|
||||
// TODO: remove once with proper Joi support
|
||||
// Because we can't use Joi to validate nested items (see above), we do it manually
|
||||
// Because we can't use Joi to validate nested items (see above), we do it
|
||||
// manually
|
||||
if (isCategoriesShorthand(item as SidebarItemConfig)) {
|
||||
Object.values(item as SidebarCategoriesShorthand).forEach((category) =>
|
||||
category.forEach(validateSidebarItem),
|
||||
|
|
|
@ -48,17 +48,16 @@ export default function getSlug({
|
|||
function computeSlug(): string {
|
||||
if (frontMatterSlug?.startsWith('/')) {
|
||||
return frontMatterSlug;
|
||||
} else {
|
||||
const dirNameSlug = getDirNameSlug();
|
||||
if (
|
||||
!frontMatterSlug &&
|
||||
isCategoryIndex(toCategoryIndexMatcherParam({source, sourceDirName}))
|
||||
) {
|
||||
return dirNameSlug;
|
||||
}
|
||||
const baseSlug = frontMatterSlug || baseID;
|
||||
return resolvePathname(baseSlug, getDirNameSlug());
|
||||
}
|
||||
const dirNameSlug = getDirNameSlug();
|
||||
if (
|
||||
!frontMatterSlug &&
|
||||
isCategoryIndex(toCategoryIndexMatcherParam({source, sourceDirName}))
|
||||
) {
|
||||
return dirNameSlug;
|
||||
}
|
||||
const baseSlug = frontMatterSlug || baseID;
|
||||
return resolvePathname(baseSlug, getDirNameSlug());
|
||||
}
|
||||
|
||||
function ensureValidSlug(slug: string): string {
|
||||
|
|
|
@ -31,11 +31,10 @@ import {CURRENT_VERSION_NAME} from './constants';
|
|||
function getVersionFileName(versionName: string): string {
|
||||
if (versionName === CURRENT_VERSION_NAME) {
|
||||
return versionName;
|
||||
} else {
|
||||
// I don't like this "version-" prefix,
|
||||
// but it's for consistency with site/versioned_docs
|
||||
return `version-${versionName}`;
|
||||
}
|
||||
// I don't like this "version-" prefix,
|
||||
// but it's for consistency with site/versioned_docs
|
||||
return `version-${versionName}`;
|
||||
}
|
||||
|
||||
// TODO legacy, the sidebar name is like "version-2.0.0-alpha.66/docs"
|
||||
|
@ -68,7 +67,8 @@ function getDocTranslations(doc: DocMetadata): TranslationFileContent {
|
|||
? {
|
||||
[`${doc.unversionedId}.sidebar_label`]: {
|
||||
message: doc.sidebar_label,
|
||||
description: `The sidebar label for doc with id=${doc.unversionedId}`,
|
||||
description:
|
||||
`The sidebar label for doc with id=${doc.unversionedId}`,
|
||||
},
|
||||
}
|
||||
: undefined),
|
||||
|
@ -253,7 +253,8 @@ function getVersionTranslationFiles(version: LoadedVersion): TranslationFiles {
|
|||
const sidebarsTranslations: TranslationFileContent =
|
||||
getSidebarsTranslations(version);
|
||||
|
||||
// const docsTranslations: TranslationFileContent = getDocsTranslations(version);
|
||||
// const docsTranslations: TranslationFileContent =
|
||||
// getDocsTranslations(version);
|
||||
|
||||
return [
|
||||
{
|
||||
|
|
|
@ -33,11 +33,9 @@ import {resolveSidebarPathOption} from './sidebars';
|
|||
|
||||
// retro-compatibility: no prefix for the default plugin id
|
||||
function addPluginIdPrefix(fileOrDir: string, pluginId: string): string {
|
||||
if (pluginId === DEFAULT_PLUGIN_ID) {
|
||||
return fileOrDir;
|
||||
} else {
|
||||
return `${pluginId}_${fileOrDir}`;
|
||||
}
|
||||
return pluginId === DEFAULT_PLUGIN_ID
|
||||
? fileOrDir
|
||||
: `${pluginId}_${fileOrDir}`;
|
||||
}
|
||||
|
||||
export function getVersionedDocsDirPath(
|
||||
|
@ -96,9 +94,8 @@ async function readVersionsFile(
|
|||
const content = JSON.parse(await fs.readFile(versionsFilePath, 'utf8'));
|
||||
ensureValidVersionArray(content);
|
||||
return content;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function readVersionNames(
|
||||
|
@ -274,15 +271,13 @@ function getDefaultVersionBanner({
|
|||
return null;
|
||||
}
|
||||
// Upcoming versions: unreleased banner
|
||||
else if (
|
||||
if (
|
||||
versionNames.indexOf(versionName) < versionNames.indexOf(lastVersionName)
|
||||
) {
|
||||
return 'unreleased';
|
||||
}
|
||||
// Older versions: display unmaintained banner
|
||||
else {
|
||||
return 'unmaintained';
|
||||
}
|
||||
return 'unmaintained';
|
||||
}
|
||||
|
||||
function getVersionBanner({
|
||||
|
@ -443,8 +438,9 @@ function checkVersionMetadataPaths({
|
|||
);
|
||||
}
|
||||
|
||||
// If the current version defines a path to a sidebar file that does not exist, we throw!
|
||||
// Note: for versioned sidebars, the file may not exist (as we prefer to not create it rather than to create an empty file)
|
||||
// If the current version defines a path to a sidebar file that does not
|
||||
// exist, we throw! Note: for versioned sidebars, the file may not exist (as
|
||||
// we prefer to not create it rather than to create an empty file)
|
||||
// See https://github.com/facebook/docusaurus/issues/3366
|
||||
// See https://github.com/facebook/docusaurus/pull/4775
|
||||
if (
|
||||
|
@ -469,11 +465,10 @@ Please set the docs "sidebarPath" field in your config file to:
|
|||
function getDefaultLastVersionName(versionNames: string[]) {
|
||||
if (versionNames.length === 1) {
|
||||
return versionNames[0];
|
||||
} else {
|
||||
return versionNames.filter(
|
||||
(versionName) => versionName !== CURRENT_VERSION_NAME,
|
||||
)[0];
|
||||
}
|
||||
return versionNames.filter(
|
||||
(versionName) => versionName !== CURRENT_VERSION_NAME,
|
||||
)[0];
|
||||
}
|
||||
|
||||
function checkVersionsOptions(
|
||||
|
@ -544,9 +539,8 @@ function filterVersions(
|
|||
return versionNamesUnfiltered.filter((name) =>
|
||||
(options.onlyIncludeVersions || []).includes(name),
|
||||
);
|
||||
} else {
|
||||
return versionNamesUnfiltered;
|
||||
}
|
||||
return versionNamesUnfiltered;
|
||||
}
|
||||
|
||||
export async function readVersionsMetadata({
|
||||
|
|
|
@ -112,29 +112,28 @@ export default async function pluginContentPages(
|
|||
options.routeBasePath,
|
||||
encodePath(fileToPath(relativeSource)),
|
||||
]);
|
||||
if (isMarkdownSource(relativeSource)) {
|
||||
const content = await fs.readFile(source, 'utf-8');
|
||||
const {
|
||||
frontMatter: unsafeFrontMatter,
|
||||
contentTitle,
|
||||
excerpt,
|
||||
} = parseMarkdownString(content);
|
||||
const frontMatter = validatePageFrontMatter(unsafeFrontMatter);
|
||||
return {
|
||||
type: 'mdx',
|
||||
permalink,
|
||||
source: aliasedSourcePath,
|
||||
title: frontMatter.title ?? contentTitle,
|
||||
description: frontMatter.description ?? excerpt,
|
||||
frontMatter,
|
||||
};
|
||||
} else {
|
||||
if (!isMarkdownSource(relativeSource)) {
|
||||
return {
|
||||
type: 'jsx',
|
||||
permalink,
|
||||
source: aliasedSourcePath,
|
||||
};
|
||||
}
|
||||
const content = await fs.readFile(source, 'utf-8');
|
||||
const {
|
||||
frontMatter: unsafeFrontMatter,
|
||||
contentTitle,
|
||||
excerpt,
|
||||
} = parseMarkdownString(content);
|
||||
const frontMatter = validatePageFrontMatter(unsafeFrontMatter);
|
||||
return {
|
||||
type: 'mdx',
|
||||
permalink,
|
||||
source: aliasedSourcePath,
|
||||
title: frontMatter.title ?? contentTitle,
|
||||
description: frontMatter.description ?? excerpt,
|
||||
frontMatter,
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.all(pagesFiles.map(toMetadata));
|
||||
|
|
|
@ -33,6 +33,8 @@ declare module '@theme/DebugJsonView' {
|
|||
}
|
||||
|
||||
declare module '@theme/DebugLayout' {
|
||||
import type {ReactNode} from 'react';
|
||||
|
||||
export default function DebugLayout(props: {
|
||||
children: ReactNode;
|
||||
}): JSX.Element;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
||||
|
||||
export default (function () {
|
||||
export default (function analyticsModule() {
|
||||
if (!ExecutionEnvironment.canUseDOM) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
|||
import globalData from '@generated/globalData';
|
||||
import type {PluginOptions} from '@docusaurus/plugin-google-gtag';
|
||||
|
||||
export default (function () {
|
||||
export default (function gtagModule() {
|
||||
if (!ExecutionEnvironment.canUseDOM) {
|
||||
return null;
|
||||
}
|
||||
|
@ -19,7 +19,8 @@ export default (function () {
|
|||
|
||||
return {
|
||||
onRouteUpdate({location}: {location: Location}) {
|
||||
// Always refer to the variable on window in-case it gets overridden elsewhere.
|
||||
// Always refer to the variable on window in case it gets overridden
|
||||
// elsewhere.
|
||||
window.gtag('config', trackingID, {
|
||||
page_path: location.pathname,
|
||||
page_title: document.title,
|
||||
|
|
|
@ -40,7 +40,8 @@ export default function pluginGoogleGtag(
|
|||
return {};
|
||||
}
|
||||
return {
|
||||
// Gtag includes GA by default, so we also preconnect to google-analytics.
|
||||
// Gtag includes GA by default, so we also preconnect to
|
||||
// google-analytics.
|
||||
headTags: [
|
||||
{
|
||||
tagName: 'link',
|
||||
|
|
|
@ -9,9 +9,12 @@
|
|||
|
||||
/**
|
||||
* @see https://github.com/endiliey/react-ideal-image/blob/master/index.d.ts
|
||||
* Note: the original type definition is WRONG. getIcon & getMessage receive full state object.
|
||||
* Note: the original type definition is WRONG. getIcon & getMessage receive
|
||||
* full state object.
|
||||
*/
|
||||
declare module '@endiliey/react-ideal-image' {
|
||||
import type {ComponentProps, ComponentType, CSSProperties} from 'react';
|
||||
|
||||
export type LoadingState = 'initial' | 'loading' | 'loaded' | 'error';
|
||||
|
||||
export type State = {
|
||||
|
@ -39,19 +42,21 @@ declare module '@endiliey/react-ideal-image' {
|
|||
|
||||
type ThemeKey = 'placeholder' | 'img' | 'icon' | 'noscript';
|
||||
|
||||
export interface ImageProps {
|
||||
export interface ImageProps extends ComponentProps<'img'> {
|
||||
/**
|
||||
* This function decides what icon to show based on the current state of the component.
|
||||
* This function decides what icon to show based on the current state of the
|
||||
* component.
|
||||
*/
|
||||
getIcon?: (state: State) => IconKey;
|
||||
/**
|
||||
* This function decides what message to show based on the icon (returned from getIcon prop) and
|
||||
* the current state of the component.
|
||||
* This function decides what message to show based on the icon (returned
|
||||
* from `getIcon` prop) and the current state of the component.
|
||||
*/
|
||||
getMessage?: (icon: IconKey, state: State) => string;
|
||||
getMessage?: (icon: IconKey, state: State) => string | null;
|
||||
/**
|
||||
* This function is called as soon as the component enters the viewport and is used to generate urls
|
||||
* based on width and format if props.srcSet doesn't provide src field.
|
||||
* This function is called as soon as the component enters the viewport and
|
||||
* is used to generate urls based on width and format if `props.srcSet`
|
||||
* doesn't provide `src` field.
|
||||
*/
|
||||
getUrl?: (srcType: SrcType) => string;
|
||||
/**
|
||||
|
@ -59,10 +64,11 @@ declare module '@endiliey/react-ideal-image' {
|
|||
*/
|
||||
height: number;
|
||||
/**
|
||||
* This provides a map of the icons. By default, the component uses icons from material design,
|
||||
* implemented as React components with the SVG element. You can customize icons
|
||||
* This provides a map of the icons. By default, the component uses icons
|
||||
* from material design, Implemented as React components with the SVG
|
||||
* element. You can customize icons
|
||||
*/
|
||||
icons: Partial<Record<IconKey, ComponentType>>;
|
||||
icons?: Partial<Record<IconKey, ComponentType>>;
|
||||
/**
|
||||
* This prop takes one of the 2 options, xhr and image.
|
||||
* Read more about it:
|
||||
|
@ -74,9 +80,10 @@ declare module '@endiliey/react-ideal-image' {
|
|||
*/
|
||||
placeholder: {color: string} | {lqip: string};
|
||||
/**
|
||||
* This function decides if image should be downloaded automatically. The default function
|
||||
* returns false for a 2g network, for a 3g network it decides based on props.threshold
|
||||
* and for a 4g network it returns true by default.
|
||||
* This function decides if image should be downloaded automatically. The
|
||||
* default function returns false for a 2g network, for a 3g network it
|
||||
* decides based on `props.threshold` and for a 4g network it returns true
|
||||
* by default.
|
||||
*/
|
||||
shouldAutoDownload?: (options: {
|
||||
connection?: 'slow-2g' | '2g' | '3g' | '4g';
|
||||
|
@ -85,18 +92,20 @@ declare module '@endiliey/react-ideal-image' {
|
|||
possiblySlowNetwork?: boolean;
|
||||
}) => boolean;
|
||||
/**
|
||||
* This provides an array of sources of different format and size of the image.
|
||||
* Read more about it:
|
||||
* This provides an array of sources of different format and size of the
|
||||
* image. Read more about it:
|
||||
* https://github.com/stereobooster/react-ideal-image/blob/master/introduction.md#srcset
|
||||
*/
|
||||
srcSet: SrcType[];
|
||||
/**
|
||||
* This provides a theme to the component. By default, the component uses inline styles,
|
||||
* but it is also possible to use CSS modules and override all styles.
|
||||
* This provides a theme to the component. By default, the component uses
|
||||
* inline styles, but it is also possible to use CSS modules and override
|
||||
* all styles.
|
||||
*/
|
||||
theme?: Partial<Record<ThemeKey, string | CSSProperties>>;
|
||||
/**
|
||||
* Tells how much to wait in milliseconds until consider the download to be slow.
|
||||
* Tells how much to wait in milliseconds until consider the download to be
|
||||
* slow.
|
||||
*/
|
||||
threshold?: number;
|
||||
/**
|
||||
|
@ -105,8 +114,6 @@ declare module '@endiliey/react-ideal-image' {
|
|||
width: number;
|
||||
}
|
||||
|
||||
type IdealImageComponent = ComponentClass<ImageProps>;
|
||||
|
||||
declare const IdealImage: IdealImageComponent;
|
||||
declare const IdealImage: (props: ImageProps) => JSX.Element;
|
||||
export default IdealImage;
|
||||
}
|
||||
|
|
|
@ -12,15 +12,21 @@ declare module '@docusaurus/plugin-ideal-image' {
|
|||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* Specify all widths you want to use; if a specified size exceeds the original image's width, the latter will be used (i.e. images won't be scaled up). You may also declare a default sizes array in the loader options in your webpack.config.js.
|
||||
* Specify all widths you want to use; if a specified size exceeds the
|
||||
* original image's width, the latter will be used (i.e. images won't be
|
||||
* scaled up). You may also declare a default sizes array in the loader
|
||||
* options in your webpack.config.js.
|
||||
*/
|
||||
sizes?: number[];
|
||||
/**
|
||||
* Specify one width you want to use; if the specified size exceeds the original image's width, the latter will be used (i.e. images won't be scaled up)
|
||||
* Specify one width you want to use; if the specified size exceeds the
|
||||
* original image's width, the latter will be used (i.e. images won't be
|
||||
* scaled up)
|
||||
*/
|
||||
size?: number;
|
||||
/**
|
||||
* As an alternative to manually specifying sizes, you can specify min, max and steps, and the sizes will be generated for you.
|
||||
* As an alternative to manually specifying sizes, you can specify min, max
|
||||
* and steps, and the sizes will be generated for you.
|
||||
*/
|
||||
min?: number;
|
||||
/**
|
||||
|
|
|
@ -68,13 +68,12 @@ const getMessage = (icon: IconKey, state: State) => {
|
|||
message: '404. Image not found',
|
||||
description: 'When the image is not found',
|
||||
});
|
||||
} else {
|
||||
return translate({
|
||||
id: 'theme.IdealImageMessage.error',
|
||||
message: 'Error. Click to reload',
|
||||
description: 'When the image fails to load for unknown error',
|
||||
});
|
||||
}
|
||||
return translate({
|
||||
id: 'theme.IdealImageMessage.error',
|
||||
message: 'Error. Click to reload',
|
||||
description: 'When the image fails to load for unknown error',
|
||||
});
|
||||
}
|
||||
default:
|
||||
throw new Error(`Wrong icon: ${icon}`);
|
||||
|
|
|
@ -46,7 +46,7 @@ function getSWBabelLoader() {
|
|||
};
|
||||
}
|
||||
|
||||
export default function (
|
||||
export default function pluginPWA(
|
||||
context: LoadContext,
|
||||
options: PluginOptions,
|
||||
): Plugin<void> {
|
||||
|
|
|
@ -219,8 +219,8 @@ async function registerSW() {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO these events still works in chrome but have been removed from the spec in 2019!
|
||||
// See https://github.com/w3c/manifest/pull/836
|
||||
// TODO these events still works in chrome but have been removed from the spec
|
||||
// in 2019! See https://github.com/w3c/manifest/pull/836
|
||||
function addLegacyAppInstalledEventsListeners() {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (debug) {
|
||||
|
@ -248,7 +248,8 @@ function addLegacyAppInstalledEventsListeners() {
|
|||
await clearRegistrations();
|
||||
});
|
||||
|
||||
// TODO this event still works in chrome but has been removed from the spec in 2019!!!
|
||||
// TODO this event still works in chrome but has been removed from the spec
|
||||
// in 2019!!!
|
||||
window.addEventListener('beforeinstallprompt', async (event) => {
|
||||
if (debug) {
|
||||
console.log(
|
||||
|
@ -256,7 +257,8 @@ function addLegacyAppInstalledEventsListeners() {
|
|||
event,
|
||||
);
|
||||
}
|
||||
// TODO instead of default browser install UI, show custom docusaurus prompt?
|
||||
// TODO instead of default browser install UI, show custom docusaurus
|
||||
// prompt?
|
||||
// event.preventDefault();
|
||||
if (debug) {
|
||||
console.log(
|
||||
|
@ -273,7 +275,7 @@ function addLegacyAppInstalledEventsListeners() {
|
|||
}
|
||||
// After uninstalling the app, if the user doesn't clear all data, then
|
||||
// the previous service worker will continue serving cached files. We
|
||||
// need to clear registrations and reload, otherwise the popup will show.
|
||||
// need to clear registrations and reload, otherwise the popup shows.
|
||||
await clearRegistrations();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -18,10 +18,10 @@ function parseSwParams() {
|
|||
return params;
|
||||
}
|
||||
|
||||
// doc advise against dynamic imports in SW
|
||||
// doc advises against dynamic imports in SW
|
||||
// https://developers.google.com/web/tools/workbox/guides/using-bundlers#code_splitting_and_dynamic_imports
|
||||
// https://twitter.com/sebastienlorber/status/1280155204575518720
|
||||
// but I think it's working fine as it's inlined by webpack, need to double check?
|
||||
// but looks it's working fine as it's inlined by webpack, need to double check
|
||||
async function runSWCustomCode(params) {
|
||||
if (process.env.PWA_SW_CUSTOM) {
|
||||
const customSW = await import(process.env.PWA_SW_CUSTOM);
|
||||
|
@ -70,6 +70,7 @@ function getPossibleURLs(url) {
|
|||
(async () => {
|
||||
const params = parseSwParams();
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const precacheManifest = self.__WB_MANIFEST;
|
||||
const controller = new PrecacheController({
|
||||
fallbackToNetwork: true, // safer to turn this true?
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import remark from 'remark';
|
||||
// import from the transpiled lib because Babel can't transpile `export =` syntax
|
||||
// import from the transpiled lib because Babel can't transpile `export =`
|
||||
// TODO change to `../index` after migrating to ESM
|
||||
import npm2yarn from '../../lib/index';
|
||||
import vfile from 'to-vfile';
|
||||
|
|
|
@ -23,7 +23,7 @@ const ContextReplacementPlugin: typeof webpack.ContextReplacementPlugin =
|
|||
requireFromDocusaurusCore('webpack/lib/ContextReplacementPlugin');
|
||||
|
||||
// Need to be inlined to prevent dark mode FOUC
|
||||
// Make sure that the 'storageKey' is the same as the one in `/theme/hooks/useTheme.js`
|
||||
// Make sure the key is the same as the one in `/theme/hooks/useTheme.js`
|
||||
const ThemeStorageKey = 'theme';
|
||||
const noFlashColorMode = ({
|
||||
defaultMode,
|
||||
|
@ -64,15 +64,17 @@ const noFlashColorMode = ({
|
|||
}
|
||||
})();`;
|
||||
|
||||
// Duplicated constant. Unfortunately we can't import it from theme-common, as we need to support older nodejs versions without ESM support
|
||||
// Duplicated constant. Unfortunately we can't import it from theme-common, as
|
||||
// we need to support older nodejs versions without ESM support
|
||||
// TODO: import from theme-common once we only support Node.js with ESM support
|
||||
// + move all those announcementBar stuff there too
|
||||
export const AnnouncementBarDismissStorageKey =
|
||||
'docusaurus.announcement.dismiss';
|
||||
const AnnouncementBarDismissDataAttribute =
|
||||
'data-announcement-bar-initially-dismissed';
|
||||
// We always render the announcement bar html on the server, to prevent layout shifts on React hydration
|
||||
// The theme can use CSS + the data attribute to hide the announcement bar asap (before React hydration)
|
||||
// We always render the announcement bar html on the server, to prevent layout
|
||||
// shifts on React hydration. The theme can use CSS + the data attribute to hide
|
||||
// the announcement bar asap (before React hydration)
|
||||
const AnnouncementBarInlineJavaScript = `
|
||||
(function() {
|
||||
function isDismissed() {
|
||||
|
|
|
@ -22,7 +22,9 @@ const threshold = 300;
|
|||
// TODO proper detection is currently unreliable!
|
||||
// see https://github.com/wessberg/scroll-behavior-polyfill/issues/16
|
||||
const SupportsNativeSmoothScrolling = false;
|
||||
// const SupportsNativeSmoothScrolling = ExecutionEnvironment.canUseDOM && 'scrollBehavior' in document.documentElement.style;
|
||||
// const SupportsNativeSmoothScrolling =
|
||||
// ExecutionEnvironment.canUseDOM &&
|
||||
// 'scrollBehavior' in document.documentElement.style;
|
||||
|
||||
type CancelScrollTop = () => void;
|
||||
|
||||
|
@ -44,13 +46,14 @@ function smoothScrollTopPolyfill(): CancelScrollTop {
|
|||
}
|
||||
rafRecursion();
|
||||
|
||||
// Break the recursion
|
||||
// Prevents the user from "fighting" against that recursion producing a weird UX
|
||||
// Break the recursion. Prevents the user from "fighting" against that
|
||||
// recursion producing a weird UX
|
||||
return () => raf && cancelAnimationFrame(raf);
|
||||
}
|
||||
|
||||
type UseSmoothScrollTopReturn = {
|
||||
// We use a cancel function because the non-native smooth scroll-top implementation must be interrupted if user scroll down
|
||||
// We use a cancel function because the non-native smooth scroll-top
|
||||
// implementation must be interrupted if user scroll down
|
||||
smoothScrollTop: () => void;
|
||||
cancelScrollToTop: CancelScrollTop;
|
||||
};
|
||||
|
|
|
@ -21,27 +21,24 @@ function BlogPostAuthor({author}: Props): JSX.Element {
|
|||
</Link>
|
||||
)}
|
||||
|
||||
{
|
||||
// Note: only legacy author front matter allow empty name (not frontMatter.authors)
|
||||
name && (
|
||||
<div
|
||||
className="avatar__intro"
|
||||
itemProp="author"
|
||||
itemScope
|
||||
itemType="https://schema.org/Person">
|
||||
<div className="avatar__name">
|
||||
<Link href={url} itemProp="url">
|
||||
<span itemProp="name">{name}</span>
|
||||
</Link>
|
||||
</div>
|
||||
{title && (
|
||||
<small className="avatar__subtitle" itemProp="description">
|
||||
{title}
|
||||
</small>
|
||||
)}
|
||||
{name && (
|
||||
<div
|
||||
className="avatar__intro"
|
||||
itemProp="author"
|
||||
itemScope
|
||||
itemType="https://schema.org/Person">
|
||||
<div className="avatar__name">
|
||||
<Link href={url} itemProp="url">
|
||||
<span itemProp="name">{name}</span>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{title && (
|
||||
<small className="avatar__subtitle" itemProp="description">
|
||||
{title}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -53,8 +53,9 @@ function BlogPostPage(props: Props): JSX.Element {
|
|||
) : undefined
|
||||
}>
|
||||
<Seo
|
||||
// TODO refactor needed: it's a bit annoying but Seo MUST be inside BlogLayout
|
||||
// otherwise default image (set by BlogLayout) would shadow the custom blog post image
|
||||
// TODO refactor needed: it's a bit annoying but Seo MUST be inside
|
||||
// BlogLayout, otherwise default image (set by BlogLayout) would shadow
|
||||
// the custom blog post image
|
||||
title={title}
|
||||
description={description}
|
||||
keywords={keywords}
|
||||
|
|
|
@ -51,7 +51,8 @@ export default function CodeBlock({
|
|||
const prismTheme = usePrismTheme();
|
||||
|
||||
// <pre> tags in markdown map to CodeBlocks and they may contain JSX children.
|
||||
// When the children is not a simple string, we just return a styled block without actually highlighting.
|
||||
// When the children is not a simple string, we just return a styled block
|
||||
// without actually highlighting.
|
||||
if (React.Children.toArray(children).some((el) => isValidElement(el))) {
|
||||
return (
|
||||
<Highlight
|
||||
|
|
|
@ -11,7 +11,8 @@ import {Details as DetailsGeneric} from '@docusaurus/theme-common';
|
|||
import type {Props} from '@theme/Details';
|
||||
import styles from './styles.module.css';
|
||||
|
||||
// Should we have a custom details/summary comp in Infima instead of reusing alert classes?
|
||||
// Should we have a custom details/summary comp in Infima instead of reusing
|
||||
// alert classes?
|
||||
const InfimaClasses = 'alert alert--info';
|
||||
|
||||
export default function Details({...props}: Props): JSX.Element {
|
||||
|
|
|
@ -75,8 +75,9 @@ export default function DocItem(props: Props): JSX.Element {
|
|||
<div
|
||||
className={clsx(ThemeClassNames.docs.docMarkdown, 'markdown')}>
|
||||
{/*
|
||||
Title can be declared inside md content or declared through front matter and added manually
|
||||
To make both cases consistent, the added title is added under the same div.markdown block
|
||||
Title can be declared inside md content or declared through
|
||||
front matter and added manually. To make both cases consistent,
|
||||
the added title is added under the same div.markdown block
|
||||
See https://github.com/facebook/docusaurus/pull/4882#issuecomment-853021120
|
||||
*/}
|
||||
{shouldAddTitle && (
|
||||
|
|
|
@ -168,7 +168,7 @@ function DocPage(props: Props): JSX.Element {
|
|||
return (
|
||||
<>
|
||||
<Head>
|
||||
{/* TODO we should add a core addRoute({htmlClassName}) generic plugin option */}
|
||||
{/* TODO we should add a core addRoute({htmlClassName}) action */}
|
||||
<html className={versionMetadata.className} />
|
||||
</Head>
|
||||
<DocsVersionProvider version={versionMetadata}>
|
||||
|
|
|
@ -48,7 +48,8 @@ export default function DocSidebarItem({
|
|||
}
|
||||
}
|
||||
|
||||
// If we navigate to a category and it becomes active, it should automatically expand itself
|
||||
// If we navigate to a category and it becomes active, it should automatically
|
||||
// expand itself
|
||||
function useAutoExpandActiveCategory({
|
||||
isActive,
|
||||
collapsed,
|
||||
|
@ -67,11 +68,14 @@ function useAutoExpandActiveCategory({
|
|||
}, [isActive, wasActive, collapsed, setCollapsed]);
|
||||
}
|
||||
|
||||
// When a collapsible category has no link, we still link it to its first child during SSR as a temporary fallback
|
||||
// This allows to be able to navigate inside the category even when JS fails to load, is delayed or simply disabled
|
||||
// React hydration becomes an optional progressive enhancement
|
||||
// see https://github.com/facebookincubator/infima/issues/36#issuecomment-772543188
|
||||
// see https://github.com/facebook/docusaurus/issues/3030
|
||||
/**
|
||||
* When a collapsible category has no link, we still link it to its first child
|
||||
* during SSR as a temporary fallback. This allows to be able to navigate inside
|
||||
* the category even when JS fails to load, is delayed or simply disabled
|
||||
* React hydration becomes an optional progressive enhancement
|
||||
* see https://github.com/facebookincubator/infima/issues/36#issuecomment-772543188
|
||||
* see https://github.com/facebook/docusaurus/issues/3030
|
||||
*/
|
||||
function useCategoryHrefWithSSRFallback(
|
||||
item: PropSidebarItemCategory,
|
||||
): string | undefined {
|
||||
|
|
|
@ -19,8 +19,8 @@ import type {MDXComponentsObject} from '@theme/MDXComponents';
|
|||
|
||||
import './styles.css';
|
||||
|
||||
// MDX elements are wrapped through the MDX pragma
|
||||
// In some cases (notably usage with Head/Helmet) we need to unwrap those elements.
|
||||
// MDX elements are wrapped through the MDX pragma. In some cases (notably usage
|
||||
// with Head/Helmet) we need to unwrap those elements.
|
||||
function unwrapMDXElement(element: ReactElement) {
|
||||
if (element?.props?.mdxType && element?.props?.originalType) {
|
||||
const {mdxType, originalType, ...newProps} = element.props;
|
||||
|
@ -55,7 +55,8 @@ const MDXComponents: MDXComponentsObject = {
|
|||
),
|
||||
details: (props): JSX.Element => {
|
||||
const items = React.Children.toArray(props.children) as ReactElement[];
|
||||
// Split summary item from the rest to pass it as a separate prop to the Details theme component
|
||||
// Split summary item from the rest to pass it as a separate prop to the
|
||||
// Details theme component
|
||||
const summary: ReactElement<ComponentProps<'summary'>> = items.find(
|
||||
(item) => item?.props?.mdxType === 'summary',
|
||||
)!;
|
||||
|
|
|
@ -78,8 +78,8 @@ export default function DocsVersionDropdownNavbarItem({
|
|||
? undefined
|
||||
: getVersionMainDoc(dropdownVersion).path;
|
||||
|
||||
// We don't want to render a version dropdown with 0 or 1 item
|
||||
// If we build the site with a single docs version (onlyIncludeVersions: ['1.0.0'])
|
||||
// We don't want to render a version dropdown with 0 or 1 item. If we build
|
||||
// the site with a single docs version (onlyIncludeVersions: ['1.0.0']),
|
||||
// We'd rather render a button instead of a dropdown
|
||||
if (items.length <= 1) {
|
||||
return (
|
||||
|
|
|
@ -25,8 +25,8 @@ const NavbarItemComponents: Record<
|
|||
search: () => SearchNavbarItem,
|
||||
dropdown: () => DropdownNavbarItem,
|
||||
|
||||
// Need to lazy load these items as we don't know for sure the docs plugin is loaded
|
||||
// See https://github.com/facebook/docusaurus/issues/3360
|
||||
// Need to lazy load these items as we don't know for sure the docs plugin is
|
||||
// loaded. See https://github.com/facebook/docusaurus/issues/3360
|
||||
/* eslint-disable @typescript-eslint/no-var-requires, global-require */
|
||||
docsVersion: () => require('@theme/NavbarItem/DocsVersionNavbarItem').default,
|
||||
docsVersionDropdown: () =>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
// By default, the classic theme does not provide any SearchBar implementation
|
||||
// If you swizzled this file, it is your responsibility to provide an implementation
|
||||
// If you swizzled this, it is your responsibility to provide an implementation
|
||||
// Tip: swizzle the SearchBar from the Algolia theme for inspiration:
|
||||
// npm run swizzle @docusaurus/theme-search-algolia SearchBar
|
||||
export {default} from '@docusaurus/Noop';
|
||||
|
|
|
@ -12,7 +12,7 @@ import TOCItems from '@theme/TOCItems';
|
|||
import styles from './styles.module.css';
|
||||
|
||||
// Using a custom className
|
||||
// This prevents TOC highlighting to highlight TOCInline/TOCCollapsible by mistake
|
||||
// This prevents TOCInline/TOCCollapsible getting highlighted by mistake
|
||||
const LINK_CLASS_NAME = 'table-of-contents__link toc-highlight';
|
||||
const LINK_ACTIVE_CLASS_NAME = 'table-of-contents__link--active';
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ function TabsComponent(props: Props): JSX.Element {
|
|||
});
|
||||
const values =
|
||||
valuesProp ??
|
||||
// We only pick keys that we recognize. MDX would inject some keys by default
|
||||
// Only pick keys that we recognize. MDX would inject some keys by default
|
||||
children.map(({props: {value, label, attributes}}) => ({
|
||||
value,
|
||||
label,
|
||||
|
|
|
@ -171,7 +171,7 @@ export function translateThemeConfig({
|
|||
themeConfig,
|
||||
translationFiles,
|
||||
}: {
|
||||
// Why partial? To make TS correctly figure out the contravariance in parameter.
|
||||
// To make TS correctly figure out the contravariance in parameter.
|
||||
// In practice it's always normalized
|
||||
themeConfig: ThemeConfig;
|
||||
translationFiles: TranslationFile[];
|
||||
|
|
|
@ -54,8 +54,8 @@ const NavbarItemBaseSchema = Joi.object({
|
|||
label: Joi.string(),
|
||||
className: Joi.string(),
|
||||
})
|
||||
// We allow any unknown attributes on the links
|
||||
// (users may need additional attributes like target, aria-role, data-customAttribute...)
|
||||
// We allow any unknown attributes on the links (users may need additional
|
||||
// attributes like target, aria-role, data-customAttribute...)
|
||||
.unknown();
|
||||
|
||||
const DefaultNavbarItemSchema = NavbarItemBaseSchema.append({
|
||||
|
@ -251,8 +251,8 @@ const FooterLinkItemSchema = Joi.object({
|
|||
.with('to', 'label')
|
||||
.with('href', 'label')
|
||||
.nand('html', 'label')
|
||||
// We allow any unknown attributes on the links
|
||||
// (users may need additional attributes like target, aria-role, data-customAttribute...)
|
||||
// We allow any unknown attributes on the links (users may need additional
|
||||
// attributes like target, aria-role, data-customAttribute...)
|
||||
.unknown();
|
||||
|
||||
const CustomCssSchema = Joi.alternatives()
|
||||
|
|
|
@ -67,7 +67,8 @@ function applyCollapsedStyle(el: HTMLElement, collapsed: boolean) {
|
|||
}
|
||||
|
||||
/*
|
||||
Lex111: Dynamic transition duration is used in Material design, this technique is good for a large number of items.
|
||||
Lex111: Dynamic transition duration is used in Material design, this technique
|
||||
is good for a large number of items.
|
||||
https://material.io/archive/guidelines/motion/duration-easing.html#duration-easing-dynamic-durations
|
||||
https://github.com/mui-org/material-ui/blob/e724d98eba018e55e1a684236a2037e24bcf050c/packages/material-ui/src/styles/createTransitions.js#L40-L43
|
||||
*/
|
||||
|
@ -151,7 +152,8 @@ type CollapsibleElementType = React.ElementType<
|
|||
Pick<React.HTMLAttributes<unknown>, 'className' | 'onTransitionEnd' | 'style'>
|
||||
>;
|
||||
|
||||
// Prevent hydration layout shift before animations are handled imperatively with JS
|
||||
// Prevent hydration layout shift before animations are handled imperatively
|
||||
// with JS
|
||||
function getSSRStyle(collapsed: boolean) {
|
||||
if (ExecutionEnvironment.canUseDOM) {
|
||||
return undefined;
|
||||
|
@ -167,8 +169,9 @@ type CollapsibleBaseProps = {
|
|||
onCollapseTransitionEnd?: (collapsed: boolean) => void;
|
||||
className?: string;
|
||||
|
||||
// This is mostly useful for details/summary component where ssrStyle is not needed (as details are hidden natively)
|
||||
// and can mess-up with the default native behavior of the browser when JS fails to load or is disabled
|
||||
// This is mostly useful for details/summary component where ssrStyle is not
|
||||
// needed (as details are hidden natively) and can mess up with the default
|
||||
// native behavior of the browser when JS fails to load or is disabled
|
||||
disableSSRStyle?: boolean;
|
||||
};
|
||||
|
||||
|
@ -189,7 +192,8 @@ function CollapsibleBase({
|
|||
|
||||
return (
|
||||
<As
|
||||
// @ts-expect-error: the "too complicated type" is produced from "CollapsibleElementType" being a huge union
|
||||
// @ts-expect-error: the "too complicated type" is produced from
|
||||
// "CollapsibleElementType" being a huge union
|
||||
ref={collapsibleRef}
|
||||
style={disableSSRStyle ? undefined : getSSRStyle(collapsed)}
|
||||
onTransitionEnd={(e: React.TransitionEvent) => {
|
||||
|
@ -215,7 +219,7 @@ function CollapsibleLazy({collapsed, ...props}: CollapsibleBaseProps) {
|
|||
}
|
||||
}, [collapsed]);
|
||||
|
||||
// lazyCollapsed updated in effect so that the first expansion transition can work
|
||||
// lazyCollapsed updated in effect so that first expansion transition can work
|
||||
const [lazyCollapsed, setLazyCollapsed] = useState(collapsed);
|
||||
useLayoutEffect(() => {
|
||||
if (mounted) {
|
||||
|
@ -229,9 +233,10 @@ function CollapsibleLazy({collapsed, ...props}: CollapsibleBaseProps) {
|
|||
}
|
||||
|
||||
type CollapsibleProps = CollapsibleBaseProps & {
|
||||
// Lazy allows to delay the rendering when collapsed => it will render children only after hydration, on first expansion
|
||||
// Required prop: it forces to think if content should be server-rendered or not!
|
||||
// This has perf impact on the SSR output and html file sizes
|
||||
// Lazy allows to delay the rendering when collapsed => it will render
|
||||
// children only after hydration, on first expansion
|
||||
// Required prop: it forces to think if content should be server-rendered
|
||||
// or not! This has perf impact on the SSR output and html file sizes
|
||||
// See https://github.com/facebook/docusaurus/issues/4753
|
||||
lazy: boolean;
|
||||
};
|
||||
|
|
|
@ -41,7 +41,7 @@ function Details({summary, children, ...props}: DetailsProps): JSX.Element {
|
|||
const {collapsed, setCollapsed} = useCollapsible({
|
||||
initialState: !props.open,
|
||||
});
|
||||
// We use a separate prop because it must be set only after animation completes
|
||||
// Use a separate prop because it must be set only after animation completes
|
||||
// Otherwise close anim won't work
|
||||
const [open, setOpen] = useState(props.open);
|
||||
|
||||
|
|
|
@ -15,7 +15,8 @@ const windowSizes = {
|
|||
|
||||
// This "ssr" value is very important to handle hydration FOUC / layout shifts
|
||||
// You have to handle server-rendering explicitly on the call-site
|
||||
// On the server, you may need to render BOTH the mobile/desktop elements (and hide one of them with mediaquery)
|
||||
// On the server, you may need to render BOTH the mobile/desktop elements (and
|
||||
// hide one of them with mediaquery)
|
||||
// We don't return "undefined" on purpose, to make it more explicit
|
||||
ssr: 'ssr',
|
||||
} as const;
|
||||
|
@ -33,7 +34,8 @@ function getWindowSize() {
|
|||
: windowSizes.mobile;
|
||||
}
|
||||
|
||||
// Simulate the SSR window size in dev, so that potential hydration FOUC/layout shift problems can be seen in dev too!
|
||||
// Simulate the SSR window size in dev, so that potential hydration FOUC/layout
|
||||
// shift problems can be seen in dev too!
|
||||
const DevSimulateSSR = process.env.NODE_ENV === 'development' && true;
|
||||
|
||||
// This hook returns an enum value on purpose!
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
|
||||
// These class names are used to style page layouts in Docusaurus
|
||||
// Those are meant to be targeted by user-provided custom CSS selectors
|
||||
// /!\ Please do not modify the classnames! This is a breaking change, and annoying for users!
|
||||
// Please do not modify the classnames! This is a breaking change, and annoying
|
||||
// for users!
|
||||
export const ThemeClassNames = {
|
||||
page: {
|
||||
blogListPage: 'blog-list-page',
|
||||
|
|
|
@ -112,8 +112,9 @@ describe('filterTOC', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
// It's not 100% clear exactly how the TOC should behave under weird heading levels provided by the user
|
||||
// Adding a test so that behavior stays the same over time
|
||||
// It's not 100% clear exactly how the TOC should behave under weird heading
|
||||
// levels provided by the user. Adding a test so that behavior stays the same
|
||||
// over time
|
||||
test('filter invalid heading levels (but possible) TOC', () => {
|
||||
const toc: TOCItem[] = [
|
||||
{
|
||||
|
|
|
@ -76,10 +76,9 @@ function readStorageState({
|
|||
);
|
||||
if (versionExists) {
|
||||
return {preferredVersionName: preferredVersionNameUnsafe};
|
||||
} else {
|
||||
DocsPreferredVersionStorage.clear(pluginId, versionPersistence);
|
||||
return {preferredVersionName: null};
|
||||
}
|
||||
DocsPreferredVersionStorage.clear(pluginId, versionPersistence);
|
||||
return {preferredVersionName: null};
|
||||
}
|
||||
|
||||
const initialState: DocsPreferredVersionState = {};
|
||||
|
@ -144,9 +143,8 @@ export function DocsPreferredVersionContextProvider({
|
|||
{children}
|
||||
</DocsPreferredVersionContextProviderUnsafe>
|
||||
);
|
||||
} else {
|
||||
return children;
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
function DocsPreferredVersionContextProviderUnsafe({
|
||||
|
|
|
@ -20,7 +20,7 @@ import {useLocation} from '@docusaurus/router';
|
|||
// TODO not ideal, see also "useDocs"
|
||||
export const isDocsPluginEnabled: boolean = !!useAllDocsData;
|
||||
|
||||
// Using a Symbol because null is a valid context value (a doc can have no sidebar)
|
||||
// Using a Symbol because null is a valid context value (a doc with no sidebar)
|
||||
// Inspired by https://github.com/jamiebuilds/unstated-next/blob/master/src/unstated-next.tsx
|
||||
const EmptyContextValue: unique symbol = Symbol('EmptyContext');
|
||||
|
||||
|
@ -101,11 +101,10 @@ export function findSidebarCategory(
|
|||
if (item.type === 'category') {
|
||||
if (predicate(item)) {
|
||||
return item;
|
||||
} else {
|
||||
const subItem = findSidebarCategory(item.items, predicate);
|
||||
if (subItem) {
|
||||
return subItem;
|
||||
}
|
||||
}
|
||||
const subItem = findSidebarCategory(item.items, predicate);
|
||||
if (subItem) {
|
||||
return subItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,10 @@ import type {Location, Action} from '@docusaurus/history';
|
|||
|
||||
type HistoryBlockHandler = (location: Location, action: Action) => void | false;
|
||||
|
||||
/*
|
||||
Permits to register a handler that will be called on history actions (pop,push,replace)
|
||||
If the handler returns false, the navigation transition will be blocked/cancelled
|
||||
/**
|
||||
* Permits to register a handler that will be called on history actions (pop,
|
||||
* push, replace) If the handler returns false, the navigation transition will
|
||||
* be blocked/cancelled
|
||||
*/
|
||||
export function useHistoryActionHandler(handler: HistoryBlockHandler): void {
|
||||
const {block} = useHistory();
|
||||
|
@ -32,11 +33,11 @@ export function useHistoryActionHandler(handler: HistoryBlockHandler): void {
|
|||
);
|
||||
}
|
||||
|
||||
/*
|
||||
Permits to register a handler that will be called on history pop navigation (backward/forward)
|
||||
If the handler returns false, the backward/forward transition will be blocked
|
||||
|
||||
Unfortunately there's no good way to detect the "direction" (backward/forward) of the POP event.
|
||||
/**
|
||||
* Permits to register a handler that will be called on history pop navigation
|
||||
* (backward/forward) If the handler returns false, the backward/forward
|
||||
* transition will be blocked. Unfortunately there's no good way to detect the
|
||||
* "direction" (backward/forward) of the POP event.
|
||||
*/
|
||||
export function useHistoryPopHandler(handler: HistoryBlockHandler): void {
|
||||
useHistoryActionHandler((location, action) => {
|
||||
|
|
|
@ -10,8 +10,11 @@
|
|||
/**
|
||||
* Gets the duplicate values in an array.
|
||||
* @param arr The array.
|
||||
* @param comparator Compares two values and returns `true` if they are equal (duplicated).
|
||||
* @returns Value of the elements `v` that have a preceding element `u` where `comparator(u, v) === true`. Values within the returned array are not guaranteed to be unique.
|
||||
* @param comparator Compares two values and returns `true` if they are equal
|
||||
* (duplicated).
|
||||
* @returns Value of the elements `v` that have a preceding element `u` where
|
||||
* `comparator(u, v) === true`. Values within the returned array are not
|
||||
* guaranteed to be unique.
|
||||
*/
|
||||
export function duplicates<T>(
|
||||
arr: readonly T[],
|
||||
|
|
|
@ -16,12 +16,13 @@ import React, {
|
|||
} from 'react';
|
||||
|
||||
/*
|
||||
The idea behind all this is that a specific component must be able to fill a placeholder in the generic layout
|
||||
The doc page should be able to fill the secondary menu of the main mobile navbar.
|
||||
This permits to reduce coupling between the main layout and the specific page.
|
||||
The idea behind all this is that a specific component must be able to fill a
|
||||
placeholder in the generic layout. The doc page should be able to fill the
|
||||
secondary menu of the main mobile navbar. This permits to reduce coupling
|
||||
between the main layout and the specific page.
|
||||
|
||||
This kind of feature is often called portal/teleport/gateway... various unmaintained React libs exist
|
||||
Most up-to-date one: https://github.com/gregberge/react-teleporter
|
||||
This kind of feature is often called portal/teleport/gateway... various
|
||||
unmaintained React libs exist. Most up-to-date one: https://github.com/gregberge/react-teleporter
|
||||
Not sure any of those is safe regarding concurrent mode.
|
||||
*/
|
||||
|
||||
|
|
|
@ -7,19 +7,27 @@
|
|||
|
||||
import {useCallback, useEffect, useLayoutEffect, useRef} from 'react';
|
||||
|
||||
// This hook is like useLayoutEffect, but without the SSR warning
|
||||
// It seems hacky but it's used in many React libs (Redux, Formik...)
|
||||
// Also mentioned here: https://github.com/facebook/react/issues/16956
|
||||
// It is useful when you need to update a ref as soon as possible after a React render (before useEffect)
|
||||
/**
|
||||
* This hook is like useLayoutEffect, but without the SSR warning
|
||||
* It seems hacky but it's used in many React libs (Redux, Formik...)
|
||||
* Also mentioned here: https://github.com/facebook/react/issues/16956
|
||||
* It is useful when you need to update a ref as soon as possible after a React
|
||||
* render (before `useEffect`)
|
||||
*/
|
||||
export const useIsomorphicLayoutEffect =
|
||||
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
||||
|
||||
// Permits to transform an unstable callback (like an arrow function provided as props)
|
||||
// to a "stable" callback that is safe to use in a useEffect dependency array
|
||||
// Useful to avoid React stale closure problems + avoid useless effect re-executions
|
||||
//
|
||||
// Workaround until the React team recommends a good solution, see https://github.com/facebook/react/issues/16956
|
||||
// This generally works has some potential drawbacks, such as https://github.com/facebook/react/issues/16956#issuecomment-536636418
|
||||
/**
|
||||
* Permits to transform an unstable callback (like an arrow function provided as
|
||||
* props) to a "stable" callback that is safe to use in a useEffect dependency
|
||||
* array. Useful to avoid React stale closure problems + avoid useless effect
|
||||
* re-executions
|
||||
*
|
||||
* Workaround until the React team recommends a good solution, see
|
||||
* https://github.com/facebook/react/issues/16956
|
||||
* This generally works but has some potential drawbacks, such as
|
||||
* https://github.com/facebook/react/issues/16956#issuecomment-536636418
|
||||
*/
|
||||
export function useDynamicCallback<T extends (...args: never[]) => unknown>(
|
||||
callback: T,
|
||||
): T {
|
||||
|
@ -29,6 +37,7 @@ export function useDynamicCallback<T extends (...args: never[]) => unknown>(
|
|||
ref.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
// @ts-expect-error: TS is right that this callback may be a supertype of T, but good enough for our use
|
||||
// @ts-expect-error: TS is right that this callback may be a supertype of T,
|
||||
// but good enough for our use
|
||||
return useCallback<T>((...args) => ref.current(...args), []);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
/**
|
||||
* Utility to convert an optional string into a Regex case insensitive and global
|
||||
* Converts an optional string into a Regex case insensitive and global
|
||||
*/
|
||||
export function isRegexpStringMatch(
|
||||
regexAsString?: string,
|
||||
|
|
|
@ -11,8 +11,8 @@ export type StorageType = typeof StorageTypes[number];
|
|||
|
||||
const DefaultStorageType: StorageType = 'localStorage';
|
||||
|
||||
// Will return null browser storage is unavailable (like running Docusaurus in iframe)
|
||||
// See https://github.com/facebook/docusaurus/pull/4501
|
||||
// Will return null browser storage is unavailable (like running Docusaurus in
|
||||
// iframe) See https://github.com/facebook/docusaurus/pull/4501
|
||||
function getBrowserStorage(
|
||||
storageType: StorageType = DefaultStorageType,
|
||||
): Storage | null {
|
||||
|
@ -23,13 +23,12 @@ function getBrowserStorage(
|
|||
}
|
||||
if (storageType === 'none') {
|
||||
return null;
|
||||
} else {
|
||||
try {
|
||||
return window[storageType];
|
||||
} catch (e) {
|
||||
logOnceBrowserStorageNotAvailableWarning(e as Error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
try {
|
||||
return window[storageType];
|
||||
} catch (e) {
|
||||
logOnceBrowserStorageNotAvailableWarning(e as Error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,10 +78,10 @@ Please only call storage APIs in effects and event handlers.`);
|
|||
|
||||
/**
|
||||
* Creates an object for accessing a particular key in localStorage.
|
||||
* The API is fail-safe, and usage of browser storage should be considered unreliable
|
||||
* Local storage might simply be unavailable (iframe + browser security) or operations might fail individually
|
||||
* Please assume that using this API can be a NO-OP
|
||||
* See also https://github.com/facebook/docusaurus/issues/6036
|
||||
* The API is fail-safe, and usage of browser storage should be considered
|
||||
* unreliable. Local storage might simply be unavailable (iframe + browser
|
||||
* security) or operations might fail individually. Please assume that using
|
||||
* this API can be a NO-OP. See also https://github.com/facebook/docusaurus/issues/6036
|
||||
*/
|
||||
export const createStorageSlot = (
|
||||
key: string,
|
||||
|
|
|
@ -36,9 +36,8 @@ export function filterTOC({
|
|||
children: filteredChildren,
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return filteredChildren;
|
||||
}
|
||||
return filteredChildren;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -39,14 +39,15 @@ export function useAlternatePageUtils(): {
|
|||
: `${baseUrlUnlocalized}${locale}/`;
|
||||
}
|
||||
|
||||
// TODO support correct alternate url when localized site is deployed on another domain
|
||||
// TODO support correct alternate url when localized site is deployed on
|
||||
// another domain
|
||||
function createUrl({
|
||||
locale,
|
||||
fullyQualified,
|
||||
}: {
|
||||
locale: string;
|
||||
// For hreflang SEO headers, we need it to be fully qualified (full protocol/domain/path...)
|
||||
// For locale dropdown, using a path is good enough
|
||||
// For hreflang SEO headers, we need it to be fully qualified (full
|
||||
// protocol/domain/path...) or locale dropdown, using a path is good enough
|
||||
fullyQualified: boolean;
|
||||
}) {
|
||||
return `${fullyQualified ? url : ''}${getLocalizedBaseUrl(
|
||||
|
|
|
@ -18,8 +18,8 @@ export type useContextualSearchFiltersReturns = {
|
|||
tags: string[];
|
||||
};
|
||||
|
||||
// We may want to support multiple search engines, don't couple that to Algolia/DocSearch
|
||||
// Maybe users will want to use its own search engine solution
|
||||
// We may want to support multiple search engines, don't couple that to
|
||||
// Algolia/DocSearch. Maybe users want to use their own search engine solution
|
||||
export function useContextualSearchFilters(): useContextualSearchFiltersReturns {
|
||||
const {i18n} = useDocusaurusContext();
|
||||
const allDocsData = useAllDocsData();
|
||||
|
|
|
@ -49,23 +49,29 @@ function createLocalePluralForms(locale: string): LocalePluralForms {
|
|||
};
|
||||
}
|
||||
|
||||
// Poor man's PluralSelector implementation, using an english fallback.
|
||||
// We want a lightweight, future-proof and good-enough solution.
|
||||
// We don't want a perfect and heavy solution.
|
||||
//
|
||||
// Docusaurus classic theme has only 2 deeply nested labels requiring complex plural rules
|
||||
// We don't want to use Intl + PluralRules polyfills + full ICU syntax (react-intl) just for that.
|
||||
//
|
||||
// Notes:
|
||||
// - 2021: 92+% Browsers support Intl.PluralRules, and support will increase in the future
|
||||
// - NodeJS >= 13 has full ICU support by default
|
||||
// - In case of "mismatch" between SSR and Browser ICU support, React keeps working!
|
||||
/**
|
||||
* Poor man's PluralSelector implementation, using an english fallback. We want
|
||||
* a lightweight, future-proof and good-enough solution. We don't want a perfect
|
||||
* and heavy solution.
|
||||
*
|
||||
* Docusaurus classic theme has only 2 deeply nested labels requiring complex
|
||||
* plural rules. We don't want to use Intl + PluralRules polyfills + full ICU
|
||||
* syntax (react-intl) just for that.
|
||||
*
|
||||
* Notes:
|
||||
* - 2021: 92+% Browsers support Intl.PluralRules, and support will increase in
|
||||
* the future
|
||||
* - NodeJS >= 13 has full ICU support by default
|
||||
* - In case of "mismatch" between SSR and Browser ICU support, React keeps
|
||||
* working!
|
||||
*/
|
||||
function useLocalePluralForms(): LocalePluralForms {
|
||||
const {
|
||||
i18n: {currentLocale},
|
||||
} = useDocusaurusContext();
|
||||
return useMemo(() => {
|
||||
// @ts-expect-error checking Intl.PluralRules in case browser doesn't have it (e.g Safari 12-)
|
||||
// @ts-expect-error checking Intl.PluralRules in case browser doesn't
|
||||
// have it (e.g Safari 12-)
|
||||
if (Intl.PluralRules) {
|
||||
try {
|
||||
return createLocalePluralForms(currentLocale);
|
||||
|
@ -94,17 +100,17 @@ function selectPluralMessage(
|
|||
|
||||
if (parts.length === 1) {
|
||||
return parts[0];
|
||||
} else {
|
||||
if (parts.length > localePluralForms.pluralForms.length) {
|
||||
console.error(
|
||||
`For locale=${localePluralForms.locale}, a maximum of ${localePluralForms.pluralForms.length} plural forms are expected (${localePluralForms.pluralForms}), but the message contains ${parts.length} plural forms: ${pluralMessages} `,
|
||||
);
|
||||
}
|
||||
const pluralForm = localePluralForms.select(count);
|
||||
const pluralFormIndex = localePluralForms.pluralForms.indexOf(pluralForm);
|
||||
// In case of not enough plural form messages, we take the last one (other) instead of returning undefined
|
||||
return parts[Math.min(pluralFormIndex, parts.length - 1)];
|
||||
}
|
||||
if (parts.length > localePluralForms.pluralForms.length) {
|
||||
console.error(
|
||||
`For locale=${localePluralForms.locale}, a maximum of ${localePluralForms.pluralForms.length} plural forms are expected (${localePluralForms.pluralForms}), but the message contains ${parts.length} plural forms: ${pluralMessages} `,
|
||||
);
|
||||
}
|
||||
const pluralForm = localePluralForms.select(count);
|
||||
const pluralFormIndex = localePluralForms.pluralForms.indexOf(pluralForm);
|
||||
// In case of not enough plural form messages, we take the last one (other)
|
||||
// instead of returning undefined
|
||||
return parts[Math.min(pluralFormIndex, parts.length - 1)];
|
||||
}
|
||||
|
||||
export function usePluralForm(): {
|
||||
|
|
|
@ -13,7 +13,8 @@ TODO make the hardcoded theme-classic classnames configurable
|
|||
(or add them to ThemeClassNames?)
|
||||
*/
|
||||
|
||||
// If the anchor has no height and is just a "marker" in the dom; we'll use the parent (normally the link text) rect boundaries instead
|
||||
// If the anchor has no height and is just a "marker" in the dom; we'll use the
|
||||
// parent (normally the link text) rect boundaries instead
|
||||
function getVisibleBoundingClientRect(element: HTMLElement): DOMRect {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const hasNoHeight = rect.top === rect.bottom;
|
||||
|
@ -23,8 +24,10 @@ function getVisibleBoundingClientRect(element: HTMLElement): DOMRect {
|
|||
return rect;
|
||||
}
|
||||
|
||||
// Considering we divide viewport into 2 zones of each 50vh
|
||||
// This returns true if an element is in the first zone (ie, appear in viewport, near the top)
|
||||
/**
|
||||
* Considering we divide viewport into 2 zones of each 50vh, this returns true
|
||||
* if an element is in the first zone (ie, appear in viewport, near the top)
|
||||
*/
|
||||
function isInViewportTopHalf(boundingRect: DOMRect) {
|
||||
return boundingRect.top > 0 && boundingRect.bottom < window.innerHeight / 2;
|
||||
}
|
||||
|
@ -54,9 +57,10 @@ function getActiveAnchor(
|
|||
anchorTopOffset: number;
|
||||
},
|
||||
): Element | null {
|
||||
// Naming is hard
|
||||
// The "nextVisibleAnchor" is the first anchor that appear under the viewport top boundary
|
||||
// Note: it does not mean this anchor is visible yet, but if user continues scrolling down, it will be the first to become visible
|
||||
// Naming is hard: The "nextVisibleAnchor" is the first anchor that appear
|
||||
// under the viewport top boundary. It does not mean this anchor is visible
|
||||
// yet, but if user continues scrolling down, it will be the first to become
|
||||
// visible
|
||||
const nextVisibleAnchor = anchors.find((anchor) => {
|
||||
const boundingRect = getVisibleBoundingClientRect(anchor);
|
||||
return boundingRect.top >= anchorTopOffset;
|
||||
|
@ -64,23 +68,22 @@ function getActiveAnchor(
|
|||
|
||||
if (nextVisibleAnchor) {
|
||||
const boundingRect = getVisibleBoundingClientRect(nextVisibleAnchor);
|
||||
// If anchor is in the top half of the viewport: it is the one we consider "active"
|
||||
// (unless it's too close to the top and and soon to be scrolled outside viewport)
|
||||
// If anchor is in the top half of the viewport: it is the one we consider
|
||||
// "active" (unless it's too close to the top and and soon to be scrolled
|
||||
// outside viewport)
|
||||
if (isInViewportTopHalf(boundingRect)) {
|
||||
return nextVisibleAnchor;
|
||||
}
|
||||
// If anchor is in the bottom half of the viewport, or under the viewport, we consider the active anchor is the previous one
|
||||
// This is because the main text appearing in the user screen mostly belong to the previous anchor
|
||||
else {
|
||||
// Returns null for the first anchor, see https://github.com/facebook/docusaurus/issues/5318
|
||||
return anchors[anchors.indexOf(nextVisibleAnchor) - 1] ?? null;
|
||||
}
|
||||
// If anchor is in the bottom half of the viewport, or under the viewport,
|
||||
// we consider the active anchor is the previous one. This is because the
|
||||
// main text appearing in the user screen mostly belong to the previous
|
||||
// anchor. Returns null for the first anchor, see
|
||||
// https://github.com/facebook/docusaurus/issues/5318
|
||||
return anchors[anchors.indexOf(nextVisibleAnchor) - 1] ?? null;
|
||||
}
|
||||
// no anchor under viewport top? (ie we are at the bottom of the page)
|
||||
// => highlight the last anchor found
|
||||
else {
|
||||
return anchors[anchors.length - 1];
|
||||
}
|
||||
return anchors[anchors.length - 1];
|
||||
}
|
||||
|
||||
function getLinkAnchorValue(link: HTMLAnchorElement): string {
|
||||
|
|
|
@ -168,7 +168,8 @@ function DocSearch({
|
|||
const transformItems = useRef<DocSearchModalProps['transformItems']>(
|
||||
(items) =>
|
||||
items.map((item) => {
|
||||
// If Algolia contains a external domain, we should navigate without relative URL
|
||||
// If Algolia contains a external domain, we should navigate without
|
||||
// relative URL
|
||||
if (isRegexpStringMatch(externalUrlRegex, item.url)) {
|
||||
return item;
|
||||
}
|
||||
|
|
|
@ -12,8 +12,8 @@ import type {Props} from '@theme/SearchMetadata';
|
|||
|
||||
// Override default/agnostic SearchMetadata to use Algolia-specific metadata
|
||||
function SearchMetadata({locale, version, tag}: Props): JSX.Element {
|
||||
// Seems safe to consider here the locale is the language,
|
||||
// as the existing docsearch:language filter is afaik a regular string-based filter
|
||||
// Seems safe to consider here the locale is the language, as the existing
|
||||
// docsearch:language filter is afaik a regular string-based filter
|
||||
const language = locale;
|
||||
|
||||
return (
|
||||
|
|
|
@ -8,7 +8,3 @@
|
|||
/// <reference types="@docusaurus/module-type-aliases" />
|
||||
/// <reference types="@docusaurus/theme-common" />
|
||||
/// <reference types="@docusaurus/theme-classic" />
|
||||
|
||||
export type FacetFilters = Required<
|
||||
Required<DocSearchProps>['searchParameters']
|
||||
>['facetFilters'];
|
||||
|
|
|
@ -20,8 +20,8 @@ describe('codeTranslationLocalesToTry', () => {
|
|||
'fr-Latn',
|
||||
]);
|
||||
expect(codeTranslationLocalesToTry('fr-FR')).toEqual(['fr-FR', 'fr']);
|
||||
// Note: "pt" is expanded into "pt-BR", not "pt-PT", as "pt-BR" is more widely used!
|
||||
// See https://github.com/facebook/docusaurus/pull/4536#issuecomment-810088783
|
||||
// Note: "pt" is expanded into "pt-BR", not "pt-PT", as "pt-BR" is more
|
||||
// widely used! See https://github.com/facebook/docusaurus/pull/4536#issuecomment-810088783
|
||||
expect(codeTranslationLocalesToTry('pt')).toEqual([
|
||||
'pt',
|
||||
'pt-BR',
|
||||
|
|
|
@ -15,8 +15,8 @@ function getDefaultLocalesDirPath(): string {
|
|||
// Return an ordered list of locales we should try
|
||||
export function codeTranslationLocalesToTry(locale: string): string[] {
|
||||
const intlLocale = new Intl.Locale(locale);
|
||||
// if locale is just a simple language like "pt", we want to fallback to pt-BR (not pt-PT!)
|
||||
// see https://github.com/facebook/docusaurus/pull/4536#issuecomment-810088783
|
||||
// if locale is just a simple language like "pt", we want to fallback to pt-BR
|
||||
// (not pt-PT!) See https://github.com/facebook/docusaurus/pull/4536#issuecomment-810088783
|
||||
if (intlLocale.language === locale) {
|
||||
const maximizedLocale = intlLocale.maximize(); // pt-Latn-BR`
|
||||
// ["pt","pt-BR"]; ["zh", "zh-Hans"]
|
||||
|
@ -27,9 +27,7 @@ export function codeTranslationLocalesToTry(locale: string): string[] {
|
|||
];
|
||||
}
|
||||
// if locale is like "pt-BR", we want to fallback to "pt"
|
||||
else {
|
||||
return [locale, intlLocale.language!];
|
||||
}
|
||||
return [locale, intlLocale.language!];
|
||||
}
|
||||
|
||||
// Useful to implement getDefaultCodeTranslationMessages() in themes
|
||||
|
|
17
packages/docusaurus-types/src/index.d.ts
vendored
17
packages/docusaurus-types/src/index.d.ts
vendored
|
@ -12,8 +12,8 @@ import type Joi from 'joi';
|
|||
import type {Overwrite, DeepPartial} from 'utility-types';
|
||||
|
||||
// Convert webpack-merge webpack-merge enum to union type
|
||||
// For type retro-compatible webpack-merge upgrade: we used string literals before)
|
||||
// see https://github.com/survivejs/webpack-merge/issues/179
|
||||
// For type retro-compatible webpack-merge upgrade: we used string literals
|
||||
// before) See https://github.com/survivejs/webpack-merge/issues/179
|
||||
type MergeStrategy = 'match' | 'merge' | 'append' | 'prepend' | 'replace';
|
||||
|
||||
export type ReportingSeverity = 'ignore' | 'log' | 'warn' | 'error' | 'throw';
|
||||
|
@ -30,7 +30,8 @@ export interface DocusaurusConfig {
|
|||
tagline: string;
|
||||
title: string;
|
||||
url: string;
|
||||
// trailingSlash undefined = legacy retrocompatible behavior => /file => /file/index.html
|
||||
// trailingSlash undefined = legacy retrocompatible behavior
|
||||
// /file => /file/index.html
|
||||
trailingSlash: boolean | undefined;
|
||||
i18n: I18nConfig;
|
||||
onBrokenLinks: ReportingSeverity;
|
||||
|
@ -73,8 +74,8 @@ export interface DocusaurusConfig {
|
|||
}
|
||||
|
||||
// Docusaurus config, as provided by the user (partial/unnormalized)
|
||||
// This type is used to provide type-safety / IDE auto-complete on the config file
|
||||
// See https://docusaurus.io/docs/typescript-support
|
||||
// This type is used to provide type-safety / IDE auto-complete on the config
|
||||
// file. See https://docusaurus.io/docs/typescript-support
|
||||
export type Config = Overwrite<
|
||||
Partial<DocusaurusConfig>,
|
||||
{
|
||||
|
@ -88,7 +89,8 @@ export type Config = Overwrite<
|
|||
/**
|
||||
* - `type: 'package'`, plugin is in a different package.
|
||||
* - `type: 'project'`, plugin is in the same docusaurus project.
|
||||
* - `type: 'local'`, none of plugin's ancestor directory contains any package.json.
|
||||
* - `type: 'local'`, none of the plugin's ancestor directories contains a
|
||||
* package.json.
|
||||
* - `type: 'synthetic'`, docusaurus generated internal plugin.
|
||||
*/
|
||||
export type DocusaurusPluginVersionInformation =
|
||||
|
@ -259,7 +261,8 @@ export interface Plugin<Content = unknown> {
|
|||
}) => Promise<void>;
|
||||
routesLoaded?: (routes: RouteConfig[]) => void; // TODO remove soon, deprecated (alpha-60)
|
||||
postBuild?: (props: Props & {content: Content}) => Promise<void>;
|
||||
// TODO refactor the configureWebpack API surface: use an object instead of multiple params (requires breaking change)
|
||||
// TODO refactor the configureWebpack API surface: use an object instead of
|
||||
// multiple params (requires breaking change)
|
||||
configureWebpack?: (
|
||||
config: Configuration,
|
||||
isServer: boolean,
|
||||
|
|
|
@ -45,8 +45,8 @@ export default function applyTrailingSlash(
|
|||
|
||||
// Never transform '/' to ''
|
||||
// Never remove the baseUrl trailing slash!
|
||||
// If baseUrl = /myBase/, we want to emit /myBase/index.html and not /myBase.html !
|
||||
// See https://github.com/facebook/docusaurus/issues/5077
|
||||
// If baseUrl = /myBase/, we want to emit /myBase/index.html and not
|
||||
// /myBase.html! See https://github.com/facebook/docusaurus/issues/5077
|
||||
const shouldNotApply = pathname === '/' || pathname === baseUrl;
|
||||
|
||||
const newPathname = shouldNotApply
|
||||
|
|
|
@ -7,11 +7,6 @@
|
|||
|
||||
import Joi from './Joi';
|
||||
|
||||
// Enhance the default Joi.string() type so that it can convert number to strings
|
||||
// If user use front matter "tag: 2021", we shouldn't need to ask the user to write "tag: '2021'"
|
||||
// Also yaml tries to convert patterns like "2019-01-01" to dates automatically
|
||||
// see https://github.com/facebook/docusaurus/issues/4642
|
||||
// see https://github.com/sideway/joi/issues/1442#issuecomment-823997884
|
||||
const JoiFrontMatterString: Joi.Extension = {
|
||||
type: 'string',
|
||||
base: Joi.string(),
|
||||
|
@ -23,4 +18,12 @@ const JoiFrontMatterString: Joi.Extension = {
|
|||
return {value};
|
||||
},
|
||||
};
|
||||
/**
|
||||
* Enhance the default Joi.string() type so that it can convert number to
|
||||
* strings. If user use front matter "tag: 2021", we shouldn't need to ask her
|
||||
* to write "tag: '2021'". Also yaml tries to convert patterns like "2019-01-01"
|
||||
* to dates automatically.
|
||||
* @see https://github.com/facebook/docusaurus/issues/4642
|
||||
* @see https://github.com/sideway/joi/issues/1442#issuecomment-823997884
|
||||
*/
|
||||
export const JoiFrontMatter: typeof Joi = Joi.extend(JoiFrontMatterString);
|
||||
|
|
|
@ -43,13 +43,9 @@ function testMarkdownPluginSchemas(schema: Joi.Schema) {
|
|||
});
|
||||
|
||||
testOK(undefined);
|
||||
testOK([function () {}]);
|
||||
testOK([[function () {}, {attr: 'val'}]]);
|
||||
testOK([
|
||||
[function () {}, {attr: 'val'}],
|
||||
function () {},
|
||||
[function () {}, {attr: 'val'}],
|
||||
]);
|
||||
testOK([() => {}]);
|
||||
testOK([[() => {}, {attr: 'val'}]]);
|
||||
testOK([[() => {}, {attr: 'val'}], () => {}, [() => {}, {attr: 'val'}]]);
|
||||
|
||||
testFail(null);
|
||||
testFail(false);
|
||||
|
@ -58,8 +54,8 @@ function testMarkdownPluginSchemas(schema: Joi.Schema) {
|
|||
testFail([false]);
|
||||
testFail([3]);
|
||||
testFail([[]]);
|
||||
testFail([[function () {}, undefined]]);
|
||||
testFail([[function () {}, true]]);
|
||||
testFail([[() => {}, undefined]]);
|
||||
testFail([[() => {}, true]]);
|
||||
}
|
||||
|
||||
describe('validation schemas', () => {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue