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:
Joshua Chen 2022-01-31 10:31:24 +08:00 committed by GitHub
parent b8ccb869f1
commit aa446b7a9c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
167 changed files with 1157 additions and 960 deletions

View file

@ -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,
},
},

View file

@ -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'`,

View file

@ -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;
}

View file

@ -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');
};

View file

@ -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');
};

View file

@ -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}) {

View file

@ -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

View file

@ -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',

View file

@ -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

View file

@ -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() {

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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'}};

View file

@ -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

View file

@ -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);

View file

@ -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);
}

View file

@ -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) => {

View file

@ -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;

View file

@ -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>,

View file

@ -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'),

View file

@ -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,

View file

@ -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,
}));
};

View file

@ -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(

View file

@ -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.

View file

@ -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 {

View file

@ -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,

View file

@ -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 {

View file

@ -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) {

View file

@ -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,

View file

@ -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;

View file

@ -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> {

View file

@ -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(

View file

@ -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,

View file

@ -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;

View file

@ -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,
}),

View file

@ -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,
},
},

View file

@ -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);

View file

@ -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);
}

View file

@ -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;

View file

@ -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');
}

View file

@ -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),

View file

@ -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 {

View file

@ -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 [
{

View file

@ -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({

View file

@ -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));

View file

@ -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;

View file

@ -7,7 +7,7 @@
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
export default (function () {
export default (function analyticsModule() {
if (!ExecutionEnvironment.canUseDOM) {
return null;
}

View file

@ -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,

View file

@ -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',

View file

@ -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;
}

View file

@ -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;
/**

View file

@ -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}`);

View file

@ -46,7 +46,7 @@ function getSWBabelLoader() {
};
}
export default function (
export default function pluginPWA(
context: LoadContext,
options: PluginOptions,
): Plugin<void> {

View file

@ -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();
}
});

View file

@ -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?

View file

@ -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';

View file

@ -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() {

View file

@ -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;
};

View file

@ -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>
);
}

View file

@ -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}

View file

@ -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

View file

@ -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 {

View file

@ -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 && (

View file

@ -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}>

View file

@ -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 {

View file

@ -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',
)!;

View file

@ -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 (

View file

@ -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: () =>

View file

@ -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';

View file

@ -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';

View file

@ -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,

View file

@ -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[];

View file

@ -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()

View file

@ -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;
};

View file

@ -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);

View file

@ -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!

View file

@ -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',

View file

@ -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[] = [
{

View file

@ -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({

View file

@ -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;
}
}
}

View file

@ -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) => {

View file

@ -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[],

View file

@ -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.
*/

View file

@ -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), []);
}

View file

@ -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,

View file

@ -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,

View file

@ -36,9 +36,8 @@ export function filterTOC({
children: filteredChildren,
},
];
} else {
return filteredChildren;
}
return filteredChildren;
});
}

View file

@ -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(

View file

@ -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();

View file

@ -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(): {

View file

@ -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 {

View file

@ -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;
}

View file

@ -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 (

View file

@ -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'];

View file

@ -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',

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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);

View file

@ -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