feat(content-docs): expose isCategoryIndex matcher to customize conventions (#6451)

Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
This commit is contained in:
Joshua Chen 2022-01-27 00:58:52 +08:00 committed by GitHub
parent 76a8d5f38a
commit 24a895fbc5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 408 additions and 93 deletions

View file

@ -1491,6 +1491,7 @@ Object {
"unversionedId": "installation", "unversionedId": "installation",
}, },
], ],
"isCategoryIndex": [Function],
"item": Object { "item": Object {
"dirName": ".", "dirName": ".",
"type": "autogenerated", "type": "autogenerated",

View file

@ -12,7 +12,7 @@ import {
readVersionDocs, readVersionDocs,
readDocFile, readDocFile,
addDocNavigation, addDocNavigation,
isConventionalDocIndex, isCategoryIndex,
} from '../docs'; } from '../docs';
import {loadSidebars} from '../sidebars'; import {loadSidebars} from '../sidebars';
import {readVersionsMetadata} from '../versions'; import {readVersionsMetadata} from '../versions';
@ -938,108 +938,124 @@ describe('versioned site', () => {
describe('isConventionalDocIndex', () => { describe('isConventionalDocIndex', () => {
test('supports readme', () => { test('supports readme', () => {
expect( expect(
isConventionalDocIndex({ isCategoryIndex({
sourceDirName: 'doesNotMatter', fileName: 'readme',
source: 'readme.md', directories: ['doesNotMatter'],
extension: '.md',
}), }),
).toEqual(true); ).toEqual(true);
expect( expect(
isConventionalDocIndex({ isCategoryIndex({
sourceDirName: 'doesNotMatter', fileName: 'readme',
source: 'readme.mdx', directories: ['doesNotMatter'],
extension: '.mdx',
}), }),
).toEqual(true); ).toEqual(true);
expect( expect(
isConventionalDocIndex({ isCategoryIndex({
sourceDirName: 'doesNotMatter', fileName: 'README',
source: 'README.md', directories: ['doesNotMatter'],
extension: '.md',
}), }),
).toEqual(true); ).toEqual(true);
expect( expect(
isConventionalDocIndex({ isCategoryIndex({
sourceDirName: 'doesNotMatter', fileName: 'ReAdMe',
source: 'parent/ReAdMe', directories: ['doesNotMatter'],
extension: '',
}), }),
).toEqual(true); ).toEqual(true);
}); });
test('supports index', () => { test('supports index', () => {
expect( expect(
isConventionalDocIndex({ isCategoryIndex({
sourceDirName: 'doesNotMatter', fileName: 'index',
source: 'index.md', directories: ['doesNotMatter'],
extension: '.md',
}), }),
).toEqual(true); ).toEqual(true);
expect( expect(
isConventionalDocIndex({ isCategoryIndex({
sourceDirName: 'doesNotMatter', fileName: 'index',
source: 'index.mdx', directories: ['doesNotMatter'],
extension: '.mdx',
}), }),
).toEqual(true); ).toEqual(true);
expect( expect(
isConventionalDocIndex({ isCategoryIndex({
sourceDirName: 'doesNotMatter', fileName: 'INDEX',
source: 'INDEX.md', directories: ['doesNotMatter'],
extension: '.md',
}), }),
).toEqual(true); ).toEqual(true);
expect( expect(
isConventionalDocIndex({ isCategoryIndex({
sourceDirName: 'doesNotMatter', fileName: 'InDeX',
source: 'parent/InDeX', directories: ['doesNotMatter'],
extension: '',
}), }),
).toEqual(true); ).toEqual(true);
}); });
test('supports <categoryName>/<categoryName>.md', () => { test('supports <categoryName>/<categoryName>.md', () => {
expect( expect(
isConventionalDocIndex({ isCategoryIndex({
sourceDirName: 'someCategory', fileName: 'someCategory',
source: 'someCategory', directories: ['someCategory', 'doesNotMatter'],
extension: '',
}), }),
).toEqual(true); ).toEqual(true);
expect( expect(
isConventionalDocIndex({ isCategoryIndex({
sourceDirName: 'someCategory', fileName: 'someCategory',
source: 'someCategory.md', directories: ['someCategory'],
extension: '.md',
}), }),
).toEqual(true); ).toEqual(true);
expect( expect(
isConventionalDocIndex({ isCategoryIndex({
sourceDirName: 'someCategory', fileName: 'someCategory',
source: 'someCategory.mdx', directories: ['someCategory'],
extension: '.mdx',
}), }),
).toEqual(true); ).toEqual(true);
expect( expect(
isConventionalDocIndex({ isCategoryIndex({
sourceDirName: 'some_category', fileName: 'SOME_CATEGORY',
source: 'SOME_CATEGORY.md', directories: ['some_category'],
extension: '.md',
}), }),
).toEqual(true); ).toEqual(true);
expect( expect(
isConventionalDocIndex({ isCategoryIndex({
sourceDirName: 'some_category', fileName: 'some_category',
source: 'parent/some_category', directories: ['some_category'],
extension: '',
}), }),
).toEqual(true); ).toEqual(true);
}); });
test('reject other cases', () => { test('reject other cases', () => {
expect( expect(
isConventionalDocIndex({ isCategoryIndex({
sourceDirName: 'someCategory', fileName: 'some_Category',
source: 'some_Category', directories: ['someCategory'],
extension: '',
}), }),
).toEqual(false); ).toEqual(false);
expect( expect(
isConventionalDocIndex({ isCategoryIndex({
sourceDirName: 'doesNotMatter', fileName: 'read_me',
source: 'read_me', directories: ['doesNotMatter'],
extension: '',
}), }),
).toEqual(false); ).toEqual(false);
expect( expect(
isConventionalDocIndex({ isCategoryIndex({
sourceDirName: 'doesNotMatter', fileName: 'the index',
source: 'the index', directories: ['doesNotMatter'],
extension: '',
}), }),
).toEqual(false); ).toEqual(false);
}); });

View file

@ -8,7 +8,7 @@
import path from 'path'; import path from 'path';
import fs from 'fs-extra'; import fs from 'fs-extra';
import logger from '@docusaurus/logger'; import logger from '@docusaurus/logger';
import {keyBy, last} from 'lodash'; import {keyBy} from 'lodash';
import { import {
aliasedSitePath, aliasedSitePath,
getEditUrl, getEditUrl,
@ -41,6 +41,8 @@ import {toDocNavigationLink, toNavigationLink} from './sidebars/utils';
import type { import type {
MetadataOptions, MetadataOptions,
PluginOptions, PluginOptions,
CategoryIndexMatcher,
CategoryIndexMatcherParam,
} from '@docusaurus/plugin-content-docs'; } from '@docusaurus/plugin-content-docs';
type LastUpdateOptions = Pick< type LastUpdateOptions = Pick<
@ -367,31 +369,62 @@ export function getMainDocId({
return getMainDoc().unversionedId; return getMainDoc().unversionedId;
} }
function getLastPathSegment(str: string): string {
return last(str.split('/'))!;
}
// By convention, Docusaurus considers some docs are "indexes": // By convention, Docusaurus considers some docs are "indexes":
// - index.md // - index.md
// - readme.md // - readme.md
// - <folder>/<folder>.md // - <folder>/<folder>.md
// //
// This function is the default implementation of this convention
//
// Those index docs produce a different behavior // Those index docs produce a different behavior
// - Slugs do not end with a weird "/index" suffix // - Slugs do not end with a weird "/index" suffix
// - Auto-generated sidebar categories link to them as intro // - Auto-generated sidebar categories link to them as intro
export function isConventionalDocIndex(doc: { export const isCategoryIndex: CategoryIndexMatcher = ({
source: DocMetadataBase['slug']; fileName,
sourceDirName: DocMetadataBase['sourceDirName']; directories,
}): boolean { }): boolean => {
// "@site/docs/folder/subFolder/subSubFolder/myDoc.md" => "myDoc" const eligibleDocIndexNames = [
const docName = path.parse(doc.source).name; 'index',
'readme',
directories[0]?.toLowerCase(),
];
return eligibleDocIndexNames.includes(fileName.toLowerCase());
};
// "folder/subFolder/subSubFolder" => "subSubFolder" export function toCategoryIndexMatcherParam({
const lastDirName = getLastPathSegment(doc.sourceDirName); source,
sourceDirName,
}: Pick<
DocMetadataBase,
'source' | 'sourceDirName'
>): CategoryIndexMatcherParam {
// source + sourceDirName are always posix-style
return {
fileName: path.posix.parse(source).name,
extension: path.posix.parse(source).ext,
directories: sourceDirName.split(path.posix.sep).reverse(),
};
}
const eligibleDocIndexNames = ['index', 'readme', lastDirName.toLowerCase()]; /**
* guides/sidebar/autogenerated.md -> 'autogenerated', '.md', ['sidebar', 'guides']
return eligibleDocIndexNames.includes(docName.toLowerCase()); */
export function splitPath(str: string): {
/**
* The list of directories, from lowest level to highest.
* If there's no dir name, directories is ['.']
*/
directories: string[];
/** The file name, without extension */
fileName: string;
/** The extension, with a leading dot */
extension: string;
} {
return {
fileName: path.parse(str).name,
extension: path.parse(str).ext,
directories: path.dirname(str).split(path.sep).reverse(),
};
} }
// Return both doc ids // Return both doc ids

View file

@ -13,6 +13,15 @@ declare module '@docusaurus/plugin-content-docs' {
numberPrefix?: number; numberPrefix?: number;
}; };
export type CategoryIndexMatcherParam = {
fileName: string;
directories: string[];
extension: string;
};
export type CategoryIndexMatcher = (
param: CategoryIndexMatcherParam,
) => boolean;
export type EditUrlFunction = (editUrlParams: { export type EditUrlFunction = (editUrlParams: {
version: string; version: string;
versionDocsDirPath: string; versionDocsDirPath: string;

View file

@ -12,6 +12,7 @@ import {
import type {Sidebar, SidebarItemsGenerator} from '../types'; import type {Sidebar, SidebarItemsGenerator} from '../types';
import fs from 'fs-extra'; import fs from 'fs-extra';
import {DefaultNumberPrefixParser} from '../../numberPrefix'; import {DefaultNumberPrefixParser} from '../../numberPrefix';
import {isCategoryIndex} from '../../docs';
describe('DefaultSidebarItemsGenerator', () => { describe('DefaultSidebarItemsGenerator', () => {
function testDefaultSidebarItemsGenerator( function testDefaultSidebarItemsGenerator(
@ -19,6 +20,7 @@ describe('DefaultSidebarItemsGenerator', () => {
) { ) {
return DefaultSidebarItemsGenerator({ return DefaultSidebarItemsGenerator({
numberPrefixParser: DefaultNumberPrefixParser, numberPrefixParser: DefaultNumberPrefixParser,
isCategoryIndex,
item: { item: {
type: 'autogenerated', type: 'autogenerated',
dirName: '.', dirName: '.',
@ -146,6 +148,7 @@ describe('DefaultSidebarItemsGenerator', () => {
const sidebarSlice = await DefaultSidebarItemsGenerator({ const sidebarSlice = await DefaultSidebarItemsGenerator({
numberPrefixParser: DefaultNumberPrefixParser, numberPrefixParser: DefaultNumberPrefixParser,
isCategoryIndex,
item: { item: {
type: 'autogenerated', type: 'autogenerated',
dirName: '.', dirName: '.',
@ -157,48 +160,48 @@ describe('DefaultSidebarItemsGenerator', () => {
docs: [ docs: [
{ {
id: 'intro', id: 'intro',
source: 'intro.md', source: '@site/docs/intro.md',
sourceDirName: '.', sourceDirName: '.',
sidebarPosition: 1, sidebarPosition: 1,
frontMatter: {}, frontMatter: {},
}, },
{ {
id: 'tutorials-index', id: 'tutorials-index',
source: 'index.md', source: '@site/docs/01-Tutorials/index.md',
sourceDirName: '01-Tutorials', sourceDirName: '01-Tutorials',
sidebarPosition: 2, sidebarPosition: 2,
frontMatter: {}, frontMatter: {},
}, },
{ {
id: 'tutorial2', id: 'tutorial2',
source: 'tutorial2.md', source: '@site/docs/01-Tutorials/tutorial2.md',
sourceDirName: '01-Tutorials', sourceDirName: '01-Tutorials',
sidebarPosition: 2, sidebarPosition: 2,
frontMatter: {}, frontMatter: {},
}, },
{ {
id: 'tutorial1', id: 'tutorial1',
source: 'tutorial1.md', source: '@site/docs/01-Tutorials/tutorial1.md',
sourceDirName: '01-Tutorials', sourceDirName: '01-Tutorials',
sidebarPosition: 1, sidebarPosition: 1,
frontMatter: {}, frontMatter: {},
}, },
{ {
id: 'guides-index', id: 'guides-index',
source: '02-Guides.md', // TODO should we allow to just use "Guides.md" to have an index? source: '@site/docs/02-Guides/02-Guides.md', // TODO should we allow to just use "Guides.md" to have an index?
sourceDirName: '02-Guides', sourceDirName: '02-Guides',
frontMatter: {}, frontMatter: {},
}, },
{ {
id: 'guide2', id: 'guide2',
source: 'guide2.md', source: '@site/docs/02-Guides/guide2.md',
sourceDirName: '02-Guides', sourceDirName: '02-Guides',
sidebarPosition: 2, sidebarPosition: 2,
frontMatter: {}, frontMatter: {},
}, },
{ {
id: 'guide1', id: 'guide1',
source: 'guide1.md', source: '@site/docs/02-Guides/guide1.md',
sourceDirName: '02-Guides', sourceDirName: '02-Guides',
sidebarPosition: 1, sidebarPosition: 1,
frontMatter: { frontMatter: {
@ -207,14 +210,14 @@ describe('DefaultSidebarItemsGenerator', () => {
}, },
{ {
id: 'nested-guide', id: 'nested-guide',
source: 'nested-guide.md', source: '@site/docs/02-Guides/01-SubGuides/nested-guide.md',
sourceDirName: '02-Guides/01-SubGuides', sourceDirName: '02-Guides/01-SubGuides',
sidebarPosition: undefined, sidebarPosition: undefined,
frontMatter: {}, frontMatter: {},
}, },
{ {
id: 'end', id: 'end',
source: 'end.md', source: '@site/docs/end.md',
sourceDirName: '.', sourceDirName: '.',
sidebarPosition: 3, sidebarPosition: 3,
frontMatter: {}, frontMatter: {},
@ -296,6 +299,7 @@ describe('DefaultSidebarItemsGenerator', () => {
const sidebarSlice = await DefaultSidebarItemsGenerator({ const sidebarSlice = await DefaultSidebarItemsGenerator({
numberPrefixParser: DefaultNumberPrefixParser, numberPrefixParser: DefaultNumberPrefixParser,
isCategoryIndex,
item: { item: {
type: 'autogenerated', type: 'autogenerated',
dirName: 'subfolder/subsubfolder', dirName: 'subfolder/subsubfolder',
@ -427,19 +431,19 @@ describe('DefaultSidebarItemsGenerator', () => {
docs: [ docs: [
{ {
id: 'parent/doc1', id: 'parent/doc1',
source: 'index.md', source: '@site/docs/Category/index.md',
sourceDirName: 'Category', sourceDirName: 'Category',
frontMatter: {}, frontMatter: {},
}, },
{ {
id: 'parent/doc2', id: 'parent/doc2',
source: 'index.md', source: '@site/docs/Category/index.md',
sourceDirName: 'Category', sourceDirName: 'Category',
frontMatter: {}, frontMatter: {},
}, },
{ {
id: 'parent/doc3', id: 'parent/doc3',
source: 'doc3.md', source: '@site/docs/Category/doc3.md',
sourceDirName: 'Category', sourceDirName: 'Category',
frontMatter: {}, frontMatter: {},
}, },
@ -473,4 +477,116 @@ describe('DefaultSidebarItemsGenerator', () => {
}, },
] as Sidebar); ] as Sidebar);
}); });
test('respects custom isCategoryIndex', async () => {
const sidebarSlice = await DefaultSidebarItemsGenerator({
numberPrefixParser: DefaultNumberPrefixParser,
isCategoryIndex({fileName, directories}) {
return (
fileName.replace(
`${DefaultNumberPrefixParser(
directories[0],
).filename.toLowerCase()}-`,
'',
) === 'index'
);
},
item: {
type: 'autogenerated',
dirName: '.',
},
version: {
versionName: 'current',
contentPath: '',
},
docs: [
{
id: 'intro',
source: '@site/docs/intro.md',
sourceDirName: '.',
sidebarPosition: 1,
frontMatter: {},
},
{
id: 'tutorials-index',
source: '@site/docs/01-Tutorials/tutorials-index.md',
sourceDirName: '01-Tutorials',
sidebarPosition: 2,
frontMatter: {},
},
{
id: 'tutorial2',
source: '@site/docs/01-Tutorials/tutorial2.md',
sourceDirName: '01-Tutorials',
sidebarPosition: 2,
frontMatter: {},
},
{
id: 'tutorial1',
source: '@site/docs/01-Tutorials/tutorial1.md',
sourceDirName: '01-Tutorials',
sidebarPosition: 1,
frontMatter: {},
},
{
id: 'not-guides-index',
source: '@site/docs/02-Guides/README.md',
sourceDirName: '02-Guides',
frontMatter: {},
},
{
id: 'guide2',
source: '@site/docs/02-Guides/guide2.md',
sourceDirName: '02-Guides',
sidebarPosition: 2,
frontMatter: {},
},
{
id: 'guide1',
source: '@site/docs/02-Guides/guide1.md',
sourceDirName: '02-Guides',
sidebarPosition: 1,
frontMatter: {
sidebar_class_name: 'foo',
},
},
],
options: {
sidebarCollapsed: true,
sidebarCollapsible: true,
},
});
expect(sidebarSlice).toEqual([
{type: 'doc', id: 'intro'},
{
type: 'category',
label: 'Tutorials',
collapsed: true,
collapsible: true,
link: {
type: 'doc',
id: 'tutorials-index',
},
items: [
{type: 'doc', id: 'tutorial1'},
{type: 'doc', id: 'tutorial2'},
],
},
{
type: 'category',
label: 'Guides',
collapsed: true,
collapsible: true,
items: [
{type: 'doc', id: 'guide1', className: 'foo'},
{type: 'doc', id: 'guide2'},
{
type: 'doc',
id: 'not-guides-index',
},
],
},
] as Sidebar);
});
}); });

View file

@ -16,6 +16,7 @@ import {DefaultSidebarItemsGenerator} from '../generator';
import {createSlugger} from '@docusaurus/utils'; import {createSlugger} from '@docusaurus/utils';
import type {VersionMetadata} from '../../types'; import type {VersionMetadata} from '../../types';
import {DefaultNumberPrefixParser} from '../../numberPrefix'; import {DefaultNumberPrefixParser} from '../../numberPrefix';
import {isCategoryIndex} from '../../docs';
describe('processSidebars', () => { describe('processSidebars', () => {
function createStaticSidebarItemGenerator( function createStaticSidebarItemGenerator(
@ -137,6 +138,7 @@ describe('processSidebars', () => {
versionName: version.versionName, versionName: version.versionName,
}, },
numberPrefixParser: DefaultNumberPrefixParser, numberPrefixParser: DefaultNumberPrefixParser,
isCategoryIndex,
options: params.sidebarOptions, options: params.sidebarOptions,
}); });
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({ expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
@ -147,6 +149,7 @@ describe('processSidebars', () => {
versionName: version.versionName, versionName: version.versionName,
}, },
numberPrefixParser: DefaultNumberPrefixParser, numberPrefixParser: DefaultNumberPrefixParser,
isCategoryIndex,
options: params.sidebarOptions, options: params.sidebarOptions,
}); });
expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({ expect(StaticSidebarItemsGenerator).toHaveBeenCalledWith({
@ -157,6 +160,7 @@ describe('processSidebars', () => {
versionName: version.versionName, versionName: version.versionName,
}, },
numberPrefixParser: DefaultNumberPrefixParser, numberPrefixParser: DefaultNumberPrefixParser,
isCategoryIndex,
options: params.sidebarOptions, options: params.sidebarOptions,
}); });

View file

@ -25,7 +25,7 @@ import path from 'path';
import fs from 'fs-extra'; import fs from 'fs-extra';
import Yaml from 'js-yaml'; import Yaml from 'js-yaml';
import {validateCategoryMetadataFile} from './validation'; import {validateCategoryMetadataFile} from './validation';
import {createDocsByIdIndex, isConventionalDocIndex} from '../docs'; import {createDocsByIdIndex, toCategoryIndexMatcherParam} from '../docs';
const BreadcrumbSeparator = '/'; const BreadcrumbSeparator = '/';
// To avoid possible name clashes with a folder of the same name as the ID // To avoid possible name clashes with a folder of the same name as the ID
@ -94,6 +94,7 @@ async function readCategoryMetadataFile(
// Comment for this feature: https://github.com/facebook/docusaurus/issues/3464#issuecomment-818670449 // Comment for this feature: https://github.com/facebook/docusaurus/issues/3464#issuecomment-818670449
export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
numberPrefixParser, numberPrefixParser,
isCategoryIndex,
docs: allDocs, docs: allDocs,
options, options,
item: {dirName: autogenDir}, item: {dirName: autogenDir},
@ -210,10 +211,13 @@ export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = async ({
} }
function findConventionalCategoryDocLink(): SidebarItemDoc | undefined { function findConventionalCategoryDocLink(): SidebarItemDoc | undefined {
return allItems.find( return allItems.find((item) => {
(item) => if (item.type !== 'doc') {
item.type === 'doc' && isConventionalDocIndex(getDoc(item.id)), return false;
) as SidebarItemDoc | undefined; }
const doc = getDoc(item.id);
return isCategoryIndex(toCategoryIndexMatcherParam(doc));
}) as SidebarItemDoc | undefined;
} }
function getCategoryLinkedDocId(): string | undefined { function getCategoryLinkedDocId(): string | undefined {

View file

@ -25,6 +25,7 @@ import {DefaultSidebarItemsGenerator} from './generator';
import {mapValues, memoize, pick} from 'lodash'; import {mapValues, memoize, pick} from 'lodash';
import combinePromises from 'combine-promises'; import combinePromises from 'combine-promises';
import {normalizeItem} from './normalization'; import {normalizeItem} from './normalization';
import {isCategoryIndex} from '../docs';
import type {Slugger} from '@docusaurus/utils'; import type {Slugger} from '@docusaurus/utils';
import type { import type {
NumberPrefixParser, NumberPrefixParser,
@ -95,6 +96,7 @@ async function processSidebar(
item, item,
numberPrefixParser, numberPrefixParser,
defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator, defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator,
isCategoryIndex,
...getSidebarItemsGeneratorDocsAndVersion(), ...getSidebarItemsGeneratorDocsAndVersion(),
options: sidebarOptions, options: sidebarOptions,
}); });

View file

@ -10,6 +10,7 @@ import type {DocMetadataBase, VersionMetadata} from '../types';
import type { import type {
NumberPrefixParser, NumberPrefixParser,
SidebarOptions, SidebarOptions,
CategoryIndexMatcher,
} from '@docusaurus/plugin-content-docs'; } from '@docusaurus/plugin-content-docs';
// Makes all properties visible when hovering over the type // Makes all properties visible when hovering over the type
@ -195,6 +196,7 @@ export type SidebarItemsGeneratorArgs = {
version: SidebarItemsGeneratorVersion; version: SidebarItemsGeneratorVersion;
docs: SidebarItemsGeneratorDoc[]; docs: SidebarItemsGeneratorDoc[];
numberPrefixParser: NumberPrefixParser; numberPrefixParser: NumberPrefixParser;
isCategoryIndex: CategoryIndexMatcher;
options: SidebarOptions; options: SidebarOptions;
}; };
export type SidebarItemsGenerator = ( export type SidebarItemsGenerator = (

View file

@ -16,7 +16,7 @@ import {
stripPathNumberPrefixes, stripPathNumberPrefixes,
} from './numberPrefix'; } from './numberPrefix';
import type {DocMetadataBase} from './types'; import type {DocMetadataBase} from './types';
import {isConventionalDocIndex} from './docs'; import {isCategoryIndex, toCategoryIndexMatcherParam} from './docs';
import type {NumberPrefixParser} from '@docusaurus/plugin-content-docs'; import type {NumberPrefixParser} from '@docusaurus/plugin-content-docs';
export default function getSlug({ export default function getSlug({
@ -29,7 +29,7 @@ export default function getSlug({
}: { }: {
baseID: string; baseID: string;
frontMatterSlug?: string; frontMatterSlug?: string;
source: DocMetadataBase['slug']; source: DocMetadataBase['source'];
sourceDirName: DocMetadataBase['sourceDirName']; sourceDirName: DocMetadataBase['sourceDirName'];
stripDirNumberPrefixes?: boolean; stripDirNumberPrefixes?: boolean;
numberPrefixParser?: NumberPrefixParser; numberPrefixParser?: NumberPrefixParser;
@ -50,7 +50,10 @@ export default function getSlug({
return frontMatterSlug; return frontMatterSlug;
} else { } else {
const dirNameSlug = getDirNameSlug(); const dirNameSlug = getDirNameSlug();
if (!frontMatterSlug && isConventionalDocIndex({source, sourceDirName})) { if (
!frontMatterSlug &&
isCategoryIndex(toCategoryIndexMatcherParam({source, sourceDirName}))
) {
return dirNameSlug; return dirNameSlug;
} }
const baseSlug = frontMatterSlug || baseID; const baseSlug = frontMatterSlug || baseID;

View file

@ -82,8 +82,8 @@ export type DocMetadataBase = LastUpdateData & {
version: string; version: string;
title: string; title: string;
description: string; description: string;
source: string; // @site aliased source => "@site/docs/folder/subFolder/subSubFolder/myDoc.md" source: string; // @site aliased posix source => "@site/docs/folder/subFolder/subSubFolder/myDoc.md"
sourceDirName: string; // relative to the versioned docs folder (can be ".") => "folder/subFolder/subSubFolder" sourceDirName: string; // posix path relative to the versioned docs folder (can be ".") => "folder/subFolder/subSubFolder"
slug: string; slug: string;
permalink: string; permalink: string;
sidebarPosition?: number; sidebarPosition?: number;

View file

@ -0,0 +1,3 @@
# Introduction
This file is called `intro.md`. Typically, it won't be selected by the convention; however, it is in this case, because we have used a custom one.

View file

@ -0,0 +1,3 @@
# Sample doc
Lorem Ipsum

View file

@ -21,6 +21,20 @@ const dogfoodingPluginInstances = [
// The target folder uses a _ prefix to test against an edge case regarding MDX partials: https://github.com/facebook/docusaurus/discussions/5181#discussioncomment-1018079 // The target folder uses a _ prefix to test against an edge case regarding MDX partials: https://github.com/facebook/docusaurus/discussions/5181#discussioncomment-1018079
path: fs.realpathSync('_dogfooding/docs-tests-symlink'), path: fs.realpathSync('_dogfooding/docs-tests-symlink'),
showLastUpdateTime: true, showLastUpdateTime: true,
sidebarItemsGenerator(args) {
return args.defaultSidebarItemsGenerator({
...args,
isCategoryIndex({fileName, directories}) {
const eligibleDocIndexNames = [
'index',
'readme',
directories[0].toLowerCase(),
'intro',
];
return eligibleDocIndexNames.includes(fileName.toLowerCase());
},
});
},
}), }),
], ],

View file

@ -77,6 +77,12 @@ type PrefixParser = (filename: string) => {
numberPrefix?: number; numberPrefix?: number;
}; };
type CategoryIndexMatcher = (doc: {
fileName: string;
directories: string[];
extension: string;
}) => boolean;
type SidebarGenerator = (generatorArgs: { type SidebarGenerator = (generatorArgs: {
item: {type: 'autogenerated'; dirName: string}; // the sidebar item with type "autogenerated" item: {type: 'autogenerated'; dirName: string}; // the sidebar item with type "autogenerated"
version: {contentPath: string; versionName: string}; // the current version version: {contentPath: string; versionName: string}; // the current version
@ -88,6 +94,7 @@ type SidebarGenerator = (generatorArgs: {
sidebarPosition?: number | undefined; sidebarPosition?: number | undefined;
}>; // all the docs of that version (unfiltered) }>; // all the docs of that version (unfiltered)
numberPrefixParser: PrefixParser; // numberPrefixParser configured for this plugin numberPrefixParser: PrefixParser; // numberPrefixParser configured for this plugin
isCategoryIndex: CategoryIndexMatcher; // the default category index matcher, that you can override
defaultSidebarItemsGenerator: SidebarGenerator; // useful to re-use/enhance default sidebar generation logic from Docusaurus defaultSidebarItemsGenerator: SidebarGenerator; // useful to re-use/enhance default sidebar generation logic from Docusaurus
}) => Promise<SidebarItem[]>; }) => Promise<SidebarItem[]>;
@ -141,6 +148,7 @@ const config = {
item, item,
version, version,
docs, docs,
isCategoryIndex,
}) { }) {
// Use the provided data to generate a custom sidebar slice // Use the provided data to generate a custom sidebar slice
return [ return [
@ -274,15 +282,15 @@ website/i18n/[locale]/docusaurus-plugin-content-docs
│ # translations for website/docs │ # translations for website/docs
├── current ├── current
   ├── api ├── api
   │   └── config.md └── config.md
   └── getting-started.md └── getting-started.md
├── current.json ├── current.json
│ # translations for website/versioned_docs/version-1.0.0 │ # translations for website/versioned_docs/version-1.0.0
├── version-1.0.0 ├── version-1.0.0
   ├── api ├── api
   │   └── config.md └── config.md
   └── getting-started.md └── getting-started.md
└── version-1.0.0.json └── version-1.0.0.json
``` ```

View file

@ -194,6 +194,102 @@ Naming your introductory document `README.md` makes it show up when browsing the
::: :::
<details>
<summary>Customizing category index matching</summary>
It is possible to opt out any of the category index conventions, or define even more conventions. You can inject your own `isCategoryIndex` matcher through the [`sidebarItemsGenerator`](#customize-the-sidebar-items-generator) callback. For example, you can also pick `intro` as another file name eligible for automatically becoming the category index.
```js title="docusaurus.config.js"
module.exports = {
plugins: [
[
'@docusaurus/plugin-content-docs',
{
async sidebarItemsGenerator({
...args,
isCategoryIndex: defaultCategoryIndexMatcher, // The default matcher implementation, given below
defaultSidebarItemsGenerator,
}) {
return defaultSidebarItemsGenerator({
...args,
// highlight-start
isCategoryIndex(doc) {
return (
// Also pick intro.md in addition to the default ones
doc.fileName.toLowerCase() === 'intro' ||
defaultCategoryIndexMatcher(doc)
);
},
// highlight-end
});
},
},
],
],
};
```
Or choose to not have any category index convention.
```js title="docusaurus.config.js"
module.exports = {
plugins: [
[
'@docusaurus/plugin-content-docs',
{
async sidebarItemsGenerator({
...args,
isCategoryIndex: defaultCategoryIndexMatcher, // The default matcher implementation, given below
defaultSidebarItemsGenerator,
}) {
return defaultSidebarItemsGenerator({
...args,
// highlight-start
isCategoryIndex() {
// No doc will be automatically picked as category index
return false;
},
// highlight-end
});
},
},
],
],
};
```
The `isCategoryIndex` matcher will be provided with three fields:
- `fileName`, the file's name without extension and with casing preserved
- `directories`, the list of directory names _from the lowest level to the highest level_, relative to the docs root directory
- `extension`, the file's extension, with a leading dot.
For example, for a doc file at `guides/sidebar/autogenerated.md`, the props the matcher receives are
```js
const props = {
fileName: 'autogenerated',
directories: ['sidebar', 'guides'],
extension: '.md',
};
```
The default implementation is:
```js
function isCategoryIndex({fileName, directories}) {
const eligibleDocIndexNames = [
'index',
'readme',
directories[0].toLowerCase(),
];
return eligibleDocIndexNames.includes(fileName.toLowerCase());
}
```
</details>
## Autogenerated sidebar metadata {#autogenerated-sidebar-metadata} ## Autogenerated sidebar metadata {#autogenerated-sidebar-metadata}
For hand-written sidebar definitions, you would provide metadata to sidebar items through `sidebars.js`; for autogenerated, Docusaurus would read them from the item's respective file. In addition, you may want to adjust the relative position of each item, because, by default, items within a sidebar slice will be generated in **alphabetical order** (using files and folders names). For hand-written sidebar definitions, you would provide metadata to sidebar items through `sidebars.js`; for autogenerated, Docusaurus would read them from the item's respective file. In addition, you may want to adjust the relative position of each item, because, by default, items within a sidebar slice will be generated in **alphabetical order** (using files and folders names).
@ -317,6 +413,7 @@ module.exports = {
item, item,
version, version,
docs, docs,
isCategoryIndex,
}) { }) {
// Example: return an hardcoded list of static sidebar items // Example: return an hardcoded list of static sidebar items
return [ return [