This commit is contained in:
2024-03-22 03:47:51 +05:30
parent 8bcf3d211e
commit 89819f6fe2
28440 changed files with 3211033 additions and 2 deletions

21
node_modules/@docusaurus/plugin-content-docs/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) Facebook, Inc. and its affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,7 @@
# `@docusaurus/plugin-content-docs`
Docs plugin for Docusaurus.
## Usage
See [plugin-content-docs documentation](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-docs).

View File

@@ -0,0 +1,12 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { type SidebarsUtils } from './sidebars/utils';
import type { CategoryGeneratedIndexMetadata, DocMetadataBase } from '@docusaurus/plugin-content-docs';
export declare function getCategoryGeneratedIndexMetadataList({ docs, sidebarsUtils, }: {
sidebarsUtils: SidebarsUtils;
docs: DocMetadataBase[];
}): CategoryGeneratedIndexMetadata[];

View File

@@ -0,0 +1,37 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.getCategoryGeneratedIndexMetadataList = void 0;
const utils_1 = require("./sidebars/utils");
const docs_1 = require("./docs");
function getCategoryGeneratedIndexMetadata({ category, sidebarsUtils, docsById, }) {
const { sidebarName, previous, next } = sidebarsUtils.getCategoryGeneratedIndexNavigation(category.link.permalink);
return {
title: category.link.title ?? category.label,
description: category.link.description,
image: category.link.image,
keywords: category.link.keywords,
slug: category.link.slug,
permalink: category.link.permalink,
sidebar: sidebarName,
navigation: {
previous: (0, utils_1.toNavigationLink)(previous, docsById),
next: (0, utils_1.toNavigationLink)(next, docsById),
},
};
}
function getCategoryGeneratedIndexMetadataList({ docs, sidebarsUtils, }) {
const docsById = (0, docs_1.createDocsByIdIndex)(docs);
const categoryGeneratedIndexItems = sidebarsUtils.getCategoryGeneratedIndexList();
return categoryGeneratedIndexItems.map((category) => getCategoryGeneratedIndexMetadata({
category,
sidebarsUtils,
docsById,
}));
}
exports.getCategoryGeneratedIndexMetadataList = getCategoryGeneratedIndexMetadataList;

View File

@@ -0,0 +1,9 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { PluginOptions } from '@docusaurus/plugin-content-docs';
import type { LoadContext } from '@docusaurus/types';
export declare function cliDocsVersionCommand(version: unknown, { id: pluginId, path: docsPath, sidebarPath }: PluginOptions, { siteDir, i18n }: LoadContext): Promise<void>;

View File

@@ -0,0 +1,92 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.cliDocsVersionCommand = void 0;
const tslib_1 = require("tslib");
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
const path_1 = tslib_1.__importDefault(require("path"));
const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
const utils_1 = require("@docusaurus/utils");
const files_1 = require("./versions/files");
const validation_1 = require("./versions/validation");
const sidebars_1 = require("./sidebars");
const constants_1 = require("./constants");
async function createVersionedSidebarFile({ siteDir, pluginId, sidebarPath, version, }) {
// 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
// We don't validate here, assuming the user has already built the version
const sidebars = await (0, sidebars_1.loadSidebarsFile)(sidebarPath);
// 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) {
await fs_extra_1.default.outputFile((0, files_1.getVersionSidebarsPath)(siteDir, pluginId, version), `${JSON.stringify(sidebars, null, 2)}\n`, 'utf8');
}
}
// Tests depend on non-default export for mocking.
async function cliDocsVersionCommand(version, { id: pluginId, path: docsPath, sidebarPath }, { siteDir, i18n }) {
// It wouldn't be very user-friendly to show a [default] log prefix,
// so we use [docs] instead of [default]
const pluginIdLogPrefix = pluginId === utils_1.DEFAULT_PLUGIN_ID ? '[docs]' : `[${pluginId}]`;
try {
(0, validation_1.validateVersionName)(version);
}
catch (err) {
logger_1.default.info `${pluginIdLogPrefix}: Invalid version name provided. Try something like: 1.0.0`;
throw err;
}
const versions = (await (0, files_1.readVersionsFile)(siteDir, pluginId)) ?? [];
// Check if version already exists.
if (versions.includes(version)) {
throw new Error(`${pluginIdLogPrefix}: this version already exists! Use a version tag that does not already exist.`);
}
if (i18n.locales.length > 1) {
logger_1.default.info `Versioned docs will be created for the following locales: name=${i18n.locales}`;
}
await Promise.all(i18n.locales.map(async (locale) => {
const localizationDir = path_1.default.resolve(siteDir, i18n.path, i18n.localeConfigs[locale].path);
// Copy docs files.
const docsDir = locale === i18n.defaultLocale
? path_1.default.resolve(siteDir, docsPath)
: (0, files_1.getDocsDirPathLocalized)({
localizationDir,
pluginId,
versionName: constants_1.CURRENT_VERSION_NAME,
});
if (!(await fs_extra_1.default.pathExists(docsDir)) ||
(await fs_extra_1.default.readdir(docsDir)).length === 0) {
if (locale === i18n.defaultLocale) {
throw new Error(logger_1.default.interpolate `${pluginIdLogPrefix}: no docs found in path=${docsDir}.`);
}
else {
logger_1.default.warn `${pluginIdLogPrefix}: no docs found in path=${docsDir}. Skipping.`;
return;
}
}
const newVersionDir = locale === i18n.defaultLocale
? (0, files_1.getVersionDocsDirPath)(siteDir, pluginId, version)
: (0, files_1.getDocsDirPathLocalized)({
localizationDir,
pluginId,
versionName: version,
});
await fs_extra_1.default.copy(docsDir, newVersionDir);
}));
await createVersionedSidebarFile({
siteDir,
pluginId,
version,
sidebarPath,
});
// Update versions.json file.
versions.unshift(version);
await fs_extra_1.default.outputFile((0, files_1.getVersionsFilePath)(siteDir, pluginId), `${JSON.stringify(versions, null, 2)}\n`);
logger_1.default.success `name=${pluginIdLogPrefix}: version name=${version} created!`;
}
exports.cliDocsVersionCommand = cliDocsVersionCommand;

View File

@@ -0,0 +1,15 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { GlobalPluginData, GlobalVersion, ActivePlugin, ActiveDocContext, DocVersionSuggestions } from '@docusaurus/plugin-content-docs/client';
import type { UseDataOptions } from '@docusaurus/types';
export declare function getActivePlugin(allPluginData: {
[pluginId: string]: GlobalPluginData;
}, pathname: string, options?: UseDataOptions): ActivePlugin | undefined;
export declare const getLatestVersion: (data: GlobalPluginData) => GlobalVersion;
export declare function getActiveVersion(data: GlobalPluginData, pathname: string): GlobalVersion | undefined;
export declare function getActiveDocContext(data: GlobalPluginData, pathname: string): ActiveDocContext;
export declare function getDocVersionSuggestions(data: GlobalPluginData, pathname: string): DocVersionSuggestions;

View File

@@ -0,0 +1,78 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { matchPath } from '@docusaurus/router';
// This code is not part of the api surface, not in ./theme on purpose
// get the data of the plugin that is currently "active"
// ie the docs of that plugin are currently browsed
// it is useful to support multiple docs plugin instances
export function getActivePlugin(allPluginData, pathname, options = {}) {
const activeEntry = Object.entries(allPluginData)
// Route sorting: '/android/foo' should match '/android' instead of '/'
.sort((a, b) => b[1].path.localeCompare(a[1].path))
.find(([, pluginData]) => !!matchPath(pathname, {
path: pluginData.path,
exact: false,
strict: false,
}));
const activePlugin = activeEntry
? { pluginId: activeEntry[0], pluginData: activeEntry[1] }
: undefined;
if (!activePlugin && options.failfast) {
throw new Error(`Can't find active docs plugin for "${pathname}" pathname, while it was expected to be found. Maybe you tried to use a docs feature that can only be used on a docs-related page? Existing docs plugin paths are: ${Object.values(allPluginData)
.map((plugin) => plugin.path)
.join(', ')}`);
}
return activePlugin;
}
export const getLatestVersion = (data) => data.versions.find((version) => version.isLast);
export function getActiveVersion(data, pathname) {
const lastVersion = getLatestVersion(data);
// Last version is a route like /docs/*,
// 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,
];
return orderedVersionsMetadata.find((version) => !!matchPath(pathname, {
path: version.path,
exact: false,
strict: false,
}));
}
export function getActiveDocContext(data, pathname) {
const activeVersion = getActiveVersion(data, pathname);
const activeDoc = activeVersion?.docs.find((doc) => !!matchPath(pathname, {
path: doc.path,
exact: true,
strict: false,
}));
function getAlternateVersionDocs(docId) {
const result = {};
data.versions.forEach((version) => {
version.docs.forEach((doc) => {
if (doc.id === docId) {
result[version.name] = doc;
}
});
});
return result;
}
const alternateVersionDocs = activeDoc
? getAlternateVersionDocs(activeDoc.id)
: {};
return {
activeVersion,
activeDoc,
alternateDocVersions: alternateVersionDocs,
};
}
export function getDocVersionSuggestions(data, pathname) {
const latestVersion = getLatestVersion(data);
const activeDocContext = getActiveDocContext(data, pathname);
const latestDocSuggestion = activeDocContext.alternateDocVersions[latestVersion.name];
return { latestDocSuggestion, latestVersionSuggestion: latestVersion };
}

View File

@@ -0,0 +1,82 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { UseDataOptions } from '@docusaurus/types';
export type ActivePlugin = {
pluginId: string;
pluginData: GlobalPluginData;
};
export type ActiveDocContext = {
activeVersion?: GlobalVersion;
activeDoc?: GlobalDoc;
alternateDocVersions: {
[versionName: string]: GlobalDoc;
};
};
export type GlobalDoc = {
/**
* For generated index pages, this is the `slug`, **not** `permalink`
* (without base URL). Because slugs have leading slashes but IDs don't,
* there won't be clashes.
*/
id: string;
path: string;
sidebar?: string;
unlisted?: boolean;
};
export type GlobalVersion = {
name: string;
label: string;
isLast: boolean;
path: string;
/** The doc with `slug: /`, or first doc in first sidebar */
mainDocId: string;
docs: GlobalDoc[];
/** Unversioned IDs. In development, this list is empty. */
draftIds: string[];
sidebars?: {
[sidebarId: string]: GlobalSidebar;
};
};
export type GlobalSidebar = {
link?: {
label: string;
path: string;
};
};
export type GlobalPluginData = {
path: string;
versions: GlobalVersion[];
breadcrumbs: boolean;
};
export type DocVersionSuggestions = {
/** Suggest the latest version */
latestVersionSuggestion: GlobalVersion;
/** Suggest the same doc, in latest version (if one exists) */
latestDocSuggestion?: GlobalDoc;
};
export declare const useAllDocsData: () => {
[pluginId: string]: GlobalPluginData;
};
export declare const useDocsData: (pluginId: string | undefined) => GlobalPluginData;
export declare function useActivePlugin(options?: UseDataOptions): ActivePlugin | undefined;
export declare function useActivePluginAndVersion(options?: UseDataOptions): {
activePlugin: ActivePlugin;
activeVersion: GlobalVersion | undefined;
} | undefined;
/** Versions are returned ordered (most recent first). */
export declare function useVersions(pluginId: string | undefined): GlobalVersion[];
export declare function useLatestVersion(pluginId: string | undefined): GlobalVersion;
/**
* Returns `undefined` on doc-unrelated pages, because there's no version
* currently considered as active.
*/
export declare function useActiveVersion(pluginId: string | undefined): GlobalVersion | undefined;
export declare function useActiveDocContext(pluginId: string | undefined): ActiveDocContext;
/**
* Useful to say "hey, you are not on the latest docs version, please switch"
*/
export declare function useDocVersionSuggestions(pluginId: string | undefined): DocVersionSuggestions;

View File

@@ -0,0 +1,67 @@
/**
* 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 { useLocation } from '@docusaurus/router';
import { useAllPluginInstancesData, usePluginData, } from '@docusaurus/useGlobalData';
import { getActivePlugin, getLatestVersion, getActiveVersion, getActiveDocContext, getDocVersionSuggestions, } from './docsClientUtils';
// Important to use a constant object to avoid React useEffect executions etc.
// see https://github.com/facebook/docusaurus/issues/5089
const StableEmptyObject = {};
// 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 = () => useAllPluginInstancesData('docusaurus-plugin-content-docs') ?? StableEmptyObject;
export const useDocsData = (pluginId) => usePluginData('docusaurus-plugin-content-docs', pluginId, {
failfast: true,
});
// TODO this feature should be provided by docusaurus core
export function useActivePlugin(options = {}) {
const data = useAllDocsData();
const { pathname } = useLocation();
return getActivePlugin(data, pathname, options);
}
export function useActivePluginAndVersion(options = {}) {
const activePlugin = useActivePlugin(options);
const { pathname } = useLocation();
if (!activePlugin) {
return undefined;
}
const activeVersion = getActiveVersion(activePlugin.pluginData, pathname);
return {
activePlugin,
activeVersion,
};
}
/** Versions are returned ordered (most recent first). */
export function useVersions(pluginId) {
const data = useDocsData(pluginId);
return data.versions;
}
export function useLatestVersion(pluginId) {
const data = useDocsData(pluginId);
return getLatestVersion(data);
}
/**
* Returns `undefined` on doc-unrelated pages, because there's no version
* currently considered as active.
*/
export function useActiveVersion(pluginId) {
const data = useDocsData(pluginId);
const { pathname } = useLocation();
return getActiveVersion(data, pathname);
}
export function useActiveDocContext(pluginId) {
const data = useDocsData(pluginId);
const { pathname } = useLocation();
return getActiveDocContext(data, pathname);
}
/**
* Useful to say "hey, you are not on the latest docs version, please switch"
*/
export function useDocVersionSuggestions(pluginId) {
const data = useDocsData(pluginId);
const { pathname } = useLocation();
return getDocVersionSuggestions(data, pathname);
}

View File

@@ -0,0 +1,14 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** The name of the version that's actively worked on (e.g. `website/docs`) */
export declare const CURRENT_VERSION_NAME = "current";
/** All doc versions are stored here by version names */
export declare const VERSIONED_DOCS_DIR = "versioned_docs";
/** All doc versioned sidebars are stored here by version names */
export declare const VERSIONED_SIDEBARS_DIR = "versioned_sidebars";
/** The version names. Should 1-1 map to the content of versioned docs dir. */
export declare const VERSIONS_JSON_FILE = "versions.json";

View File

@@ -0,0 +1,17 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.VERSIONS_JSON_FILE = exports.VERSIONED_SIDEBARS_DIR = exports.VERSIONED_DOCS_DIR = exports.CURRENT_VERSION_NAME = void 0;
/** The name of the version that's actively worked on (e.g. `website/docs`) */
exports.CURRENT_VERSION_NAME = 'current';
/** All doc versions are stored here by version names */
exports.VERSIONED_DOCS_DIR = 'versioned_docs';
/** All doc versioned sidebars are stored here by version names */
exports.VERSIONED_SIDEBARS_DIR = 'versioned_sidebars';
/** The version names. Should 1-1 map to the content of versioned docs dir. */
exports.VERSIONS_JSON_FILE = 'versions.json';

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { MetadataOptions, PluginOptions, CategoryIndexMatcher, DocMetadataBase, VersionMetadata, LoadedVersion } from '@docusaurus/plugin-content-docs';
import type { LoadContext } from '@docusaurus/types';
import type { SidebarsUtils } from './sidebars/utils';
import type { DocFile } from './types';
export declare function readDocFile(versionMetadata: Pick<VersionMetadata, 'contentPath' | 'contentPathLocalized'>, source: string): Promise<DocFile>;
export declare function readVersionDocs(versionMetadata: VersionMetadata, options: Pick<PluginOptions, 'include' | 'exclude' | 'showLastUpdateAuthor' | 'showLastUpdateTime'>): Promise<DocFile[]>;
export type DocEnv = 'production' | 'development';
export declare function processDocMetadata(args: {
docFile: DocFile;
versionMetadata: VersionMetadata;
context: LoadContext;
options: MetadataOptions;
env: DocEnv;
}): Promise<DocMetadataBase>;
export declare function addDocNavigation({ docs, sidebarsUtils, }: {
docs: DocMetadataBase[];
sidebarsUtils: SidebarsUtils;
}): LoadedVersion['docs'];
/**
* The "main doc" is the "version entry point"
* We browse this doc by clicking on a version:
* - the "home" doc (at '/docs/')
* - the first doc of the first sidebar
* - a random doc (if no docs are in any sidebar... edge case)
*/
export declare function getMainDocId({ docs, sidebarsUtils, }: {
docs: DocMetadataBase[];
sidebarsUtils: SidebarsUtils;
}): string;
export declare const isCategoryIndex: CategoryIndexMatcher;
/**
* `guides/sidebar/autogenerated.md` ->
* `'autogenerated', '.md', ['sidebar', 'guides']`
*/
export declare function toCategoryIndexMatcherParam({ source, sourceDirName, }: Pick<DocMetadataBase, 'source' | 'sourceDirName'>): Parameters<CategoryIndexMatcher>[0];
export declare function createDocsByIdIndex<Doc extends {
id: string;
}>(docs: Doc[]): {
[docId: string]: Doc;
};

View File

@@ -0,0 +1,294 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.createDocsByIdIndex = exports.toCategoryIndexMatcherParam = exports.isCategoryIndex = exports.getMainDocId = exports.addDocNavigation = exports.processDocMetadata = exports.readVersionDocs = exports.readDocFile = void 0;
const tslib_1 = require("tslib");
const path_1 = tslib_1.__importDefault(require("path"));
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
const utils_1 = require("@docusaurus/utils");
const lastUpdate_1 = require("./lastUpdate");
const slug_1 = tslib_1.__importDefault(require("./slug"));
const numberPrefix_1 = require("./numberPrefix");
const frontMatter_1 = require("./frontMatter");
const utils_2 = require("./sidebars/utils");
async function readLastUpdateData(filePath, options, lastUpdateFrontMatter) {
const { showLastUpdateAuthor, showLastUpdateTime } = options;
if (showLastUpdateAuthor || showLastUpdateTime) {
const frontMatterTimestamp = lastUpdateFrontMatter?.date
? new Date(lastUpdateFrontMatter.date).getTime() / 1000
: undefined;
if (lastUpdateFrontMatter?.author && lastUpdateFrontMatter.date) {
return {
lastUpdatedAt: frontMatterTimestamp,
lastUpdatedBy: lastUpdateFrontMatter.author,
};
}
// Use fake data in dev for faster development.
const fileLastUpdateData = process.env.NODE_ENV === 'production'
? await (0, lastUpdate_1.getFileLastUpdate)(filePath)
: {
author: 'Author',
timestamp: 1539502055,
};
const { author, timestamp } = fileLastUpdateData ?? {};
return {
lastUpdatedBy: showLastUpdateAuthor
? lastUpdateFrontMatter?.author ?? author
: undefined,
lastUpdatedAt: showLastUpdateTime
? frontMatterTimestamp ?? timestamp
: undefined,
};
}
return {};
}
async function readDocFile(versionMetadata, source) {
const contentPath = await (0, utils_1.getFolderContainingFile)((0, utils_1.getContentPathList)(versionMetadata), source);
const filePath = path_1.default.join(contentPath, source);
const content = await fs_extra_1.default.readFile(filePath, 'utf-8');
return { source, content, contentPath, filePath };
}
exports.readDocFile = readDocFile;
async function readVersionDocs(versionMetadata, options) {
const sources = await (0, utils_1.Globby)(options.include, {
cwd: versionMetadata.contentPath,
ignore: options.exclude,
});
return Promise.all(sources.map((source) => readDocFile(versionMetadata, source)));
}
exports.readVersionDocs = readVersionDocs;
async function doProcessDocMetadata({ docFile, versionMetadata, context, options, env, }) {
const { source, content, contentPath, filePath } = docFile;
const { siteDir, i18n, siteConfig: { markdown: { parseFrontMatter }, }, } = context;
const { frontMatter: unsafeFrontMatter, contentTitle, excerpt, } = await (0, utils_1.parseMarkdownFile)({
filePath,
fileContent: content,
parseFrontMatter,
});
const frontMatter = (0, frontMatter_1.validateDocFrontMatter)(unsafeFrontMatter);
const { custom_edit_url: customEditURL,
// 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, last_update: lastUpdateFrontMatter, } = frontMatter;
const lastUpdate = await readLastUpdateData(filePath, options, lastUpdateFrontMatter);
// E.g. api/plugins/myDoc -> myDoc; myDoc -> myDoc
const sourceFileNameWithoutExtension = path_1.default.basename(source, path_1.default.extname(source));
// E.g. api/plugins/myDoc -> api/plugins; myDoc -> .
const sourceDirName = path_1.default.dirname(source);
const { filename: unprefixedFileName, numberPrefix } = parseNumberPrefixes
? options.numberPrefixParser(sourceFileNameWithoutExtension)
: { filename: sourceFileNameWithoutExtension, numberPrefix: undefined };
const baseID = frontMatter.id ?? unprefixedFileName;
if (baseID.includes('/')) {
throw new Error(`Document id "${baseID}" cannot include slash.`);
}
// For autogenerated sidebars, sidebar position can come from filename number
// prefix or front matter
const sidebarPosition = frontMatter.sidebar_position ?? numberPrefix;
// TODO legacy retrocompatibility
// I think it's bad to affect the front matter id with the dirname?
function computeDirNameIdPrefix() {
if (sourceDirName === '.') {
return undefined;
}
// Eventually remove the number prefixes from intermediate directories
return parseNumberPrefixes
? (0, numberPrefix_1.stripPathNumberPrefixes)(sourceDirName, options.numberPrefixParser)
: sourceDirName;
}
const id = [computeDirNameIdPrefix(), baseID].filter(Boolean).join('/');
const docSlug = (0, slug_1.default)({
baseID,
source,
sourceDirName,
frontMatterSlug: frontMatter.slug,
stripDirNumberPrefixes: parseNumberPrefixes,
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)
const title = frontMatter.title ?? contentTitle ?? baseID;
const description = frontMatter.description ?? excerpt ?? '';
const permalink = (0, utils_1.normalizeUrl)([versionMetadata.path, docSlug]);
function getDocEditUrl() {
const relativeFilePath = path_1.default.relative(contentPath, filePath);
if (typeof options.editUrl === 'function') {
return options.editUrl({
version: versionMetadata.versionName,
versionDocsDirPath: (0, utils_1.posixPath)(path_1.default.relative(siteDir, versionMetadata.contentPath)),
docPath: (0, utils_1.posixPath)(relativeFilePath),
permalink,
locale: context.i18n.currentLocale,
});
}
else if (typeof options.editUrl === 'string') {
const isLocalized = contentPath === versionMetadata.contentPathLocalized;
const baseVersionEditUrl = isLocalized && options.editLocalizedFiles
? versionMetadata.editUrlLocalized
: versionMetadata.editUrl;
return (0, utils_1.getEditUrl)(relativeFilePath, baseVersionEditUrl);
}
return undefined;
}
const draft = (0, utils_1.isDraft)({ env, frontMatter });
const unlisted = (0, utils_1.isUnlisted)({ env, frontMatter });
const formatDate = (locale, date, calendar) => {
try {
return new Intl.DateTimeFormat(locale, {
day: 'numeric',
month: 'short',
year: 'numeric',
timeZone: 'UTC',
calendar,
}).format(date);
}
catch (err) {
logger_1.default.error `Can't format docs lastUpdatedAt date "${String(date)}"`;
throw err;
}
};
// Assign all of object properties during instantiation (if possible) for
// NodeJS optimization.
// Adding properties to object after instantiation will cause hidden
// class transitions.
return {
id,
title,
description,
source: (0, utils_1.aliasedSitePath)(filePath, siteDir),
sourceDirName,
slug: docSlug,
permalink,
draft,
unlisted,
editUrl: customEditURL !== undefined ? customEditURL : getDocEditUrl(),
tags: (0, utils_1.normalizeFrontMatterTags)(versionMetadata.tagsPath, frontMatter.tags),
version: versionMetadata.versionName,
lastUpdatedBy: lastUpdate.lastUpdatedBy,
lastUpdatedAt: lastUpdate.lastUpdatedAt,
formattedLastUpdatedAt: lastUpdate.lastUpdatedAt
? formatDate(i18n.currentLocale, new Date(lastUpdate.lastUpdatedAt * 1000), i18n.localeConfigs[i18n.currentLocale].calendar)
: undefined,
sidebarPosition,
frontMatter,
};
}
async function processDocMetadata(args) {
try {
return await doProcessDocMetadata(args);
}
catch (err) {
throw new Error(`Can't process doc metadata for doc at path path=${args.docFile.filePath} in version name=${args.versionMetadata.versionName}`, { cause: err });
}
}
exports.processDocMetadata = processDocMetadata;
function getUnlistedIds(docs) {
return new Set(docs.filter((doc) => doc.unlisted).map((doc) => doc.id));
}
function addDocNavigation({ docs, sidebarsUtils, }) {
const docsById = createDocsByIdIndex(docs);
const unlistedIds = getUnlistedIds(docs);
// Add sidebar/next/previous to the docs
function addNavData(doc) {
const navigation = sidebarsUtils.getDocNavigation({
docId: doc.id,
displayedSidebar: doc.frontMatter.displayed_sidebar,
unlistedIds,
});
const toNavigationLinkByDocId = (docId, type) => {
if (!docId) {
return undefined;
}
const navDoc = docsById[docId];
if (!navDoc) {
// This could only happen if user provided the ID through front matter
throw new Error(`Error when loading ${doc.id} in ${doc.sourceDirName}: the pagination_${type} front matter points to a non-existent ID ${docId}.`);
}
// Gracefully handle explicitly providing an unlisted doc ID in production
if (navDoc.unlisted) {
return undefined;
}
return (0, utils_2.toDocNavigationLink)(navDoc);
};
const previous = doc.frontMatter.pagination_prev !== undefined
? toNavigationLinkByDocId(doc.frontMatter.pagination_prev, 'prev')
: (0, utils_2.toNavigationLink)(navigation.previous, docsById);
const next = doc.frontMatter.pagination_next !== undefined
? toNavigationLinkByDocId(doc.frontMatter.pagination_next, 'next')
: (0, utils_2.toNavigationLink)(navigation.next, docsById);
return { ...doc, sidebar: navigation.sidebarName, previous, next };
}
const docsWithNavigation = docs.map(addNavData);
// Sort to ensure consistent output for tests
docsWithNavigation.sort((a, b) => a.id.localeCompare(b.id));
return docsWithNavigation;
}
exports.addDocNavigation = addDocNavigation;
/**
* The "main doc" is the "version entry point"
* We browse this doc by clicking on a version:
* - the "home" doc (at '/docs/')
* - the first doc of the first sidebar
* - a random doc (if no docs are in any sidebar... edge case)
*/
function getMainDocId({ docs, sidebarsUtils, }) {
function getMainDoc() {
const versionHomeDoc = docs.find((doc) => doc.slug === '/');
const firstDocIdOfFirstSidebar = sidebarsUtils.getFirstDocIdOfFirstSidebar();
if (versionHomeDoc) {
return versionHomeDoc;
}
else if (firstDocIdOfFirstSidebar) {
return docs.find((doc) => doc.id === firstDocIdOfFirstSidebar);
}
return docs[0];
}
return getMainDoc().id;
}
exports.getMainDocId = getMainDocId;
// By convention, Docusaurus considers some docs are "indexes":
// - index.md
// - readme.md
// - <folder>/<folder>.md
//
// This function is the default implementation of this convention
//
// Those index docs produce a different behavior
// - Slugs do not end with a weird "/index" suffix
// - Auto-generated sidebar categories link to them as intro
const isCategoryIndex = ({ fileName, directories, }) => {
const eligibleDocIndexNames = [
'index',
'readme',
directories[0]?.toLowerCase(),
];
return eligibleDocIndexNames.includes(fileName.toLowerCase());
};
exports.isCategoryIndex = isCategoryIndex;
/**
* `guides/sidebar/autogenerated.md` ->
* `'autogenerated', '.md', ['sidebar', 'guides']`
*/
function toCategoryIndexMatcherParam({ source, sourceDirName, }) {
// source + sourceDirName are always posix-style
return {
fileName: path_1.default.posix.parse(source).name,
extension: path_1.default.posix.parse(source).ext,
directories: sourceDirName.split(path_1.default.posix.sep).reverse(),
};
}
exports.toCategoryIndexMatcherParam = toCategoryIndexMatcherParam;
// Docs are indexed by their id
function createDocsByIdIndex(docs) {
return lodash_1.default.keyBy(docs, (d) => d.id);
}
exports.createDocsByIdIndex = createDocsByIdIndex;

View File

@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { DocFrontMatter } from '@docusaurus/plugin-content-docs';
export declare function validateDocFrontMatter(frontMatter: {
[key: string]: unknown;
}): DocFrontMatter;

View File

@@ -0,0 +1,54 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateDocFrontMatter = void 0;
const utils_validation_1 = require("@docusaurus/utils-validation");
const FrontMatterLastUpdateErrorMessage = '{{#label}} does not look like a valid front matter FileChange object. Please use a FileChange object (with an author and/or date).';
// NOTE: we don't add any default value on purpose here
// We don't want default values to magically appear in doc metadata and props
// While the user did not provide those values explicitly
// We use default values in code instead
const DocFrontMatterSchema = utils_validation_1.JoiFrontMatter.object({
id: utils_validation_1.JoiFrontMatter.string(),
// See https://github.com/facebook/docusaurus/issues/4591#issuecomment-822372398
title: utils_validation_1.JoiFrontMatter.string().allow(''),
hide_title: utils_validation_1.JoiFrontMatter.boolean(),
hide_table_of_contents: utils_validation_1.JoiFrontMatter.boolean(),
keywords: utils_validation_1.JoiFrontMatter.array().items(utils_validation_1.JoiFrontMatter.string().required()),
image: utils_validation_1.URISchema,
// See https://github.com/facebook/docusaurus/issues/4591#issuecomment-822372398
description: utils_validation_1.JoiFrontMatter.string().allow(''),
slug: utils_validation_1.JoiFrontMatter.string(),
sidebar_label: utils_validation_1.JoiFrontMatter.string(),
sidebar_position: utils_validation_1.JoiFrontMatter.number(),
sidebar_class_name: utils_validation_1.JoiFrontMatter.string(),
sidebar_custom_props: utils_validation_1.JoiFrontMatter.object().unknown(),
displayed_sidebar: utils_validation_1.JoiFrontMatter.string().allow(null),
tags: utils_validation_1.FrontMatterTagsSchema,
pagination_label: utils_validation_1.JoiFrontMatter.string(),
custom_edit_url: utils_validation_1.URISchema.allow('', null),
parse_number_prefixes: utils_validation_1.JoiFrontMatter.boolean(),
pagination_next: utils_validation_1.JoiFrontMatter.string().allow(null),
pagination_prev: utils_validation_1.JoiFrontMatter.string().allow(null),
...utils_validation_1.FrontMatterTOCHeadingLevels,
last_update: utils_validation_1.JoiFrontMatter.object({
author: utils_validation_1.JoiFrontMatter.string(),
date: utils_validation_1.JoiFrontMatter.date().raw(),
})
.or('author', 'date')
.messages({
'object.missing': FrontMatterLastUpdateErrorMessage,
'object.base': FrontMatterLastUpdateErrorMessage,
}),
})
.unknown()
.concat(utils_validation_1.ContentVisibilitySchema);
function validateDocFrontMatter(frontMatter) {
return (0, utils_validation_1.validateFrontMatter)(frontMatter, DocFrontMatterSchema);
}
exports.validateDocFrontMatter = validateDocFrontMatter;

View File

@@ -0,0 +1,9 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { FullVersion } from './types';
import type { GlobalVersion } from '@docusaurus/plugin-content-docs/client';
export declare function toGlobalDataVersion(version: FullVersion): GlobalVersion;

View File

@@ -0,0 +1,60 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.toGlobalDataVersion = void 0;
const tslib_1 = require("tslib");
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const docs_1 = require("./docs");
function toGlobalDataDoc(doc) {
return {
id: doc.id,
path: doc.permalink,
// optimize global data size: do not add unlisted: false/undefined
...(doc.unlisted && { unlisted: doc.unlisted }),
// TODO optimize size? remove attribute when no sidebar (breaking change?)
sidebar: doc.sidebar,
};
}
function toGlobalDataGeneratedIndex(doc) {
return {
id: doc.slug,
path: doc.permalink,
sidebar: doc.sidebar,
};
}
function toGlobalSidebars(sidebars, version) {
return lodash_1.default.mapValues(sidebars, (sidebar, sidebarId) => {
const firstLink = version.sidebarsUtils.getFirstLink(sidebarId);
if (!firstLink) {
return {};
}
return {
link: {
path: firstLink.type === 'generated-index'
? firstLink.permalink
: version.docs.find((doc) => doc.id === firstLink.id).permalink,
label: firstLink.label,
},
};
});
}
function toGlobalDataVersion(version) {
return {
name: version.versionName,
label: version.label,
isLast: version.isLast,
path: version.path,
mainDocId: (0, docs_1.getMainDocId)(version),
docs: version.docs
.map(toGlobalDataDoc)
.concat(version.categoryGeneratedIndices.map(toGlobalDataGeneratedIndex)),
draftIds: version.drafts.map((doc) => doc.id),
sidebars: toGlobalSidebars(version.sidebars, version),
};
}
exports.toGlobalDataVersion = toGlobalDataVersion;

View File

@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { PluginOptions, LoadedContent } from '@docusaurus/plugin-content-docs';
import type { LoadContext, Plugin } from '@docusaurus/types';
export default function pluginContentDocs(context: LoadContext, options: PluginOptions): Promise<Plugin<LoadedContent>>;
export { validateOptions } from './options';

View File

@@ -0,0 +1,233 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateOptions = void 0;
const tslib_1 = require("tslib");
const path_1 = tslib_1.__importDefault(require("path"));
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
const utils_1 = require("@docusaurus/utils");
const sidebars_1 = require("./sidebars");
const generator_1 = require("./sidebars/generator");
const docs_1 = require("./docs");
const versions_1 = require("./versions");
const cli_1 = require("./cli");
const constants_1 = require("./constants");
const globalData_1 = require("./globalData");
const translations_1 = require("./translations");
const routes_1 = require("./routes");
const utils_2 = require("./sidebars/utils");
async function pluginContentDocs(context, options) {
const { siteDir, generatedFilesDir, baseUrl, siteConfig } = context;
// Mutate options to resolve sidebar path according to siteDir
options.sidebarPath = (0, sidebars_1.resolveSidebarPathOption)(siteDir, options.sidebarPath);
const versionsMetadata = await (0, versions_1.readVersionsMetadata)({ context, options });
const pluginId = options.id;
const pluginDataDirRoot = path_1.default.join(generatedFilesDir, 'docusaurus-plugin-content-docs');
const dataDir = path_1.default.join(pluginDataDirRoot, pluginId);
const aliasedSource = (source) => `~docs/${(0, utils_1.posixPath)(path_1.default.relative(pluginDataDirRoot, source))}`;
// TODO env should be injected into all plugins
const env = process.env.NODE_ENV;
return {
name: 'docusaurus-plugin-content-docs',
extendCli(cli) {
const isDefaultPluginId = pluginId === utils_1.DEFAULT_PLUGIN_ID;
// Need to create one distinct command per plugin instance
// otherwise 2 instances would try to execute the command!
const command = isDefaultPluginId
? 'docs:version'
: `docs:version:${pluginId}`;
const commandDescription = isDefaultPluginId
? 'Tag a new docs version'
: `Tag a new docs version (${pluginId})`;
cli
.command(command)
.arguments('<version>')
.description(commandDescription)
.action((version) => (0, cli_1.cliDocsVersionCommand)(version, options, context));
},
getTranslationFiles({ content }) {
return (0, translations_1.getLoadedContentTranslationFiles)(content);
},
getPathsToWatch() {
function getVersionPathsToWatch(version) {
const result = [
...options.include.flatMap((pattern) => (0, utils_1.getContentPathList)(version).map((docsDirPath) => `${docsDirPath}/${pattern}`)),
`${version.contentPath}/**/${generator_1.CategoryMetadataFilenamePattern}`,
];
if (typeof version.sidebarFilePath === 'string') {
result.unshift(version.sidebarFilePath);
}
return result;
}
return versionsMetadata.flatMap(getVersionPathsToWatch);
},
async loadContent() {
async function loadVersionDocsBase(versionMetadata) {
const docFiles = await (0, docs_1.readVersionDocs)(versionMetadata, options);
if (docFiles.length === 0) {
throw new Error(`Docs version "${versionMetadata.versionName}" has no docs! At least one doc should exist at "${path_1.default.relative(siteDir, versionMetadata.contentPath)}".`);
}
function processVersionDoc(docFile) {
return (0, docs_1.processDocMetadata)({
docFile,
versionMetadata,
context,
options,
env,
});
}
return Promise.all(docFiles.map(processVersionDoc));
}
async function doLoadVersion(versionMetadata) {
const docsBase = await loadVersionDocsBase(versionMetadata);
// TODO we only ever need draftIds in further code, not full draft items
// To simplify and prevent mistakes, avoid exposing draft
// replace draft=>draftIds in content loaded
const [drafts, docs] = lodash_1.default.partition(docsBase, (doc) => doc.draft);
const sidebars = await (0, sidebars_1.loadSidebars)(versionMetadata.sidebarFilePath, {
sidebarItemsGenerator: options.sidebarItemsGenerator,
numberPrefixParser: options.numberPrefixParser,
docs,
drafts,
version: versionMetadata,
sidebarOptions: {
sidebarCollapsed: options.sidebarCollapsed,
sidebarCollapsible: options.sidebarCollapsible,
},
categoryLabelSlugger: (0, utils_1.createSlugger)(),
});
const sidebarsUtils = (0, utils_2.createSidebarsUtils)(sidebars);
const docsById = (0, docs_1.createDocsByIdIndex)(docs);
const allDocIds = Object.keys(docsById);
sidebarsUtils.checkLegacyVersionedSidebarNames({
sidebarFilePath: versionMetadata.sidebarFilePath,
versionMetadata,
});
sidebarsUtils.checkSidebarsDocIds({
allDocIds,
sidebarFilePath: versionMetadata.sidebarFilePath,
versionMetadata,
});
return {
...versionMetadata,
docs: (0, docs_1.addDocNavigation)({
docs,
sidebarsUtils,
}),
drafts,
sidebars,
};
}
async function loadVersion(versionMetadata) {
try {
return await doLoadVersion(versionMetadata);
}
catch (err) {
logger_1.default.error `Loading of version failed for version name=${versionMetadata.versionName}`;
throw err;
}
}
return {
loadedVersions: await Promise.all(versionsMetadata.map(loadVersion)),
};
},
translateContent({ content, translationFiles }) {
return (0, translations_1.translateLoadedContent)(content, translationFiles);
},
async contentLoaded({ content, actions }) {
const versions = content.loadedVersions.map(versions_1.toFullVersion);
await (0, routes_1.createAllRoutes)({
baseUrl,
versions,
options,
actions,
aliasedSource,
});
actions.setGlobalData({
path: (0, utils_1.normalizeUrl)([baseUrl, options.routeBasePath]),
versions: versions.map(globalData_1.toGlobalDataVersion),
breadcrumbs: options.breadcrumbs,
});
},
configureWebpack(_config, isServer, utils, content) {
const { rehypePlugins, remarkPlugins, beforeDefaultRehypePlugins, beforeDefaultRemarkPlugins, } = options;
function getSourceToPermalink() {
const allDocs = content.loadedVersions.flatMap((v) => v.docs);
return Object.fromEntries(allDocs.map(({ source, permalink }) => [source, permalink]));
}
const docsMarkdownOptions = {
siteDir,
sourceToPermalink: getSourceToPermalink(),
versionsMetadata,
onBrokenMarkdownLink: (brokenMarkdownLink) => {
logger_1.default.report(siteConfig.onBrokenMarkdownLinks) `Docs markdown link couldn't be resolved: (url=${brokenMarkdownLink.link}) in path=${brokenMarkdownLink.filePath} for version number=${brokenMarkdownLink.contentPaths.versionName}`;
},
};
function createMDXLoaderRule() {
const contentDirs = versionsMetadata
.flatMap(utils_1.getContentPathList)
// Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970
.map(utils_1.addTrailingPathSeparator);
return {
test: /\.mdx?$/i,
include: contentDirs,
use: [
{
loader: require.resolve('@docusaurus/mdx-loader'),
options: {
admonitions: options.admonitions,
remarkPlugins,
rehypePlugins,
beforeDefaultRehypePlugins,
beforeDefaultRemarkPlugins,
staticDirs: siteConfig.staticDirectories.map((dir) => path_1.default.resolve(siteDir, dir)),
siteDir,
isMDXPartial: (0, utils_1.createAbsoluteFilePathMatcher)(options.exclude, contentDirs),
metadataPath: (mdxPath) => {
// Note that metadataPath must be the same/in-sync as
// the path from createData for each MDX.
const aliasedPath = (0, utils_1.aliasedSitePath)(mdxPath, siteDir);
return path_1.default.join(dataDir, `${(0, utils_1.docuHash)(aliasedPath)}.json`);
},
// Assets allow to convert some relative images paths to
// require(...) calls
createAssets: ({ frontMatter, }) => ({
image: frontMatter.image,
}),
markdownConfig: siteConfig.markdown,
},
},
{
loader: path_1.default.resolve(__dirname, './markdown/index.js'),
options: docsMarkdownOptions,
},
].filter(Boolean),
};
}
return {
ignoreWarnings: [
// Suppress warnings about non-existing of versions file.
(e) => e.message.includes("Can't resolve") &&
e.message.includes(constants_1.VERSIONS_JSON_FILE),
],
resolve: {
alias: {
'~docs': pluginDataDirRoot,
},
},
module: {
rules: [createMDXLoaderRule()],
},
};
},
};
}
exports.default = pluginContentDocs;
var options_1 = require("./options");
Object.defineProperty(exports, "validateOptions", { enumerable: true, get: function () { return options_1.validateOptions; } });

View File

@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
export declare function getFileLastUpdate(filePath: string): Promise<{
timestamp: number;
author: string;
} | null>;

View File

@@ -0,0 +1,47 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.getFileLastUpdate = void 0;
const tslib_1 = require("tslib");
const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
const utils_1 = require("@docusaurus/utils");
let showedGitRequirementError = false;
let showedFileNotTrackedError = false;
async function getFileLastUpdate(filePath) {
if (!filePath) {
return null;
}
// Wrap in try/catch in case the shell commands fail
// (e.g. project doesn't use Git, etc).
try {
const result = (0, utils_1.getFileCommitDate)(filePath, {
age: 'newest',
includeAuthor: true,
});
return { timestamp: result.timestamp, author: result.author };
}
catch (err) {
if (err instanceof utils_1.GitNotFoundError) {
if (!showedGitRequirementError) {
logger_1.default.warn('Sorry, the docs plugin last update options require Git.');
showedGitRequirementError = true;
}
}
else if (err instanceof utils_1.FileNotTrackedError) {
if (!showedFileNotTrackedError) {
logger_1.default.warn('Cannot infer the update date for some files, as they are not tracked by git.');
showedFileNotTrackedError = true;
}
}
else {
logger_1.default.warn(err);
}
return null;
}
}
exports.getFileLastUpdate = getFileLastUpdate;

View File

@@ -0,0 +1,9 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { DocsMarkdownOption } from '../types';
import type { LoaderContext } from 'webpack';
export default function markdownLoader(this: LoaderContext<DocsMarkdownOption>, source: string): void;

View File

@@ -0,0 +1,16 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
const linkify_1 = require("./linkify");
function markdownLoader(source) {
const fileString = source;
const callback = this.async();
const options = this.getOptions();
return callback(null, (0, linkify_1.linkify)(fileString, this.resourcePath, options));
}
exports.default = markdownLoader;

View File

@@ -0,0 +1,8 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { DocsMarkdownOption } from '../types';
export declare function linkify(fileString: string, filePath: string, options: DocsMarkdownOption): string;

View File

@@ -0,0 +1,34 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.linkify = void 0;
const utils_1 = require("@docusaurus/utils");
function getVersion(filePath, options) {
const versionFound = options.versionsMetadata.find((version) => (0, utils_1.getContentPathList)(version).some((docsDirPath) => filePath.startsWith(docsDirPath)));
// At this point, this should never happen, because the MDX loaders' paths are
// literally using the version content paths; but if we allow sourcing content
// from outside the docs directory (through the `include` option, for example;
// is there a compelling use-case?), this would actually be testable
if (!versionFound) {
throw new Error(`Unexpected error: Markdown file at "${filePath}" does not belong to any docs version!`);
}
return versionFound;
}
function linkify(fileString, filePath, options) {
const { siteDir, sourceToPermalink, onBrokenMarkdownLink } = options;
const { newContent, brokenMarkdownLinks } = (0, utils_1.replaceMarkdownLinks)({
siteDir,
fileString,
filePath,
contentPaths: getVersion(filePath, options),
sourceToPermalink,
});
brokenMarkdownLinks.forEach((l) => onBrokenMarkdownLink(l));
return newContent;
}
exports.linkify = linkify;

View File

@@ -0,0 +1,11 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { NumberPrefixParser } from '@docusaurus/plugin-content-docs';
export declare const DefaultNumberPrefixParser: NumberPrefixParser;
export declare const DisabledNumberPrefixParser: NumberPrefixParser;
export declare function stripNumberPrefix(str: string, parser: NumberPrefixParser): string;
export declare function stripPathNumberPrefixes(path: string, parser: NumberPrefixParser): string;

View File

@@ -0,0 +1,50 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.stripPathNumberPrefixes = exports.stripNumberPrefix = exports.DisabledNumberPrefixParser = exports.DefaultNumberPrefixParser = void 0;
// Best-effort to avoid parsing some patterns as number prefix
// ignore common date-like patterns: https://github.com/facebook/docusaurus/issues/4640
// ignore common versioning patterns: https://github.com/facebook/docusaurus/issues/4653
// Both of them would look like 7.0-foo or 2021-11-foo
// 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 either a
// version or a float. User can configure her own NumberPrefixParser if she
// wants 8.0 to be interpreted as a float
const ignoredPrefixPattern = /^\d+[-_.]\d+/;
const numberPrefixPattern = /^(?<numberPrefix>\d+)\s*[-_.]+\s*(?<suffix>[^-_.\s].*)$/;
// 0-myDoc => {filename: myDoc, numberPrefix: 0}
// 003 - myDoc => {filename: myDoc, numberPrefix: 3}
const DefaultNumberPrefixParser = (filename) => {
if (ignoredPrefixPattern.test(filename)) {
return { filename, numberPrefix: undefined };
}
const match = numberPrefixPattern.exec(filename);
if (!match) {
return { filename, numberPrefix: undefined };
}
return {
filename: match.groups.suffix,
numberPrefix: parseInt(match.groups.numberPrefix, 10),
};
};
exports.DefaultNumberPrefixParser = DefaultNumberPrefixParser;
const DisabledNumberPrefixParser = (filename) => ({ filename, numberPrefix: undefined });
exports.DisabledNumberPrefixParser = DisabledNumberPrefixParser;
// 0-myDoc => myDoc
function stripNumberPrefix(str, parser) {
return parser(str).filename;
}
exports.stripNumberPrefix = stripNumberPrefix;
// 0-myFolder/0-mySubfolder/0-myDoc => myFolder/mySubfolder/myDoc
function stripPathNumberPrefixes(path, parser) {
return path
.split('/')
.map((segment) => stripNumberPrefix(segment, parser))
.join('/');
}
exports.stripPathNumberPrefixes = stripPathNumberPrefixes;

View File

@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { OptionValidationContext } from '@docusaurus/types';
import type { PluginOptions, Options } from '@docusaurus/plugin-content-docs';
export declare const DEFAULT_OPTIONS: Omit<PluginOptions, 'id' | 'sidebarPath'>;
export declare function validateOptions({ validate, options: userOptions, }: OptionValidationContext<Options, PluginOptions>): PluginOptions;

View File

@@ -0,0 +1,127 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateOptions = exports.DEFAULT_OPTIONS = void 0;
const tslib_1 = require("tslib");
const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
const utils_validation_1 = require("@docusaurus/utils-validation");
const utils_1 = require("@docusaurus/utils");
const generator_1 = require("./sidebars/generator");
const numberPrefix_1 = require("./numberPrefix");
exports.DEFAULT_OPTIONS = {
path: 'docs',
routeBasePath: 'docs',
tagsBasePath: 'tags',
include: ['**/*.{md,mdx}'],
exclude: utils_1.GlobExcludeDefault,
sidebarItemsGenerator: generator_1.DefaultSidebarItemsGenerator,
numberPrefixParser: numberPrefix_1.DefaultNumberPrefixParser,
docsRootComponent: '@theme/DocsRoot',
docVersionRootComponent: '@theme/DocVersionRoot',
docRootComponent: '@theme/DocRoot',
docItemComponent: '@theme/DocItem',
docTagDocListComponent: '@theme/DocTagDocListPage',
docTagsListComponent: '@theme/DocTagsListPage',
docCategoryGeneratedIndexComponent: '@theme/DocCategoryGeneratedIndexPage',
remarkPlugins: [],
rehypePlugins: [],
beforeDefaultRemarkPlugins: [],
beforeDefaultRehypePlugins: [],
showLastUpdateTime: false,
showLastUpdateAuthor: false,
admonitions: true,
includeCurrentVersion: true,
disableVersioning: false,
lastVersion: undefined,
versions: {},
editCurrentVersion: false,
editLocalizedFiles: false,
sidebarCollapsible: true,
sidebarCollapsed: true,
breadcrumbs: true,
};
const VersionOptionsSchema = utils_validation_1.Joi.object({
path: utils_validation_1.Joi.string().allow('').optional(),
label: utils_validation_1.Joi.string().optional(),
banner: utils_validation_1.Joi.string().equal('none', 'unreleased', 'unmaintained').optional(),
badge: utils_validation_1.Joi.boolean().optional(),
className: utils_validation_1.Joi.string().optional(),
noIndex: utils_validation_1.Joi.boolean().optional(),
});
const VersionsOptionsSchema = utils_validation_1.Joi.object()
.pattern(utils_validation_1.Joi.string().required(), VersionOptionsSchema)
.default(exports.DEFAULT_OPTIONS.versions);
const OptionsSchema = utils_validation_1.Joi.object({
path: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.path),
editUrl: utils_validation_1.Joi.alternatives().try(utils_validation_1.URISchema, utils_validation_1.Joi.function()),
editCurrentVersion: utils_validation_1.Joi.boolean().default(exports.DEFAULT_OPTIONS.editCurrentVersion),
editLocalizedFiles: utils_validation_1.Joi.boolean().default(exports.DEFAULT_OPTIONS.editLocalizedFiles),
routeBasePath: utils_validation_1.RouteBasePathSchema.default(exports.DEFAULT_OPTIONS.routeBasePath),
tagsBasePath: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.tagsBasePath),
// @ts-expect-error: deprecated
homePageId: utils_validation_1.Joi.any().forbidden().messages({
'any.unknown': 'The docs plugin option homePageId is not supported anymore. To make a doc the "home", please add "slug: /" in its front matter. See: https://docusaurus.io/docs/next/docs-introduction#home-page-docs',
}),
include: utils_validation_1.Joi.array().items(utils_validation_1.Joi.string()).default(exports.DEFAULT_OPTIONS.include),
exclude: utils_validation_1.Joi.array().items(utils_validation_1.Joi.string()).default(exports.DEFAULT_OPTIONS.exclude),
sidebarPath: utils_validation_1.Joi.alternatives().try(utils_validation_1.Joi.boolean().invalid(true), utils_validation_1.Joi.string()),
sidebarItemsGenerator: utils_validation_1.Joi.function().default(() => exports.DEFAULT_OPTIONS.sidebarItemsGenerator),
sidebarCollapsible: utils_validation_1.Joi.boolean().default(exports.DEFAULT_OPTIONS.sidebarCollapsible),
sidebarCollapsed: utils_validation_1.Joi.boolean().default(exports.DEFAULT_OPTIONS.sidebarCollapsed),
numberPrefixParser: utils_validation_1.Joi.alternatives()
.try(utils_validation_1.Joi.function(),
// Convert boolean values to functions
utils_validation_1.Joi.alternatives().conditional(utils_validation_1.Joi.boolean(), {
then: utils_validation_1.Joi.custom((val) => val ? numberPrefix_1.DefaultNumberPrefixParser : numberPrefix_1.DisabledNumberPrefixParser),
}))
.default(() => exports.DEFAULT_OPTIONS.numberPrefixParser),
docsRootComponent: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.docsRootComponent),
docVersionRootComponent: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.docVersionRootComponent),
docRootComponent: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.docRootComponent),
docItemComponent: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.docItemComponent),
docTagsListComponent: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.docTagsListComponent),
docTagDocListComponent: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.docTagDocListComponent),
docCategoryGeneratedIndexComponent: utils_validation_1.Joi.string().default(exports.DEFAULT_OPTIONS.docCategoryGeneratedIndexComponent),
remarkPlugins: utils_validation_1.RemarkPluginsSchema.default(exports.DEFAULT_OPTIONS.remarkPlugins),
rehypePlugins: utils_validation_1.RehypePluginsSchema.default(exports.DEFAULT_OPTIONS.rehypePlugins),
beforeDefaultRemarkPlugins: utils_validation_1.RemarkPluginsSchema.default(exports.DEFAULT_OPTIONS.beforeDefaultRemarkPlugins),
beforeDefaultRehypePlugins: utils_validation_1.RehypePluginsSchema.default(exports.DEFAULT_OPTIONS.beforeDefaultRehypePlugins),
admonitions: utils_validation_1.AdmonitionsSchema.default(exports.DEFAULT_OPTIONS.admonitions),
showLastUpdateTime: utils_validation_1.Joi.bool().default(exports.DEFAULT_OPTIONS.showLastUpdateTime),
showLastUpdateAuthor: utils_validation_1.Joi.bool().default(exports.DEFAULT_OPTIONS.showLastUpdateAuthor),
includeCurrentVersion: utils_validation_1.Joi.bool().default(exports.DEFAULT_OPTIONS.includeCurrentVersion),
onlyIncludeVersions: utils_validation_1.Joi.array().items(utils_validation_1.Joi.string().required()).optional(),
disableVersioning: utils_validation_1.Joi.bool().default(exports.DEFAULT_OPTIONS.disableVersioning),
lastVersion: utils_validation_1.Joi.string().optional(),
versions: VersionsOptionsSchema,
breadcrumbs: utils_validation_1.Joi.bool().default(exports.DEFAULT_OPTIONS.breadcrumbs),
});
function validateOptions({ validate, options: userOptions, }) {
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
if (typeof options.sidebarCollapsed === 'undefined') {
options = {
...options,
sidebarCollapsed: false,
};
}
if (options.sidebarCollapsed) {
logger_1.default.warn `The docs plugin config is inconsistent. It does not make sense to use code=${'sidebarCollapsible: false'} and code=${'sidebarCollapsed: true'} at the same time. code=${'sidebarCollapsed: true'} will be ignored.`;
options = {
...options,
sidebarCollapsed: false,
};
}
}
const normalizedOptions = validate(OptionsSchema, options);
return normalizedOptions;
}
exports.validateOptions = validateOptions;

View File

@@ -0,0 +1,21 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { VersionTag, VersionTags } from './types';
import type { SidebarItemDoc } from './sidebars/types';
import type { PropSidebars, PropVersionMetadata, PropTagDocList, PropTagsListPage, PropSidebarItemLink, DocMetadata, LoadedVersion } from '@docusaurus/plugin-content-docs';
export declare function toSidebarDocItemLinkProp({ item, doc, }: {
item: SidebarItemDoc;
doc: Pick<DocMetadata, 'id' | 'title' | 'permalink' | 'unlisted' | 'frontMatter'>;
}): PropSidebarItemLink;
export declare function toSidebarsProp(loadedVersion: LoadedVersion): PropSidebars;
export declare function toVersionMetadataProp(pluginId: string, loadedVersion: LoadedVersion): PropVersionMetadata;
export declare function toTagDocListProp({ allTagsPath, tag, docs, }: {
allTagsPath: string;
tag: VersionTag;
docs: DocMetadata[];
}): PropTagDocList;
export declare function toTagsListTagsProp(versionTags: VersionTags): PropTagsListPage['tags'];

View File

@@ -0,0 +1,153 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.toTagsListTagsProp = exports.toTagDocListProp = exports.toVersionMetadataProp = exports.toSidebarsProp = exports.toSidebarDocItemLinkProp = void 0;
const tslib_1 = require("tslib");
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const docs_1 = require("./docs");
function toSidebarDocItemLinkProp({ item, doc, }) {
const { id, title, permalink, frontMatter: { sidebar_label: sidebarLabel, sidebar_custom_props: customProps, }, unlisted, } = doc;
return {
type: 'link',
label: sidebarLabel ?? item.label ?? title,
href: permalink,
className: item.className,
customProps: item.customProps ?? customProps,
docId: id,
unlisted,
};
}
exports.toSidebarDocItemLinkProp = toSidebarDocItemLinkProp;
function toSidebarsProp(loadedVersion) {
const docsById = (0, docs_1.createDocsByIdIndex)(loadedVersion.docs);
function getDocById(docId) {
const docMetadata = docsById[docId];
if (!docMetadata) {
throw new Error(`Invalid sidebars file. The document with id "${docId}" was used in the sidebar, but no document with this id could be found.
Available document ids are:
- ${Object.keys(docsById).sort().join('\n- ')}`);
}
return docMetadata;
}
const convertDocLink = (item) => {
const doc = getDocById(item.id);
return toSidebarDocItemLinkProp({ item, doc });
};
function getCategoryLinkHref(link) {
switch (link?.type) {
case 'doc':
return getDocById(link.id).permalink;
case 'generated-index':
return link.permalink;
default:
return undefined;
}
}
function getCategoryLinkUnlisted(link) {
if (link?.type === 'doc') {
return getDocById(link.id).unlisted;
}
return false;
}
function getCategoryLinkCustomProps(link) {
switch (link?.type) {
case 'doc':
return getDocById(link.id).frontMatter.sidebar_custom_props;
default:
return undefined;
}
}
function convertCategory(item) {
const { link, ...rest } = item;
const href = getCategoryLinkHref(link);
const linkUnlisted = getCategoryLinkUnlisted(link);
const customProps = item.customProps ?? getCategoryLinkCustomProps(link);
return {
...rest,
items: item.items.map(normalizeItem),
...(href && { href }),
...(linkUnlisted && { linkUnlisted }),
...(customProps && { customProps }),
};
}
function normalizeItem(item) {
switch (item.type) {
case 'category':
return convertCategory(item);
case 'ref':
case 'doc':
return convertDocLink(item);
case 'link':
default:
return item;
}
}
// Transform the sidebar so that all sidebar item will be in the
// form of 'link' or 'category' only.
// This is what will be passed as props to the UI component.
return lodash_1.default.mapValues(loadedVersion.sidebars, (items) => items.map(normalizeItem));
}
exports.toSidebarsProp = toSidebarsProp;
function toVersionDocsProp(loadedVersion) {
return Object.fromEntries(loadedVersion.docs.map((doc) => [
doc.id,
{
id: doc.id,
title: doc.title,
description: doc.description,
sidebar: doc.sidebar,
},
]));
}
function toVersionMetadataProp(pluginId, loadedVersion) {
return {
pluginId,
version: loadedVersion.versionName,
label: loadedVersion.label,
banner: loadedVersion.banner,
badge: loadedVersion.badge,
noIndex: loadedVersion.noIndex,
className: loadedVersion.className,
isLast: loadedVersion.isLast,
docsSidebars: toSidebarsProp(loadedVersion),
docs: toVersionDocsProp(loadedVersion),
};
}
exports.toVersionMetadataProp = toVersionMetadataProp;
function toTagDocListProp({ allTagsPath, tag, docs, }) {
function toDocListProp() {
const list = lodash_1.default.compact(tag.docIds.map((id) => docs.find((doc) => doc.id === id)));
// Sort docs by title
list.sort((doc1, doc2) => doc1.title.localeCompare(doc2.title));
return list.map((doc) => ({
id: doc.id,
title: doc.title,
description: doc.description,
permalink: doc.permalink,
}));
}
return {
label: tag.label,
permalink: tag.permalink,
allTagsPath,
count: tag.docIds.length,
items: toDocListProp(),
unlisted: tag.unlisted,
};
}
exports.toTagDocListProp = toTagDocListProp;
function toTagsListTagsProp(versionTags) {
return Object.values(versionTags)
.filter((tagValue) => !tagValue.unlisted)
.map((tagValue) => ({
label: tagValue.label,
permalink: tagValue.permalink,
count: tagValue.docIds.length,
}));
}
exports.toTagsListTagsProp = toTagsListTagsProp;

View File

@@ -0,0 +1,22 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { PluginContentLoadedActions, RouteConfig } from '@docusaurus/types';
import type { FullVersion } from './types';
import type { PluginOptions } from '@docusaurus/plugin-content-docs';
type BuildAllRoutesParam = Omit<CreateAllRoutesParam, 'actions'> & {
actions: Omit<PluginContentLoadedActions, 'addRoute' | 'setGlobalData'>;
};
export declare function buildAllRoutes(param: BuildAllRoutesParam): Promise<RouteConfig[]>;
type CreateAllRoutesParam = {
baseUrl: string;
versions: FullVersion[];
options: PluginOptions;
actions: PluginContentLoadedActions;
aliasedSource: (str: string) => string;
};
export declare function createAllRoutes(param: CreateAllRoutesParam): Promise<void>;
export {};

View File

@@ -0,0 +1,168 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.createAllRoutes = exports.buildAllRoutes = void 0;
const tslib_1 = require("tslib");
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
const utils_1 = require("@docusaurus/utils");
const props_1 = require("./props");
const tags_1 = require("./tags");
async function buildVersionCategoryGeneratedIndexRoutes({ version, actions, options, aliasedSource, }) {
const slugs = (0, utils_1.createSlugger)();
async function buildCategoryGeneratedIndexRoute(categoryGeneratedIndex) {
const { sidebar, ...prop } = categoryGeneratedIndex;
const propFileName = slugs.slug(`${version.path}-${categoryGeneratedIndex.sidebar}-category-${categoryGeneratedIndex.title}`);
const propData = await actions.createData(`${(0, utils_1.docuHash)(`category/${propFileName}`)}.json`, JSON.stringify(prop, null, 2));
return {
path: categoryGeneratedIndex.permalink,
component: options.docCategoryGeneratedIndexComponent,
exact: true,
modules: {
categoryGeneratedIndex: aliasedSource(propData),
},
// Same as doc, this sidebar route attribute permits to associate this
// subpage to the given sidebar
...(sidebar && { sidebar }),
};
}
return Promise.all(version.categoryGeneratedIndices.map(buildCategoryGeneratedIndexRoute));
}
async function buildVersionDocRoutes({ version, actions, options, }) {
return Promise.all(version.docs.map(async (metadataItem) => {
await actions.createData(
// Note that this created data path must be in sync with
// metadataPath provided to mdx-loader.
`${(0, utils_1.docuHash)(metadataItem.source)}.json`, JSON.stringify(metadataItem, null, 2));
const docRoute = {
path: metadataItem.permalink,
component: options.docItemComponent,
exact: true,
modules: {
content: metadataItem.source,
},
// Because the parent (DocRoot) comp need to access it easily
// This permits to render the sidebar once without unmount/remount when
// navigating (and preserve sidebar state)
...(metadataItem.sidebar && {
sidebar: metadataItem.sidebar,
}),
};
return docRoute;
}));
}
async function buildVersionSidebarRoute(param) {
const [docRoutes, categoryGeneratedIndexRoutes] = await Promise.all([
buildVersionDocRoutes(param),
buildVersionCategoryGeneratedIndexRoutes(param),
]);
const subRoutes = [...docRoutes, ...categoryGeneratedIndexRoutes];
return {
path: param.version.path,
exact: false,
component: param.options.docRootComponent,
routes: subRoutes,
};
}
async function buildVersionTagsRoutes(param) {
const { version, options, actions, aliasedSource } = param;
const versionTags = (0, tags_1.getVersionTags)(version.docs);
async function buildTagsListRoute() {
// Don't create a tags list page if there's no tag
if (Object.keys(versionTags).length === 0) {
return null;
}
const tagsProp = (0, props_1.toTagsListTagsProp)(versionTags);
const tagsPropPath = await actions.createData(`${(0, utils_1.docuHash)(`tags-list-${version.versionName}-prop`)}.json`, JSON.stringify(tagsProp, null, 2));
return {
path: version.tagsPath,
exact: true,
component: options.docTagsListComponent,
modules: {
tags: aliasedSource(tagsPropPath),
},
};
}
async function buildTagDocListRoute(tag) {
const tagProps = (0, props_1.toTagDocListProp)({
allTagsPath: version.tagsPath,
tag,
docs: version.docs,
});
const tagPropPath = await actions.createData(`${(0, utils_1.docuHash)(`tag-${tag.permalink}`)}.json`, JSON.stringify(tagProps, null, 2));
return {
path: tag.permalink,
component: options.docTagDocListComponent,
exact: true,
modules: {
tag: aliasedSource(tagPropPath),
},
};
}
const [tagsListRoute, allTagsDocListRoutes] = await Promise.all([
buildTagsListRoute(),
Promise.all(Object.values(versionTags).map(buildTagDocListRoute)),
]);
return lodash_1.default.compact([tagsListRoute, ...allTagsDocListRoutes]);
}
async function buildVersionRoutes(param) {
const { version, actions, options, aliasedSource } = param;
async function buildVersionSubRoutes() {
const [sidebarRoute, tagsRoutes] = await Promise.all([
buildVersionSidebarRoute(param),
buildVersionTagsRoutes(param),
]);
return [sidebarRoute, ...tagsRoutes];
}
async function doBuildVersionRoutes() {
const versionProp = (0, props_1.toVersionMetadataProp)(options.id, version);
const versionPropPath = await actions.createData(`${(0, utils_1.docuHash)(`version-${version.versionName}-metadata-prop`)}.json`, JSON.stringify(versionProp, null, 2));
const subRoutes = await buildVersionSubRoutes();
return {
path: version.path,
exact: false,
component: options.docVersionRootComponent,
routes: subRoutes,
modules: {
version: aliasedSource(versionPropPath),
},
priority: version.routePriority,
};
}
try {
return await doBuildVersionRoutes();
}
catch (err) {
logger_1.default.error `Can't create version routes for version name=${version.versionName}`;
throw err;
}
}
// TODO we want this buildAllRoutes function to be easily testable
// Ideally, we should avoid side effects here (ie not injecting actions)
async function buildAllRoutes(param) {
const subRoutes = await Promise.all(param.versions.map((version) => buildVersionRoutes({
...param,
version,
})));
// all docs routes are wrapped under a single parent route, this ensures
// the theme layout never unmounts/remounts when navigating between versions
return [
{
path: (0, utils_1.normalizeUrl)([param.baseUrl, param.options.routeBasePath]),
exact: false,
component: param.options.docsRootComponent,
routes: subRoutes,
},
];
}
exports.buildAllRoutes = buildAllRoutes;
async function createAllRoutes(param) {
const routes = await buildAllRoutes(param);
routes.forEach(param.actions.addRoute);
}
exports.createAllRoutes = createAllRoutes;

View File

@@ -0,0 +1,9 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
export { CURRENT_VERSION_NAME, VERSIONED_DOCS_DIR, VERSIONED_SIDEBARS_DIR, VERSIONS_JSON_FILE, } from './constants';
export { filterVersions, getDefaultVersionBanner, getVersionBadge, getVersionBanner, } from './versions';
export { readVersionNames } from './versions/files';

View File

@@ -0,0 +1,25 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.readVersionNames = exports.getVersionBanner = exports.getVersionBadge = exports.getDefaultVersionBanner = exports.filterVersions = exports.VERSIONS_JSON_FILE = exports.VERSIONED_SIDEBARS_DIR = exports.VERSIONED_DOCS_DIR = exports.CURRENT_VERSION_NAME = void 0;
// APIs available to Node.js
// Those are undocumented but used by some third-party plugins
// For this reason it's preferable to avoid doing breaking changes
// See also https://github.com/facebook/docusaurus/pull/6477
var constants_1 = require("./constants");
Object.defineProperty(exports, "CURRENT_VERSION_NAME", { enumerable: true, get: function () { return constants_1.CURRENT_VERSION_NAME; } });
Object.defineProperty(exports, "VERSIONED_DOCS_DIR", { enumerable: true, get: function () { return constants_1.VERSIONED_DOCS_DIR; } });
Object.defineProperty(exports, "VERSIONED_SIDEBARS_DIR", { enumerable: true, get: function () { return constants_1.VERSIONED_SIDEBARS_DIR; } });
Object.defineProperty(exports, "VERSIONS_JSON_FILE", { enumerable: true, get: function () { return constants_1.VERSIONS_JSON_FILE; } });
var versions_1 = require("./versions");
Object.defineProperty(exports, "filterVersions", { enumerable: true, get: function () { return versions_1.filterVersions; } });
Object.defineProperty(exports, "getDefaultVersionBanner", { enumerable: true, get: function () { return versions_1.getDefaultVersionBanner; } });
Object.defineProperty(exports, "getVersionBadge", { enumerable: true, get: function () { return versions_1.getVersionBadge; } });
Object.defineProperty(exports, "getVersionBanner", { enumerable: true, get: function () { return versions_1.getVersionBanner; } });
var files_1 = require("./versions/files");
Object.defineProperty(exports, "readVersionNames", { enumerable: true, get: function () { return files_1.readVersionNames; } });

View File

@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { SidebarItemsGenerator } from './types';
export declare const CategoryMetadataFilenameBase = "_category_";
export declare const CategoryMetadataFilenamePattern = "_category_.{json,yml,yaml}";
export declare const DefaultSidebarItemsGenerator: SidebarItemsGenerator;

View File

@@ -0,0 +1,210 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.DefaultSidebarItemsGenerator = exports.CategoryMetadataFilenamePattern = exports.CategoryMetadataFilenameBase = void 0;
const tslib_1 = require("tslib");
const path_1 = tslib_1.__importDefault(require("path"));
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
const utils_1 = require("@docusaurus/utils");
const docs_1 = require("../docs");
const BreadcrumbSeparator = '/';
// Just an alias to the make code more explicit
function getLocalDocId(docId) {
return lodash_1.default.last(docId.split('/'));
}
exports.CategoryMetadataFilenameBase = '_category_';
exports.CategoryMetadataFilenamePattern = '_category_.{json,yml,yaml}';
// Comment for this feature: https://github.com/facebook/docusaurus/issues/3464#issuecomment-818670449
const DefaultSidebarItemsGenerator = ({ numberPrefixParser, isCategoryIndex, docs: allDocs, item: { dirName: autogenDir }, categoriesMetadata, }) => {
const docsById = (0, docs_1.createDocsByIdIndex)(allDocs);
const findDoc = (docId) => docsById[docId];
const getDoc = (docId) => {
const doc = findDoc(docId);
if (!doc) {
throw new Error(`Can't find any doc with ID ${docId}.
Available doc IDs:
- ${Object.keys(docsById).join('\n- ')}`);
}
return doc;
};
/**
* Step 1. Extract the docs that are in the autogen dir.
*/
function getAutogenDocs() {
function isInAutogeneratedDir(doc) {
return (
// Doc at the root of the autogenerated sidebar dir
doc.sourceDirName === autogenDir ||
// Autogen dir is . and doc is in subfolder
autogenDir === '.' ||
// Autogen dir is not . and doc is in subfolder
// "api/myDoc" startsWith "api/" (note "api2/myDoc" is not included)
doc.sourceDirName.startsWith((0, utils_1.addTrailingSlash)(autogenDir)));
}
const docs = allDocs.filter(isInAutogeneratedDir);
if (docs.length === 0) {
logger_1.default.warn `No docs found in path=${autogenDir}: can't auto-generate a sidebar.`;
}
return docs;
}
/**
* Step 2. Turn the linear file list into a tree structure.
*/
function treeify(docs) {
// 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()
function getRelativeBreadcrumb(doc) {
return autogenDir === doc.sourceDirName
? []
: doc.sourceDirName
.replace((0, utils_1.addTrailingSlash)(autogenDir), '')
.split(BreadcrumbSeparator);
}
const treeRoot = {};
docs.forEach((doc) => {
const breadcrumb = getRelativeBreadcrumb(doc);
// We walk down the file's path to generate the fs structure
let currentDir = treeRoot;
breadcrumb.forEach((dir) => {
if (typeof currentDir[dir] === 'undefined') {
currentDir[dir] = {}; // Create new folder.
}
currentDir = currentDir[dir]; // Go into the subdirectory.
});
// We've walked through the path. Register the file in this directory.
currentDir[path_1.default.basename(doc.source)] = doc.id;
});
return treeRoot;
}
/**
* 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) {
function createDocItem(id, fullPath, fileName) {
const { sidebarPosition: position, frontMatter: { sidebar_label: label, sidebar_class_name: className, sidebar_custom_props: customProps, }, } = getDoc(id);
return {
type: 'doc',
id,
position,
source: fileName,
// We don't want these fields to magically appear in the generated
// sidebar
...(label !== undefined && { label }),
...(className !== undefined && { className }),
...(customProps !== undefined && { customProps }),
};
}
function createCategoryItem(dir, fullPath, folderName) {
const categoryMetadata = categoriesMetadata[path_1.default.posix.join(autogenDir, fullPath)];
const allItems = Object.entries(dir).map(([key, content]) => dirToItem(content, key, `${fullPath}/${key}`));
// Try to match a doc inside the category folder,
// using the "local id" (myDoc) or "qualified id" (dirName/myDoc)
function findDocByLocalId(localId) {
return allItems.find((item) => item.type === 'doc' && getLocalDocId(item.id) === localId);
}
function findConventionalCategoryDocLink() {
return allItems.find((item) => {
if (item.type !== 'doc') {
return false;
}
const doc = getDoc(item.id);
return isCategoryIndex((0, docs_1.toCategoryIndexMatcherParam)(doc));
});
}
// In addition to the ID, this function also retrieves metadata of the
// linked doc that could be used as fallback values for category metadata
function getCategoryLinkedDocMetadata() {
const link = categoryMetadata?.link;
if (link !== undefined && link?.type !== 'doc') {
// If a link is explicitly specified, we won't apply conventions
return undefined;
}
const id = link
? findDocByLocalId(link.id)?.id ?? getDoc(link.id).id
: findConventionalCategoryDocLink()?.id;
if (!id) {
return undefined;
}
const doc = getDoc(id);
return {
id,
position: doc.sidebarPosition,
label: doc.frontMatter.sidebar_label ?? doc.title,
customProps: doc.frontMatter.sidebar_custom_props,
className: doc.frontMatter.sidebar_class_name,
};
}
const categoryLinkedDoc = getCategoryLinkedDocMetadata();
const link = categoryLinkedDoc
? {
type: 'doc',
id: categoryLinkedDoc.id, // We "remap" a potentially "local id" to a "qualified id"
}
: categoryMetadata?.link;
// If a doc is linked, remove it from the category subItems
const items = allItems.filter((item) => !(item.type === 'doc' && item.id === categoryLinkedDoc?.id));
const className = categoryMetadata?.className ?? categoryLinkedDoc?.className;
const customProps = categoryMetadata?.customProps ?? categoryLinkedDoc?.customProps;
const { filename, numberPrefix } = numberPrefixParser(folderName);
return {
type: 'category',
label: categoryMetadata?.label ?? categoryLinkedDoc?.label ?? filename,
collapsible: categoryMetadata?.collapsible,
collapsed: categoryMetadata?.collapsed,
position: categoryMetadata?.position ??
categoryLinkedDoc?.position ??
numberPrefix,
source: folderName,
...(customProps !== undefined && { customProps }),
...(className !== undefined && { className }),
items,
...(link && { link }),
};
}
function dirToItem(dir, // The directory item to be transformed.
itemKey, // File/folder name; for categories, it's used to generate the next `relativePath`.
fullPath) {
return typeof dir === 'object'
? createCategoryItem(dir, fullPath, itemKey)
: createDocItem(dir, fullPath, itemKey);
}
return Object.entries(fsModel).map(([key, content]) => dirToItem(content, key, key));
}
/**
* 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) {
const processedSidebarItems = sidebarItems.map((item) => {
if (item.type === 'category') {
return { ...item, items: sortItems(item.items) };
}
return item;
});
const sortedSidebarItems = lodash_1.default.sortBy(processedSidebarItems, [
'position',
'source',
]);
return sortedSidebarItems.map(({ position, source, ...item }) => item);
}
// TODO: the whole code is designed for pipeline operator
const docs = getAutogenDocs();
const fsModel = treeify(docs);
const sidebarWithPosition = generateSidebar(fsModel);
const sortedSidebar = sortItems(sidebarWithPosition);
return sortedSidebar;
};
exports.DefaultSidebarItemsGenerator = DefaultSidebarItemsGenerator;

View File

@@ -0,0 +1,13 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { PluginOptions } from '@docusaurus/plugin-content-docs';
import type { SidebarsConfig, Sidebars, SidebarProcessorParams } from './types';
export declare const DefaultSidebars: SidebarsConfig;
export declare const DisabledSidebars: SidebarsConfig;
export declare function resolveSidebarPathOption(siteDir: string, sidebarPathOption: PluginOptions['sidebarPath']): PluginOptions['sidebarPath'];
export declare function loadSidebarsFile(sidebarFilePath: string | false | undefined): Promise<SidebarsConfig>;
export declare function loadSidebars(sidebarFilePath: string | false | undefined, options: SidebarProcessorParams): Promise<Sidebars>;

View File

@@ -0,0 +1,98 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadSidebars = exports.loadSidebarsFile = exports.resolveSidebarPathOption = exports.DisabledSidebars = exports.DefaultSidebars = void 0;
const tslib_1 = require("tslib");
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
const path_1 = tslib_1.__importDefault(require("path"));
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
const utils_1 = require("@docusaurus/utils");
const js_yaml_1 = tslib_1.__importDefault(require("js-yaml"));
const combine_promises_1 = tslib_1.__importDefault(require("combine-promises"));
const validation_1 = require("./validation");
const normalization_1 = require("./normalization");
const processor_1 = require("./processor");
const postProcessor_1 = require("./postProcessor");
exports.DefaultSidebars = {
defaultSidebar: [
{
type: 'autogenerated',
dirName: '.',
},
],
};
exports.DisabledSidebars = {};
// If a path is provided, make it absolute
function resolveSidebarPathOption(siteDir, sidebarPathOption) {
return sidebarPathOption
? path_1.default.resolve(siteDir, sidebarPathOption)
: sidebarPathOption;
}
exports.resolveSidebarPathOption = resolveSidebarPathOption;
async function readCategoriesMetadata(contentPath) {
const categoryFiles = await (0, utils_1.Globby)('**/_category_.{json,yml,yaml}', {
cwd: contentPath,
});
const categoryToFile = lodash_1.default.groupBy(categoryFiles, path_1.default.dirname);
return (0, combine_promises_1.default)(lodash_1.default.mapValues(categoryToFile, async (files, folder) => {
const filePath = files[0];
if (files.length > 1) {
logger_1.default.warn `There are more than one category metadata files for path=${folder}: ${files.join(', ')}. The behavior is undetermined.`;
}
const content = await fs_extra_1.default.readFile(path_1.default.join(contentPath, filePath), 'utf-8');
try {
return (0, validation_1.validateCategoryMetadataFile)(js_yaml_1.default.load(content));
}
catch (err) {
logger_1.default.error `The docs sidebar category metadata file path=${filePath} looks invalid!`;
throw err;
}
}));
}
async function loadSidebarsFileUnsafe(sidebarFilePath) {
// false => no sidebars
if (sidebarFilePath === false) {
return exports.DisabledSidebars;
}
// undefined => defaults to autogenerated sidebars
if (typeof sidebarFilePath === 'undefined') {
return exports.DefaultSidebars;
}
// Non-existent sidebars file: no sidebars
// Note: this edge case can happen on versioned docs, not current version
// We avoid creating empty versioned sidebars file with the CLI
if (!(await fs_extra_1.default.pathExists(sidebarFilePath))) {
return exports.DisabledSidebars;
}
// We don't want sidebars to be cached because of hot reloading.
const module = await (0, utils_1.loadFreshModule)(sidebarFilePath);
// TODO unsafe, need to refactor and improve validation
return module;
}
async function loadSidebarsFile(sidebarFilePath) {
const sidebars = await loadSidebarsFileUnsafe(sidebarFilePath);
// TODO unsafe, need to refactor and improve validation
return sidebars;
}
exports.loadSidebarsFile = loadSidebarsFile;
async function loadSidebars(sidebarFilePath, options) {
try {
const sidebarsConfig = await loadSidebarsFileUnsafe(sidebarFilePath);
const normalizedSidebars = (0, normalization_1.normalizeSidebars)(sidebarsConfig);
(0, validation_1.validateSidebars)(normalizedSidebars);
const categoriesMetadata = await readCategoriesMetadata(options.version.contentPath);
const processedSidebars = await (0, processor_1.processSidebars)(normalizedSidebars, categoriesMetadata, options);
return (0, postProcessor_1.postProcessSidebars)(processedSidebars, options);
}
catch (err) {
logger_1.default.error `Sidebars file at path=${sidebarFilePath} failed to be loaded.`;
throw err;
}
}
exports.loadSidebars = loadSidebars;

View File

@@ -0,0 +1,13 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { NormalizedSidebarItem, NormalizedSidebars, SidebarItemConfig, SidebarsConfig } from './types';
/**
* Normalizes recursively item and all its children. Ensures that at the end
* each item will be an object with the corresponding type.
*/
export declare function normalizeItem(item: SidebarItemConfig): NormalizedSidebarItem[];
export declare function normalizeSidebars(sidebars: SidebarsConfig): NormalizedSidebars;

View File

@@ -0,0 +1,59 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.normalizeSidebars = exports.normalizeItem = void 0;
const tslib_1 = require("tslib");
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
const utils_1 = require("./utils");
function normalizeCategoriesShorthand(sidebar) {
return Object.entries(sidebar).map(([label, items]) => ({
type: 'category',
label,
items,
}));
}
/**
* Normalizes recursively item and all its children. Ensures that at the end
* each item will be an object with the corresponding type.
*/
function normalizeItem(item) {
if (typeof item === 'string') {
return [{ type: 'doc', id: item }];
}
if ((0, utils_1.isCategoriesShorthand)(item)) {
// This will never throw anyways
return normalizeSidebar(item, 'sidebar items slice');
}
if ((item.type === 'doc' || item.type === 'ref') &&
typeof item.label === 'string') {
return [{ ...item, translatable: true }];
}
if (item.type === 'category') {
const normalizedCategory = {
...item,
items: normalizeSidebar(item.items, logger_1.default.interpolate `code=${'items'} of the category name=${item.label}`),
};
return [normalizedCategory];
}
return [item];
}
exports.normalizeItem = normalizeItem;
function normalizeSidebar(sidebar, place) {
if (!Array.isArray(sidebar) && !(0, utils_1.isCategoriesShorthand)(sidebar)) {
throw new Error(logger_1.default.interpolate `Invalid sidebar items collection code=${JSON.stringify(sidebar)} in ${place}: it must either be an array of sidebar items or a shorthand notation (which doesn't contain a code=${'type'} property). See url=${'https://docusaurus.io/docs/sidebar/items'} for all valid syntaxes.`);
}
const normalizedSidebar = Array.isArray(sidebar)
? sidebar
: normalizeCategoriesShorthand(sidebar);
return normalizedSidebar.flatMap((subItem) => normalizeItem(subItem));
}
function normalizeSidebars(sidebars) {
return lodash_1.default.mapValues(sidebars, (sidebar, id) => normalizeSidebar(sidebar, logger_1.default.interpolate `sidebar name=${id}`));
}
exports.normalizeSidebars = normalizeSidebars;

View File

@@ -0,0 +1,11 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { Sidebars, SidebarProcessorParams, ProcessedSidebars } from './types';
export type SidebarPostProcessorParams = SidebarProcessorParams & {
draftIds: Set<string>;
};
export declare function postProcessSidebars(sidebars: ProcessedSidebars, params: SidebarProcessorParams): Sidebars;

View File

@@ -0,0 +1,80 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.postProcessSidebars = void 0;
const tslib_1 = require("tslib");
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const utils_1 = require("@docusaurus/utils");
function normalizeCategoryLink(category, params) {
if (category.link?.type === 'doc' && params.draftIds.has(category.link.id)) {
return undefined;
}
if (category.link?.type === 'generated-index') {
// Default slug logic can be improved
const getDefaultSlug = () => `/category/${params.categoryLabelSlugger.slug(category.label)}`;
const slug = category.link.slug ?? getDefaultSlug();
const permalink = (0, utils_1.normalizeUrl)([params.version.path, slug]);
return {
...category.link,
slug,
permalink,
};
}
return category.link;
}
function postProcessSidebarItem(item, params) {
if (item.type === 'category') {
// Fail-fast if there's actually no subitems, no because all subitems are
// drafts. This is likely a configuration mistake.
if (item.items.length === 0 && !item.link) {
throw new Error(`Sidebar category ${item.label} has neither any subitem nor a link. This makes this item not able to link to anything.`);
}
const category = {
...item,
collapsed: item.collapsed ?? params.sidebarOptions.sidebarCollapsed,
collapsible: item.collapsible ?? params.sidebarOptions.sidebarCollapsible,
link: normalizeCategoryLink(item, params),
items: item.items
.map((subItem) => postProcessSidebarItem(subItem, params))
.filter((v) => Boolean(v)),
};
// If the current category doesn't have subitems, we render a normal link
// instead.
if (category.items.length === 0) {
// Doesn't make sense to render an empty generated index page, so we
// filter the entire category out as well.
if (!category.link ||
category.link.type === 'generated-index' ||
params.draftIds.has(category.link.id)) {
return null;
}
return {
type: 'doc',
label: category.label,
id: category.link.id,
};
}
// A non-collapsible category can't be collapsed!
if (!category.collapsible) {
category.collapsed = false;
}
return category;
}
if ((item.type === 'doc' || item.type === 'ref') &&
params.draftIds.has(item.id)) {
return null;
}
return item;
}
function postProcessSidebars(sidebars, params) {
const draftIds = new Set(params.drafts.map((d) => d.id));
return lodash_1.default.mapValues(sidebars, (sidebar) => sidebar
.map((item) => postProcessSidebarItem(item, { ...params, draftIds }))
.filter((v) => Boolean(v)));
}
exports.postProcessSidebars = postProcessSidebars;

View File

@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { NormalizedSidebars, ProcessedSidebars, SidebarProcessorParams, CategoryMetadataFile } from './types';
export declare function processSidebars(unprocessedSidebars: NormalizedSidebars, categoriesMetadata: {
[filePath: string]: CategoryMetadataFile;
}, params: SidebarProcessorParams): Promise<ProcessedSidebars>;

View File

@@ -0,0 +1,78 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.processSidebars = void 0;
const tslib_1 = require("tslib");
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const combine_promises_1 = tslib_1.__importDefault(require("combine-promises"));
const generator_1 = require("./generator");
const validation_1 = require("./validation");
const docs_1 = require("../docs");
function toSidebarItemsGeneratorDoc(doc) {
return lodash_1.default.pick(doc, [
'id',
'title',
'frontMatter',
'source',
'sourceDirName',
'sidebarPosition',
]);
}
function toSidebarItemsGeneratorVersion(version) {
return lodash_1.default.pick(version, ['versionName', 'contentPath']);
}
// Handle the generation of autogenerated sidebar items and other
// post-processing checks
async function processSidebar(unprocessedSidebar, categoriesMetadata, params) {
const { sidebarItemsGenerator, numberPrefixParser, docs, version } = params;
// Just a minor lazy transformation optimization
const getSidebarItemsGeneratorDocsAndVersion = lodash_1.default.memoize(() => ({
docs: docs.map(toSidebarItemsGeneratorDoc),
version: toSidebarItemsGeneratorVersion(version),
}));
async function processAutoGeneratedItem(item) {
const generatedItems = await sidebarItemsGenerator({
item,
numberPrefixParser,
defaultSidebarItemsGenerator: generator_1.DefaultSidebarItemsGenerator,
isCategoryIndex: docs_1.isCategoryIndex,
...getSidebarItemsGeneratorDocsAndVersion(),
categoriesMetadata,
});
// Process again... weird but sidebar item generated might generate some
// auto-generated items?
// TODO repeatedly process & unwrap autogenerated items until there are no
// more autogenerated items, or when loop count (e.g. 10) is reached
return processItems(generatedItems);
}
async function processItem(item) {
if (item.type === 'category') {
return [
{
...item,
items: (await Promise.all(item.items.map(processItem))).flat(),
},
];
}
if (item.type === 'autogenerated') {
return processAutoGeneratedItem(item);
}
return [item];
}
async function processItems(items) {
return (await Promise.all(items.map(processItem))).flat();
}
const processedSidebar = await processItems(unprocessedSidebar);
return processedSidebar;
}
async function processSidebars(unprocessedSidebars, categoriesMetadata, params) {
const processedSidebars = await (0, combine_promises_1.default)(lodash_1.default.mapValues(unprocessedSidebars, (unprocessedSidebar) => processSidebar(unprocessedSidebar, categoriesMetadata, params)));
(0, validation_1.validateSidebars)(processedSidebars);
return processedSidebars;
}
exports.processSidebars = processSidebars;

View File

@@ -0,0 +1,188 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { Optional, Required } from 'utility-types';
import type { NumberPrefixParser, SidebarOptions, CategoryIndexMatcher, DocMetadataBase, VersionMetadata } from '@docusaurus/plugin-content-docs';
import type { Slugger } from '@docusaurus/utils';
type Expand<T extends {
[x: string]: unknown;
}> = {
[P in keyof T]: T[P];
};
export type SidebarItemBase = {
className?: string;
customProps?: {
[key: string]: unknown;
};
};
export type SidebarItemDoc = SidebarItemBase & {
type: 'doc' | 'ref';
label?: string;
id: string;
/**
* This is an internal marker. Items with labels defined in the config needs
* to be translated with JSON
*/
translatable?: true;
};
export type SidebarItemHtml = SidebarItemBase & {
type: 'html';
value: string;
defaultStyle?: boolean;
};
export type SidebarItemLink = SidebarItemBase & {
type: 'link';
href: string;
label: string;
autoAddBaseUrl?: boolean;
description?: string;
};
export type SidebarItemAutogenerated = SidebarItemBase & {
type: 'autogenerated';
dirName: string;
};
type SidebarItemCategoryBase = SidebarItemBase & {
type: 'category';
label: string;
collapsed: boolean;
collapsible: boolean;
description?: string;
};
export type SidebarItemCategoryLinkDoc = {
type: 'doc';
id: string;
};
export type SidebarItemCategoryLinkGeneratedIndexConfig = {
type: 'generated-index';
slug?: string;
title?: string;
description?: string;
image?: string;
keywords?: string | readonly string[];
};
export type SidebarItemCategoryLinkGeneratedIndex = {
type: 'generated-index';
slug: string;
permalink: string;
title?: string;
description?: string;
image?: string;
keywords?: string | readonly string[];
};
export type SidebarItemCategoryLinkConfig = SidebarItemCategoryLinkDoc | SidebarItemCategoryLinkGeneratedIndexConfig;
export type SidebarItemCategoryLink = SidebarItemCategoryLinkDoc | SidebarItemCategoryLinkGeneratedIndex;
export type SidebarItemCategoryConfig = Expand<Optional<SidebarItemCategoryBase, 'collapsed' | 'collapsible'> & {
items: SidebarCategoriesShorthand | SidebarItemConfig[];
link?: SidebarItemCategoryLinkConfig;
}>;
export type SidebarCategoriesShorthand = {
[sidebarCategory: string]: SidebarCategoriesShorthand | SidebarItemConfig[];
};
export type SidebarItemConfig = Omit<SidebarItemDoc, 'translatable'> | SidebarItemHtml | SidebarItemLink | SidebarItemAutogenerated | SidebarItemCategoryConfig | string | SidebarCategoriesShorthand;
export type SidebarConfig = SidebarCategoriesShorthand | SidebarItemConfig[];
export type SidebarsConfig = {
[sidebarId: string]: SidebarConfig;
};
export type NormalizedSidebarItemCategory = Expand<Optional<SidebarItemCategoryBase, 'collapsed' | 'collapsible'> & {
items: NormalizedSidebarItem[];
link?: SidebarItemCategoryLinkConfig;
}>;
export type NormalizedSidebarItem = SidebarItemDoc | SidebarItemHtml | SidebarItemLink | NormalizedSidebarItemCategory | SidebarItemAutogenerated;
export type NormalizedSidebar = NormalizedSidebarItem[];
export type NormalizedSidebars = {
[sidebarId: string]: NormalizedSidebar;
};
export type ProcessedSidebarItemCategory = Expand<Optional<SidebarItemCategoryBase, 'collapsed' | 'collapsible'> & {
items: ProcessedSidebarItem[];
link?: SidebarItemCategoryLinkConfig;
}>;
export type ProcessedSidebarItem = SidebarItemDoc | SidebarItemHtml | SidebarItemLink | ProcessedSidebarItemCategory;
export type ProcessedSidebar = ProcessedSidebarItem[];
export type ProcessedSidebars = {
[sidebarId: string]: ProcessedSidebar;
};
export type SidebarItemCategory = Expand<SidebarItemCategoryBase & {
items: SidebarItem[];
link?: SidebarItemCategoryLink;
}>;
export type SidebarItemCategoryWithLink = Required<SidebarItemCategory, 'link'>;
export type SidebarItemCategoryWithGeneratedIndex = SidebarItemCategoryWithLink & {
link: SidebarItemCategoryLinkGeneratedIndex;
};
export type SidebarItem = SidebarItemDoc | SidebarItemHtml | SidebarItemLink | SidebarItemCategory;
export type SidebarNavigationItem = SidebarItemDoc | SidebarItemCategoryWithLink;
export type Sidebar = SidebarItem[];
export type SidebarItemType = SidebarItem['type'];
export type Sidebars = {
[sidebarId: string]: Sidebar;
};
export type PropSidebarItemCategory = Expand<SidebarItemCategoryBase & {
items: PropSidebarItem[];
href?: string;
linkUnlisted?: boolean;
}>;
export type PropSidebarItemLink = SidebarItemLink & {
docId?: string;
unlisted?: boolean;
};
export type PropSidebarItemHtml = SidebarItemHtml;
export type PropSidebarItem = PropSidebarItemLink | PropSidebarItemCategory | PropSidebarItemHtml;
export type PropSidebar = PropSidebarItem[];
export type PropSidebars = {
[sidebarId: string]: PropSidebar;
};
export type PropSidebarBreadcrumbsItem = PropSidebarItemLink | PropSidebarItemCategory;
export type CategoryMetadataFile = {
label?: string;
position?: number;
collapsed?: boolean;
collapsible?: boolean;
className?: string;
link?: SidebarItemCategoryLinkConfig | null;
customProps?: {
[key: string]: unknown;
};
};
export type SidebarItemsGeneratorDoc = Pick<DocMetadataBase, 'id' | 'title' | 'frontMatter' | 'source' | 'sourceDirName' | 'sidebarPosition'>;
export type SidebarItemsGeneratorVersion = Pick<VersionMetadata, 'versionName' | 'contentPath'>;
export type SidebarItemsGeneratorArgs = {
/** The sidebar item with type "autogenerated" to be transformed. */
item: SidebarItemAutogenerated;
/** Useful metadata for the version this sidebar belongs to. */
version: SidebarItemsGeneratorVersion;
/** All the docs of that version (unfiltered). */
docs: SidebarItemsGeneratorDoc[];
/** Number prefix parser configured for this plugin. */
numberPrefixParser: NumberPrefixParser;
/** The default category index matcher which you can override. */
isCategoryIndex: CategoryIndexMatcher;
/**
* Key is the path relative to the doc content directory, value is the
* category metadata file's content.
*/
categoriesMetadata: {
[filePath: string]: CategoryMetadataFile;
};
};
export type SidebarItemsGenerator = (generatorArgs: SidebarItemsGeneratorArgs) => NormalizedSidebar | Promise<NormalizedSidebar>;
export type SidebarItemsGeneratorOption = (generatorArgs: {
/**
* Useful to re-use/enhance the default sidebar generation logic from
* Docusaurus.
* @see https://github.com/facebook/docusaurus/issues/4640#issuecomment-822292320
*/
defaultSidebarItemsGenerator: SidebarItemsGenerator;
} & SidebarItemsGeneratorArgs) => NormalizedSidebar | Promise<NormalizedSidebar>;
export type SidebarProcessorParams = {
sidebarItemsGenerator: SidebarItemsGeneratorOption;
numberPrefixParser: NumberPrefixParser;
docs: DocMetadataBase[];
drafts: DocMetadataBase[];
version: VersionMetadata;
categoryLabelSlugger: Slugger;
sidebarOptions: SidebarOptions;
};
export {};

View File

@@ -0,0 +1,8 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });

View File

@@ -0,0 +1,67 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { Sidebars, Sidebar, SidebarItem, SidebarItemCategory, SidebarItemLink, SidebarItemDoc, SidebarCategoriesShorthand, SidebarItemConfig, SidebarItemCategoryWithGeneratedIndex, SidebarNavigationItem } from './types';
import type { DocMetadataBase, PropNavigationLink, VersionMetadata } from '@docusaurus/plugin-content-docs';
export declare function isCategoriesShorthand(item: SidebarItemConfig): item is SidebarCategoriesShorthand;
export declare function transformSidebarItems(sidebar: Sidebar, updateFn: (item: SidebarItem) => SidebarItem): Sidebar;
export declare function collectSidebarDocItems(sidebar: Sidebar): SidebarItemDoc[];
export declare function collectSidebarCategories(sidebar: Sidebar): SidebarItemCategory[];
export declare function collectSidebarLinks(sidebar: Sidebar): SidebarItemLink[];
export declare function collectSidebarRefs(sidebar: Sidebar): SidebarItemDoc[];
export declare function collectSidebarDocIds(sidebar: Sidebar): string[];
export declare function collectSidebarNavigation(sidebar: Sidebar): SidebarNavigationItem[];
export declare function collectSidebarsDocIds(sidebars: Sidebars): {
[sidebarId: string]: string[];
};
export declare function collectSidebarsNavigations(sidebars: Sidebars): {
[sidebarId: string]: SidebarNavigationItem[];
};
export type SidebarNavigation = {
sidebarName: string | undefined;
previous: SidebarNavigationItem | undefined;
next: SidebarNavigationItem | undefined;
};
export type SidebarsUtils = {
sidebars: Sidebars;
getFirstDocIdOfFirstSidebar: () => string | undefined;
getSidebarNameByDocId: (docId: string) => string | undefined;
getDocNavigation: (params: {
docId: string;
displayedSidebar: string | null | undefined;
unlistedIds: Set<string>;
}) => SidebarNavigation;
getCategoryGeneratedIndexList: () => SidebarItemCategoryWithGeneratedIndex[];
getCategoryGeneratedIndexNavigation: (categoryGeneratedIndexPermalink: string) => SidebarNavigation;
/**
* This function may return undefined. This is usually a user mistake, because
* it means this sidebar will never be displayed; however, we can still use
* `displayed_sidebar` to make it displayed. Pretty weird but valid use-case
*/
getFirstLink: (sidebarId: string) => {
type: 'doc';
id: string;
label: string;
} | {
type: 'generated-index';
permalink: string;
label: string;
} | undefined;
checkLegacyVersionedSidebarNames: ({ versionMetadata, }: {
sidebarFilePath: string;
versionMetadata: VersionMetadata;
}) => void;
checkSidebarsDocIds: ({ allDocIds, sidebarFilePath, versionMetadata, }: {
allDocIds: string[];
sidebarFilePath: string;
versionMetadata: VersionMetadata;
}) => void;
};
export declare function createSidebarsUtils(sidebars: Sidebars): SidebarsUtils;
export declare function toDocNavigationLink(doc: DocMetadataBase): PropNavigationLink;
export declare function toNavigationLink(navigationItem: SidebarNavigationItem | undefined, docsById: {
[docId: string]: DocMetadataBase;
}): PropNavigationLink | undefined;

View File

@@ -0,0 +1,323 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.toNavigationLink = exports.toDocNavigationLink = exports.createSidebarsUtils = exports.collectSidebarsNavigations = exports.collectSidebarsDocIds = exports.collectSidebarNavigation = exports.collectSidebarDocIds = exports.collectSidebarRefs = exports.collectSidebarLinks = exports.collectSidebarCategories = exports.collectSidebarDocItems = exports.transformSidebarItems = exports.isCategoriesShorthand = void 0;
const tslib_1 = require("tslib");
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const utils_1 = require("@docusaurus/utils");
function isCategoriesShorthand(item) {
return typeof item === 'object' && !item.type;
}
exports.isCategoriesShorthand = isCategoriesShorthand;
function transformSidebarItems(sidebar, updateFn) {
function transformRecursive(item) {
if (item.type === 'category') {
return updateFn({
...item,
items: item.items.map(transformRecursive),
});
}
return updateFn(item);
}
return sidebar.map(transformRecursive);
}
exports.transformSidebarItems = transformSidebarItems;
/**
* 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) {
function flattenRecursive(item) {
return item.type === 'category'
? [item, ...item.items.flatMap(flattenRecursive)]
: [item];
}
return items.flatMap(flattenRecursive);
}
function collectSidebarItemsOfType(type, sidebar) {
return flattenSidebarItems(sidebar).filter((item) => item.type === type);
}
function collectSidebarDocItems(sidebar) {
return collectSidebarItemsOfType('doc', sidebar);
}
exports.collectSidebarDocItems = collectSidebarDocItems;
function collectSidebarCategories(sidebar) {
return collectSidebarItemsOfType('category', sidebar);
}
exports.collectSidebarCategories = collectSidebarCategories;
function collectSidebarLinks(sidebar) {
return collectSidebarItemsOfType('link', sidebar);
}
exports.collectSidebarLinks = collectSidebarLinks;
function collectSidebarRefs(sidebar) {
return collectSidebarItemsOfType('ref', sidebar);
}
exports.collectSidebarRefs = collectSidebarRefs;
// /!\ docId order matters for navigation!
function collectSidebarDocIds(sidebar) {
return flattenSidebarItems(sidebar).flatMap((item) => {
if (item.type === 'category') {
return item.link?.type === 'doc' ? [item.link.id] : [];
}
if (item.type === 'doc') {
return [item.id];
}
return [];
});
}
exports.collectSidebarDocIds = collectSidebarDocIds;
function collectSidebarNavigation(sidebar) {
return flattenSidebarItems(sidebar).flatMap((item) => {
if (item.type === 'category' && item.link) {
return [item];
}
if (item.type === 'doc') {
return [item];
}
return [];
});
}
exports.collectSidebarNavigation = collectSidebarNavigation;
function collectSidebarsDocIds(sidebars) {
return lodash_1.default.mapValues(sidebars, collectSidebarDocIds);
}
exports.collectSidebarsDocIds = collectSidebarsDocIds;
function collectSidebarsNavigations(sidebars) {
return lodash_1.default.mapValues(sidebars, collectSidebarNavigation);
}
exports.collectSidebarsNavigations = collectSidebarsNavigations;
function createSidebarsUtils(sidebars) {
const sidebarNameToDocIds = collectSidebarsDocIds(sidebars);
const sidebarNameToNavigationItems = collectSidebarsNavigations(sidebars);
// Reverse mapping
const docIdToSidebarName = Object.fromEntries(Object.entries(sidebarNameToDocIds).flatMap(([sidebarName, docIds]) => docIds.map((docId) => [docId, sidebarName])));
function getFirstDocIdOfFirstSidebar() {
return Object.values(sidebarNameToDocIds)[0]?.[0];
}
function getSidebarNameByDocId(docId) {
return docIdToSidebarName[docId];
}
function emptySidebarNavigation() {
return {
sidebarName: undefined,
previous: undefined,
next: undefined,
};
}
function getDocNavigation({ docId, displayedSidebar, unlistedIds, }) {
const sidebarName = displayedSidebar === undefined
? getSidebarNameByDocId(docId)
: displayedSidebar;
if (!sidebarName) {
return emptySidebarNavigation();
}
let navigationItems = sidebarNameToNavigationItems[sidebarName];
if (!navigationItems) {
throw new Error(`Doc with ID ${docId} wants to display sidebar ${sidebarName} but a sidebar with this name doesn't exist`);
}
// Filter unlisted items from navigation
navigationItems = navigationItems.filter((item) => {
if (item.type === 'doc' && unlistedIds.has(item.id)) {
return false;
}
if (item.type === 'category' &&
item.link.type === 'doc' &&
unlistedIds.has(item.link.id)) {
return false;
}
return true;
});
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 };
}
return {
sidebarName,
previous: navigationItems[currentItemIndex - 1],
next: navigationItems[currentItemIndex + 1],
};
}
function getCategoryGeneratedIndexList() {
return Object.values(sidebarNameToNavigationItems)
.flat()
.flatMap((item) => {
if (item.type === 'category' && item.link.type === 'generated-index') {
return [item];
}
return [];
});
}
/**
* We identity the category generated index by its permalink (should be
* unique). More reliable than using object identity
*/
function getCategoryGeneratedIndexNavigation(categoryGeneratedIndexPermalink) {
function isCurrentCategoryGeneratedIndexItem(item) {
return (item.type === 'category' &&
item.link.type === 'generated-index' &&
item.link.permalink === categoryGeneratedIndexPermalink);
}
const sidebarName = Object.entries(sidebarNameToNavigationItems).find(([, navigationItems]) => navigationItems.find(isCurrentCategoryGeneratedIndexItem))[0];
const navigationItems = sidebarNameToNavigationItems[sidebarName];
const currentItemIndex = navigationItems.findIndex(isCurrentCategoryGeneratedIndexItem);
return {
sidebarName,
previous: navigationItems[currentItemIndex - 1],
next: navigationItems[currentItemIndex + 1],
};
}
// TODO remove in Docusaurus v4
function getLegacyVersionedPrefix(versionMetadata) {
return `version-${versionMetadata.versionName}/`;
}
// In early v2, sidebar names used to be versioned
// example: "version-2.0.0-alpha.66/my-sidebar-name"
// In v3 it's not the case anymore and we throw an error to explain
// TODO remove in Docusaurus v4
function checkLegacyVersionedSidebarNames({ versionMetadata, sidebarFilePath, }) {
const illegalPrefix = getLegacyVersionedPrefix(versionMetadata);
const legacySidebarNames = Object.keys(sidebars).filter((sidebarName) => sidebarName.startsWith(illegalPrefix));
if (legacySidebarNames.length > 0) {
throw new Error(`Invalid sidebar file at "${(0, utils_1.toMessageRelativeFilePath)(sidebarFilePath)}".
These legacy versioned sidebar names are not supported anymore in Docusaurus v3:
- ${legacySidebarNames.sort().join('\n- ')}
The sidebar names you should now use are:
- ${legacySidebarNames
.sort()
.map((legacyName) => legacyName.split('/').splice(1).join('/'))
.join('\n- ')}
Please remove the "${illegalPrefix}" prefix from your versioned sidebar file.
This breaking change is documented on Docusaurus v3 release notes: https://docusaurus.io/blog/releases/3.0
`);
}
}
// throw a better error message for Docusaurus v3 breaking change
// TODO this can be removed in Docusaurus v4
function handleLegacyVersionedDocIds({ invalidDocIds, sidebarFilePath, versionMetadata, }) {
const illegalPrefix = getLegacyVersionedPrefix(versionMetadata);
// In older v2.0 alpha/betas, versioned docs had a legacy versioned prefix
// Example: "version-1.4/my-doc-id"
//
const legacyVersionedDocIds = invalidDocIds.filter((docId) => docId.startsWith(illegalPrefix));
if (legacyVersionedDocIds.length > 0) {
throw new Error(`Invalid sidebar file at "${(0, utils_1.toMessageRelativeFilePath)(sidebarFilePath)}".
These legacy versioned document ids are not supported anymore in Docusaurus v3:
- ${legacyVersionedDocIds.sort().join('\n- ')}
The document ids you should now use are:
- ${legacyVersionedDocIds
.sort()
.map((legacyId) => legacyId.split('/').splice(1).join('/'))
.join('\n- ')}
Please remove the "${illegalPrefix}" prefix from your versioned sidebar file.
This breaking change is documented on Docusaurus v3 release notes: https://docusaurus.io/blog/releases/3.0
`);
}
}
function checkSidebarsDocIds({ allDocIds, sidebarFilePath, versionMetadata, }) {
const allSidebarDocIds = Object.values(sidebarNameToDocIds).flat();
const invalidDocIds = lodash_1.default.difference(allSidebarDocIds, allDocIds);
if (invalidDocIds.length > 0) {
handleLegacyVersionedDocIds({
invalidDocIds,
sidebarFilePath,
versionMetadata,
});
throw new Error(`Invalid sidebar file at "${(0, utils_1.toMessageRelativeFilePath)(sidebarFilePath)}".
These sidebar document ids do not exist:
- ${invalidDocIds.sort().join('\n- ')}
Available document ids are:
- ${lodash_1.default.uniq(allDocIds).sort().join('\n- ')}
`);
}
}
function getFirstLink(sidebar) {
for (const item of sidebar) {
if (item.type === 'doc') {
return {
type: 'doc',
id: item.id,
label: item.label ?? item.id,
};
}
else if (item.type === 'category') {
if (item.link?.type === 'doc') {
return {
type: 'doc',
id: item.link.id,
label: item.label,
};
}
else if (item.link?.type === 'generated-index') {
return {
type: 'generated-index',
permalink: item.link.permalink,
label: item.label,
};
}
const firstSubItem = getFirstLink(item.items);
if (firstSubItem) {
return firstSubItem;
}
}
}
return undefined;
}
return {
sidebars,
getFirstDocIdOfFirstSidebar,
getSidebarNameByDocId,
getDocNavigation,
getCategoryGeneratedIndexList,
getCategoryGeneratedIndexNavigation,
checkLegacyVersionedSidebarNames,
checkSidebarsDocIds,
getFirstLink: (id) => getFirstLink(sidebars[id]),
};
}
exports.createSidebarsUtils = createSidebarsUtils;
function toDocNavigationLink(doc) {
const { title, permalink, frontMatter: { pagination_label: paginationLabel, sidebar_label: sidebarLabel, }, } = doc;
return { title: paginationLabel ?? sidebarLabel ?? title, permalink };
}
exports.toDocNavigationLink = toDocNavigationLink;
function toNavigationLink(navigationItem, docsById) {
function getDocById(docId) {
const doc = docsById[docId];
if (!doc) {
throw new Error(`Can't create navigation link: no doc found with id=${docId}`);
}
return doc;
}
if (!navigationItem) {
return undefined;
}
if (navigationItem.type === 'category') {
return navigationItem.link.type === 'doc'
? toDocNavigationLink(getDocById(navigationItem.link.id))
: {
title: navigationItem.label,
permalink: navigationItem.link.permalink,
};
}
return toDocNavigationLink(getDocById(navigationItem.id));
}
exports.toNavigationLink = toNavigationLink;

View File

@@ -0,0 +1,11 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { NormalizedSidebars, CategoryMetadataFile } from './types';
export declare function validateSidebars(sidebars: {
[sidebarId: string]: unknown;
}): asserts sidebars is NormalizedSidebars;
export declare function validateCategoryMetadataFile(unsafeContent: unknown): CategoryMetadataFile;

View File

@@ -0,0 +1,150 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateCategoryMetadataFile = exports.validateSidebars = void 0;
const utils_validation_1 = require("@docusaurus/utils-validation");
// 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
const sidebarItemBaseSchema = utils_validation_1.Joi.object({
className: utils_validation_1.Joi.string(),
customProps: utils_validation_1.Joi.object().unknown(),
});
const sidebarItemAutogeneratedSchema = sidebarItemBaseSchema.append({
type: 'autogenerated',
dirName: utils_validation_1.Joi.string()
.required()
.pattern(/^[^/](?:.*[^/])?$/)
.message('"dirName" must be a dir path relative to the docs folder root, and should not start or end with slash'),
});
const sidebarItemDocSchema = sidebarItemBaseSchema.append({
type: utils_validation_1.Joi.string().valid('doc', 'ref').required(),
id: utils_validation_1.Joi.string().required(),
label: utils_validation_1.Joi.string(),
translatable: utils_validation_1.Joi.boolean(),
});
const sidebarItemHtmlSchema = sidebarItemBaseSchema.append({
type: 'html',
value: utils_validation_1.Joi.string().required(),
defaultStyle: utils_validation_1.Joi.boolean(),
});
const sidebarItemLinkSchema = sidebarItemBaseSchema.append({
type: 'link',
href: utils_validation_1.URISchema.required(),
autoAddBaseUrl: utils_validation_1.Joi.boolean(),
label: utils_validation_1.Joi.string()
.required()
.messages({ 'any.unknown': '"label" must be a string' }),
description: utils_validation_1.Joi.string().optional().messages({
'any.unknown': '"description" must be a string',
}),
});
const sidebarItemCategoryLinkSchema = utils_validation_1.Joi.object()
.allow(null)
.when('.type', {
switch: [
{
is: 'doc',
then: utils_validation_1.Joi.object({
type: 'doc',
id: utils_validation_1.Joi.string().required(),
}),
},
{
is: 'generated-index',
then: utils_validation_1.Joi.object({
type: 'generated-index',
slug: utils_validation_1.Joi.string().optional(),
// This one is not in the user config, only in the normalized version
// permalink: Joi.string().optional(),
title: utils_validation_1.Joi.string().optional(),
description: utils_validation_1.Joi.string().optional(),
image: utils_validation_1.Joi.string().optional(),
keywords: [utils_validation_1.Joi.string(), utils_validation_1.Joi.array().items(utils_validation_1.Joi.string())],
}),
},
{
is: utils_validation_1.Joi.required(),
then: utils_validation_1.Joi.forbidden().messages({
'any.unknown': 'Unknown sidebar category link type "{.type}".',
}),
},
],
});
const sidebarItemCategorySchema = sidebarItemBaseSchema.append({
type: 'category',
label: utils_validation_1.Joi.string()
.required()
.messages({ 'any.unknown': '"label" must be a string' }),
items: utils_validation_1.Joi.array()
.required()
.messages({ 'any.unknown': '"items" must be an array' }),
// TODO: Joi doesn't allow mutual recursion. See https://github.com/sideway/joi/issues/2611
// .items(Joi.link('#sidebarItemSchema')),
link: sidebarItemCategoryLinkSchema,
collapsed: utils_validation_1.Joi.boolean().messages({
'any.unknown': '"collapsed" must be a boolean',
}),
collapsible: utils_validation_1.Joi.boolean().messages({
'any.unknown': '"collapsible" must be a boolean',
}),
description: utils_validation_1.Joi.string().optional().messages({
'any.unknown': '"description" must be a string',
}),
});
const sidebarItemSchema = utils_validation_1.Joi.object().when('.type', {
switch: [
{ is: 'link', then: sidebarItemLinkSchema },
{
is: utils_validation_1.Joi.string().valid('doc', 'ref').required(),
then: sidebarItemDocSchema,
},
{ is: 'html', then: sidebarItemHtmlSchema },
{ is: 'autogenerated', then: sidebarItemAutogeneratedSchema },
{ is: 'category', then: sidebarItemCategorySchema },
{
is: utils_validation_1.Joi.any().required(),
then: utils_validation_1.Joi.forbidden().messages({
'any.unknown': 'Unknown sidebar item type "{.type}".',
}),
},
],
});
// .id('sidebarItemSchema');
function validateSidebarItem(item) {
// TODO: remove once with proper Joi support
// Because we can't use Joi to validate nested items (see above), we do it
// manually
utils_validation_1.Joi.assert(item, sidebarItemSchema);
if (item.type === 'category') {
item.items.forEach(validateSidebarItem);
}
}
function validateSidebars(sidebars) {
Object.values(sidebars).forEach((sidebar) => {
sidebar.forEach(validateSidebarItem);
});
}
exports.validateSidebars = validateSidebars;
const categoryMetadataFileSchema = utils_validation_1.Joi.object({
label: utils_validation_1.Joi.string(),
position: utils_validation_1.Joi.number(),
collapsed: utils_validation_1.Joi.boolean(),
collapsible: utils_validation_1.Joi.boolean(),
className: utils_validation_1.Joi.string(),
link: sidebarItemCategoryLinkSchema,
customProps: utils_validation_1.Joi.object().unknown(),
});
function validateCategoryMetadataFile(unsafeContent) {
const { error, value } = categoryMetadataFileSchema.validate(unsafeContent);
if (error) {
throw error;
}
return value;
}
exports.validateCategoryMetadataFile = validateCategoryMetadataFile;

View File

@@ -0,0 +1,15 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { NumberPrefixParser, DocMetadataBase } from '@docusaurus/plugin-content-docs';
export default function getSlug({ baseID, frontMatterSlug, source, sourceDirName, stripDirNumberPrefixes, numberPrefixParser, }: {
baseID: string;
frontMatterSlug?: string;
source: DocMetadataBase['source'];
sourceDirName: DocMetadataBase['sourceDirName'];
stripDirNumberPrefixes?: boolean;
numberPrefixParser?: NumberPrefixParser;
}): string;

View File

@@ -0,0 +1,50 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("@docusaurus/utils");
const numberPrefix_1 = require("./numberPrefix");
const docs_1 = require("./docs");
function getSlug({ baseID, frontMatterSlug, source, sourceDirName, stripDirNumberPrefixes = true, numberPrefixParser = numberPrefix_1.DefaultNumberPrefixParser, }) {
function getDirNameSlug() {
const dirNameStripped = stripDirNumberPrefixes
? (0, numberPrefix_1.stripPathNumberPrefixes)(sourceDirName, numberPrefixParser)
: sourceDirName;
const resolveDirname = sourceDirName === '.'
? '/'
: (0, utils_1.addLeadingSlash)((0, utils_1.addTrailingSlash)(dirNameStripped));
return resolveDirname;
}
function computeSlug() {
if (frontMatterSlug?.startsWith('/')) {
return frontMatterSlug;
}
const dirNameSlug = getDirNameSlug();
if (!frontMatterSlug &&
(0, docs_1.isCategoryIndex)((0, docs_1.toCategoryIndexMatcherParam)({ source, sourceDirName }))) {
return dirNameSlug;
}
const baseSlug = frontMatterSlug ?? baseID;
return (0, utils_1.resolvePathname)(baseSlug, getDirNameSlug());
}
function ensureValidSlug(slug) {
if (!(0, utils_1.isValidPathname)(slug)) {
throw new Error(`We couldn't compute a valid slug for document with ID "${baseID}" in "${sourceDirName}" directory.
The slug we computed looks invalid: ${slug}.
Maybe your slug front matter is incorrect or there are special characters in the file path?
By using front matter to set a custom slug, you should be able to fix this error:
---
slug: /my/customDocPath
---
`);
}
return slug;
}
return ensureValidSlug(computeSlug());
}
exports.default = getSlug;

View File

@@ -0,0 +1,9 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { VersionTags } from './types';
import type { DocMetadata } from '@docusaurus/plugin-content-docs';
export declare function getVersionTags(docs: DocMetadata[]): VersionTags;

View File

@@ -0,0 +1,28 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.getVersionTags = void 0;
const tslib_1 = require("tslib");
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const utils_1 = require("@docusaurus/utils");
function getVersionTags(docs) {
const groups = (0, utils_1.groupTaggedItems)(docs, (doc) => doc.tags);
return lodash_1.default.mapValues(groups, ({ tag, items: tagDocs }) => {
const tagVisibility = (0, utils_1.getTagVisibility)({
items: tagDocs,
isUnlisted: (item) => item.unlisted,
});
return {
label: tag.label,
docIds: tagVisibility.listedItems.map((item) => item.id),
permalink: tag.permalink,
unlisted: tagVisibility.unlisted,
};
});
}
exports.getVersionTags = getVersionTags;

View File

@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { LoadedContent } from '@docusaurus/plugin-content-docs';
import type { TranslationFile } from '@docusaurus/types';
export declare function getLoadedContentTranslationFiles(loadedContent: LoadedContent): TranslationFile[];
export declare function translateLoadedContent(loadedContent: LoadedContent, translationFiles: TranslationFile[]): LoadedContent;

View File

@@ -0,0 +1,168 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.translateLoadedContent = exports.getLoadedContentTranslationFiles = void 0;
const tslib_1 = require("tslib");
const lodash_1 = tslib_1.__importDefault(require("lodash"));
const utils_1 = require("@docusaurus/utils");
const constants_1 = require("./constants");
const utils_2 = require("./sidebars/utils");
function getVersionFileName(versionName) {
if (versionName === constants_1.CURRENT_VERSION_NAME) {
return versionName;
}
// I don't like this "version-" prefix,
// but it's for consistency with site/versioned_docs
return `version-${versionName}`;
}
function getSidebarTranslationFileContent(sidebar, sidebarName) {
const categories = (0, utils_2.collectSidebarCategories)(sidebar);
const categoryContent = Object.fromEntries(categories.flatMap((category) => {
const entries = [];
entries.push([
`sidebar.${sidebarName}.category.${category.label}`,
{
message: category.label,
description: `The label for category ${category.label} in sidebar ${sidebarName}`,
},
]);
if (category.link?.type === 'generated-index') {
if (category.link.title) {
entries.push([
`sidebar.${sidebarName}.category.${category.label}.link.generated-index.title`,
{
message: category.link.title,
description: `The generated-index page title for category ${category.label} in sidebar ${sidebarName}`,
},
]);
}
if (category.link.description) {
entries.push([
`sidebar.${sidebarName}.category.${category.label}.link.generated-index.description`,
{
message: category.link.description,
description: `The generated-index page description for category ${category.label} in sidebar ${sidebarName}`,
},
]);
}
}
return entries;
}));
const links = (0, utils_2.collectSidebarLinks)(sidebar);
const linksContent = Object.fromEntries(links.map((link) => [
`sidebar.${sidebarName}.link.${link.label}`,
{
message: link.label,
description: `The label for link ${link.label} in sidebar ${sidebarName}, linking to ${link.href}`,
},
]));
const docs = (0, utils_2.collectSidebarDocItems)(sidebar)
.concat((0, utils_2.collectSidebarRefs)(sidebar))
.filter((item) => item.translatable);
const docLinksContent = Object.fromEntries(docs.map((doc) => [
`sidebar.${sidebarName}.doc.${doc.label}`,
{
message: doc.label,
description: `The label for the doc item ${doc.label} in sidebar ${sidebarName}, linking to the doc ${doc.id}`,
},
]));
return (0, utils_1.mergeTranslations)([categoryContent, linksContent, docLinksContent]);
}
function translateSidebar({ sidebar, sidebarName, sidebarsTranslations, }) {
function transformSidebarCategoryLink(category) {
if (!category.link) {
return undefined;
}
if (category.link.type === 'generated-index') {
const title = sidebarsTranslations[`sidebar.${sidebarName}.category.${category.label}.link.generated-index.title`]?.message ?? category.link.title;
const description = sidebarsTranslations[`sidebar.${sidebarName}.category.${category.label}.link.generated-index.description`]?.message ?? category.link.description;
return {
...category.link,
title,
description,
};
}
return category.link;
}
return (0, utils_2.transformSidebarItems)(sidebar, (item) => {
if (item.type === 'category') {
const link = transformSidebarCategoryLink(item);
return {
...item,
label: sidebarsTranslations[`sidebar.${sidebarName}.category.${item.label}`]
?.message ?? item.label,
...(link && { link }),
};
}
if (item.type === 'link') {
return {
...item,
label: sidebarsTranslations[`sidebar.${sidebarName}.link.${item.label}`]
?.message ?? item.label,
};
}
if ((item.type === 'doc' || item.type === 'ref') && item.translatable) {
return {
...item,
label: sidebarsTranslations[`sidebar.${sidebarName}.doc.${item.label}`]
?.message ?? item.label,
};
}
return item;
});
}
function getSidebarsTranslations(version) {
return (0, utils_1.mergeTranslations)(Object.entries(version.sidebars).map(([sidebarName, sidebar]) => getSidebarTranslationFileContent(sidebar, sidebarName)));
}
function translateSidebars(version, sidebarsTranslations) {
return lodash_1.default.mapValues(version.sidebars, (sidebar, sidebarName) => translateSidebar({
sidebar,
sidebarName,
sidebarsTranslations,
}));
}
function getVersionTranslationFiles(version) {
const versionTranslations = {
'version.label': {
message: version.label,
description: `The label for version ${version.versionName}`,
},
};
const sidebarsTranslations = getSidebarsTranslations(version);
return [
{
path: getVersionFileName(version.versionName),
content: (0, utils_1.mergeTranslations)([versionTranslations, sidebarsTranslations]),
},
];
}
function translateVersion(version, translationFiles) {
const versionTranslations = translationFiles[getVersionFileName(version.versionName)].content;
return {
...version,
label: versionTranslations['version.label']?.message ?? version.label,
sidebars: translateSidebars(version, versionTranslations),
};
}
function getVersionsTranslationFiles(versions) {
return versions.flatMap(getVersionTranslationFiles);
}
function translateVersions(versions, translationFiles) {
return versions.map((version) => translateVersion(version, translationFiles));
}
function getLoadedContentTranslationFiles(loadedContent) {
return getVersionsTranslationFiles(loadedContent.loadedVersions);
}
exports.getLoadedContentTranslationFiles = getLoadedContentTranslationFiles;
function translateLoadedContent(loadedContent, translationFiles) {
const translationFilesMap = lodash_1.default.keyBy(translationFiles, (f) => f.path);
return {
loadedVersions: translateVersions(loadedContent.loadedVersions, translationFilesMap),
};
}
exports.translateLoadedContent = translateLoadedContent;

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { BrokenMarkdownLink, Tag } from '@docusaurus/utils';
import type { VersionMetadata, LoadedVersion, CategoryGeneratedIndexMetadata } from '@docusaurus/plugin-content-docs';
import type { SidebarsUtils } from './sidebars/utils';
export type DocFile = {
contentPath: string;
filePath: string;
source: string;
content: string;
};
export type SourceToPermalink = {
[source: string]: string;
};
export type VersionTag = Tag & {
/** All doc ids having this tag. */
docIds: string[];
unlisted: boolean;
};
export type VersionTags = {
[permalink: string]: VersionTag;
};
export type FullVersion = LoadedVersion & {
sidebarsUtils: SidebarsUtils;
categoryGeneratedIndices: CategoryGeneratedIndexMetadata[];
};
export type DocBrokenMarkdownLink = BrokenMarkdownLink<VersionMetadata>;
export type DocsMarkdownOption = {
versionsMetadata: VersionMetadata[];
siteDir: string;
sourceToPermalink: SourceToPermalink;
onBrokenMarkdownLink: (brokenMarkdownLink: DocBrokenMarkdownLink) => void;
};

View File

@@ -0,0 +1,8 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });

View File

@@ -0,0 +1,50 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { PluginOptions, VersionMetadata } from '@docusaurus/plugin-content-docs';
import type { VersionContext } from './index';
/** `[siteDir]/community_versioned_docs/version-1.0.0` */
export declare function getVersionDocsDirPath(siteDir: string, pluginId: string, versionName: string): string;
/** `[siteDir]/community_versioned_sidebars/version-1.0.0-sidebars.json` */
export declare function getVersionSidebarsPath(siteDir: string, pluginId: string, versionName: string): string;
export declare function getDocsDirPathLocalized({ localizationDir, pluginId, versionName, }: {
localizationDir: string;
pluginId: string;
versionName: string;
}): string;
/** `community` => `[siteDir]/community_versions.json` */
export declare function getVersionsFilePath(siteDir: string, pluginId: string): string;
/**
* Reads the plugin's respective `versions.json` file, and returns its content.
*
* @throws Throws if validation fails, i.e. `versions.json` doesn't contain an
* array of valid version names.
*/
export declare function readVersionsFile(siteDir: string, pluginId: string): Promise<string[] | null>;
/**
* Reads the `versions.json` file, and returns an ordered list of version names.
*
* - If `disableVersioning` is turned on, it will return `["current"]` (requires
* `includeCurrentVersion` to be true);
* - If `includeCurrentVersion` is turned on, "current" will be inserted at the
* beginning, if not already there.
*
* You need to use {@link filterVersions} after this.
*
* @throws Throws an error if `disableVersioning: true` but `versions.json`
* doesn't exist (i.e. site is not versioned)
* @throws Throws an error if versions list is empty (empty `versions.json` or
* `disableVersioning` is true, and not including current version)
*/
export declare function readVersionNames(siteDir: string, options: PluginOptions): Promise<string[]>;
/**
* Gets the path-related version metadata.
*
* @throws Throws if the resolved docs folder or sidebars file doesn't exist.
* Does not throw if a versioned sidebar is missing (since we don't create empty
* files).
*/
export declare function getVersionMetadataPaths({ versionName, context, options, }: VersionContext): Promise<Pick<VersionMetadata, 'contentPath' | 'contentPathLocalized' | 'sidebarFilePath'>>;

View File

@@ -0,0 +1,141 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.getVersionMetadataPaths = exports.readVersionNames = exports.readVersionsFile = exports.getVersionsFilePath = exports.getDocsDirPathLocalized = exports.getVersionSidebarsPath = exports.getVersionDocsDirPath = void 0;
const tslib_1 = require("tslib");
const path_1 = tslib_1.__importDefault(require("path"));
const fs_extra_1 = tslib_1.__importDefault(require("fs-extra"));
const utils_1 = require("@docusaurus/utils");
const constants_1 = require("../constants");
const validation_1 = require("./validation");
/** Add a prefix like `community_version-1.0.0`. No-op for default instance. */
function addPluginIdPrefix(fileOrDir, pluginId) {
return pluginId === utils_1.DEFAULT_PLUGIN_ID
? fileOrDir
: `${pluginId}_${fileOrDir}`;
}
/** `[siteDir]/community_versioned_docs/version-1.0.0` */
function getVersionDocsDirPath(siteDir, pluginId, versionName) {
return path_1.default.join(siteDir, addPluginIdPrefix(constants_1.VERSIONED_DOCS_DIR, pluginId), `version-${versionName}`);
}
exports.getVersionDocsDirPath = getVersionDocsDirPath;
/** `[siteDir]/community_versioned_sidebars/version-1.0.0-sidebars.json` */
function getVersionSidebarsPath(siteDir, pluginId, versionName) {
return path_1.default.join(siteDir, addPluginIdPrefix(constants_1.VERSIONED_SIDEBARS_DIR, pluginId), `version-${versionName}-sidebars.json`);
}
exports.getVersionSidebarsPath = getVersionSidebarsPath;
function getDocsDirPathLocalized({ localizationDir, pluginId, versionName, }) {
return (0, utils_1.getPluginI18nPath)({
localizationDir,
pluginName: 'docusaurus-plugin-content-docs',
pluginId,
subPaths: [
versionName === constants_1.CURRENT_VERSION_NAME
? constants_1.CURRENT_VERSION_NAME
: `version-${versionName}`,
],
});
}
exports.getDocsDirPathLocalized = getDocsDirPathLocalized;
/** `community` => `[siteDir]/community_versions.json` */
function getVersionsFilePath(siteDir, pluginId) {
return path_1.default.join(siteDir, addPluginIdPrefix(constants_1.VERSIONS_JSON_FILE, pluginId));
}
exports.getVersionsFilePath = getVersionsFilePath;
/**
* Reads the plugin's respective `versions.json` file, and returns its content.
*
* @throws Throws if validation fails, i.e. `versions.json` doesn't contain an
* array of valid version names.
*/
async function readVersionsFile(siteDir, pluginId) {
const versionsFilePath = getVersionsFilePath(siteDir, pluginId);
if (await fs_extra_1.default.pathExists(versionsFilePath)) {
const content = await fs_extra_1.default.readJSON(versionsFilePath);
(0, validation_1.validateVersionNames)(content);
return content;
}
return null;
}
exports.readVersionsFile = readVersionsFile;
/**
* Reads the `versions.json` file, and returns an ordered list of version names.
*
* - If `disableVersioning` is turned on, it will return `["current"]` (requires
* `includeCurrentVersion` to be true);
* - If `includeCurrentVersion` is turned on, "current" will be inserted at the
* beginning, if not already there.
*
* You need to use {@link filterVersions} after this.
*
* @throws Throws an error if `disableVersioning: true` but `versions.json`
* doesn't exist (i.e. site is not versioned)
* @throws Throws an error if versions list is empty (empty `versions.json` or
* `disableVersioning` is true, and not including current version)
*/
async function readVersionNames(siteDir, options) {
const versionFileContent = await readVersionsFile(siteDir, options.id);
if (!versionFileContent && options.disableVersioning) {
throw new Error(`Docs: using "disableVersioning: true" option on a non-versioned site does not make sense.`);
}
const versions = options.disableVersioning ? [] : versionFileContent ?? [];
// We add the current version at the beginning, unless:
// - user don't want to; or
// - it's already been explicitly added to versions.json
if (options.includeCurrentVersion &&
!versions.includes(constants_1.CURRENT_VERSION_NAME)) {
versions.unshift(constants_1.CURRENT_VERSION_NAME);
}
if (versions.length === 0) {
throw new Error(`It is not possible to use docs without any version. No version is included because you have requested to not include ${path_1.default.resolve(options.path)} through "includeCurrentVersion: false", while ${options.disableVersioning
? 'versioning is disabled with "disableVersioning: true"'
: `the versions file is empty/non-existent`}.`);
}
return versions;
}
exports.readVersionNames = readVersionNames;
/**
* Gets the path-related version metadata.
*
* @throws Throws if the resolved docs folder or sidebars file doesn't exist.
* Does not throw if a versioned sidebar is missing (since we don't create empty
* files).
*/
async function getVersionMetadataPaths({ versionName, context, options, }) {
const isCurrent = versionName === constants_1.CURRENT_VERSION_NAME;
const contentPathLocalized = getDocsDirPathLocalized({
localizationDir: context.localizationDir,
pluginId: options.id,
versionName,
});
const contentPath = isCurrent
? path_1.default.resolve(context.siteDir, options.path)
: getVersionDocsDirPath(context.siteDir, options.id, versionName);
const sidebarFilePath = isCurrent
? options.sidebarPath
: getVersionSidebarsPath(context.siteDir, options.id, versionName);
if (!(await fs_extra_1.default.pathExists(contentPath))) {
throw new Error(`The docs folder does not exist for version "${versionName}". A docs folder is expected to be found at ${path_1.default.relative(context.siteDir, contentPath)}.`);
}
// 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 (versionName === constants_1.CURRENT_VERSION_NAME &&
typeof sidebarFilePath === 'string' &&
!(await fs_extra_1.default.pathExists(sidebarFilePath))) {
throw new Error(`The path to the sidebar file does not exist at "${path_1.default.relative(context.siteDir, sidebarFilePath)}".
Please set the docs "sidebarPath" field in your config file to:
- a sidebars path that exists
- false: to disable the sidebar
- undefined: for Docusaurus to generate it automatically`);
}
return { contentPath, contentPathLocalized, sidebarFilePath };
}
exports.getVersionMetadataPaths = getVersionMetadataPaths;

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { FullVersion } from '../types';
import type { LoadContext } from '@docusaurus/types';
import type { LoadedVersion, PluginOptions, VersionBanner, VersionMetadata } from '@docusaurus/plugin-content-docs';
export type VersionContext = {
/** The version name to get banner of. */
versionName: string;
/** All versions, ordered from newest to oldest. */
versionNames: string[];
lastVersionName: string;
context: LoadContext;
options: PluginOptions;
};
/**
* The default version banner depends on the version's relative position to the
* latest version. More recent ones are "unreleased", and older ones are
* "unmaintained".
*/
export declare function getDefaultVersionBanner({ versionName, versionNames, lastVersionName, }: VersionContext): VersionBanner | null;
export declare function getVersionBanner(context: VersionContext): VersionMetadata['banner'];
export declare function getVersionBadge({ versionName, versionNames, options, }: VersionContext): VersionMetadata['badge'];
export declare function getVersionNoIndex({ versionName, options, }: VersionContext): VersionMetadata['noIndex'];
/**
* Filter versions according to provided options (i.e. `onlyIncludeVersions`).
*
* Note: we preserve the order in which versions are provided; the order of the
* `onlyIncludeVersions` array does not matter
*/
export declare function filterVersions(versionNamesUnfiltered: string[], options: PluginOptions): string[];
export declare function readVersionsMetadata({ context, options, }: {
context: LoadContext;
options: PluginOptions;
}): Promise<VersionMetadata[]>;
export declare function toFullVersion(version: LoadedVersion): FullVersion;

View File

@@ -0,0 +1,173 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.toFullVersion = exports.readVersionsMetadata = exports.filterVersions = exports.getVersionNoIndex = exports.getVersionBadge = exports.getVersionBanner = exports.getDefaultVersionBanner = void 0;
const tslib_1 = require("tslib");
const path_1 = tslib_1.__importDefault(require("path"));
const utils_1 = require("@docusaurus/utils");
const constants_1 = require("../constants");
const validation_1 = require("./validation");
const files_1 = require("./files");
const utils_2 = require("../sidebars/utils");
const categoryGeneratedIndex_1 = require("../categoryGeneratedIndex");
function getVersionEditUrls({ contentPath, contentPathLocalized, context, options, }) {
// If the user is using the functional form of editUrl,
// she has total freedom and we can't compute a "version edit url"
if (!options.editUrl || typeof options.editUrl === 'function') {
return { editUrl: undefined, editUrlLocalized: undefined };
}
const editDirPath = options.editCurrentVersion ? options.path : contentPath;
const editDirPathLocalized = options.editCurrentVersion
? (0, files_1.getDocsDirPathLocalized)({
localizationDir: context.localizationDir,
versionName: constants_1.CURRENT_VERSION_NAME,
pluginId: options.id,
})
: contentPathLocalized;
const versionPathSegment = (0, utils_1.posixPath)(path_1.default.relative(context.siteDir, path_1.default.resolve(context.siteDir, editDirPath)));
const versionPathSegmentLocalized = (0, utils_1.posixPath)(path_1.default.relative(context.siteDir, path_1.default.resolve(context.siteDir, editDirPathLocalized)));
const editUrl = (0, utils_1.normalizeUrl)([options.editUrl, versionPathSegment]);
const editUrlLocalized = (0, utils_1.normalizeUrl)([
options.editUrl,
versionPathSegmentLocalized,
]);
return { editUrl, editUrlLocalized };
}
/**
* The default version banner depends on the version's relative position to the
* latest version. More recent ones are "unreleased", and older ones are
* "unmaintained".
*/
function getDefaultVersionBanner({ versionName, versionNames, lastVersionName, }) {
// Current version: good, no banner
if (versionName === lastVersionName) {
return null;
}
// Upcoming versions: unreleased banner
if (versionNames.indexOf(versionName) < versionNames.indexOf(lastVersionName)) {
return 'unreleased';
}
// Older versions: display unmaintained banner
return 'unmaintained';
}
exports.getDefaultVersionBanner = getDefaultVersionBanner;
function getVersionBanner(context) {
const { versionName, options } = context;
const versionBannerOption = options.versions[versionName]?.banner;
if (versionBannerOption) {
return versionBannerOption === 'none' ? null : versionBannerOption;
}
return getDefaultVersionBanner(context);
}
exports.getVersionBanner = getVersionBanner;
function getVersionBadge({ versionName, versionNames, options, }) {
// If site is not versioned or only one version is included
// we don't show the version badge by default
// See https://github.com/facebook/docusaurus/issues/3362
const defaultVersionBadge = versionNames.length !== 1;
return options.versions[versionName]?.badge ?? defaultVersionBadge;
}
exports.getVersionBadge = getVersionBadge;
function getVersionNoIndex({ versionName, options, }) {
return options.versions[versionName]?.noIndex ?? false;
}
exports.getVersionNoIndex = getVersionNoIndex;
function getVersionClassName({ versionName, options, }) {
const defaultVersionClassName = `docs-version-${versionName}`;
return options.versions[versionName]?.className ?? defaultVersionClassName;
}
function getVersionLabel({ versionName, options, }) {
const defaultVersionLabel = versionName === constants_1.CURRENT_VERSION_NAME ? 'Next' : versionName;
return options.versions[versionName]?.label ?? defaultVersionLabel;
}
function getVersionPathPart({ versionName, options, lastVersionName, }) {
function getDefaultVersionPathPart() {
if (versionName === lastVersionName) {
return '';
}
return versionName === constants_1.CURRENT_VERSION_NAME ? 'next' : versionName;
}
return options.versions[versionName]?.path ?? getDefaultVersionPathPart();
}
async function createVersionMetadata(context) {
const { versionName, lastVersionName, options, context: loadContext } = context;
const { sidebarFilePath, contentPath, contentPathLocalized } = await (0, files_1.getVersionMetadataPaths)(context);
const versionPathPart = getVersionPathPart(context);
const routePath = (0, utils_1.normalizeUrl)([
loadContext.baseUrl,
options.routeBasePath,
versionPathPart,
]);
const versionEditUrls = getVersionEditUrls({
contentPath,
contentPathLocalized,
context: loadContext,
options,
});
return {
versionName,
label: getVersionLabel(context),
banner: getVersionBanner(context),
badge: getVersionBadge(context),
noIndex: getVersionNoIndex(context),
className: getVersionClassName(context),
path: routePath,
tagsPath: (0, utils_1.normalizeUrl)([routePath, options.tagsBasePath]),
...versionEditUrls,
isLast: versionName === lastVersionName,
routePriority: versionPathPart === '' ? -1 : undefined,
sidebarFilePath,
contentPath,
contentPathLocalized,
};
}
/**
* Filter versions according to provided options (i.e. `onlyIncludeVersions`).
*
* Note: we preserve the order in which versions are provided; the order of the
* `onlyIncludeVersions` array does not matter
*/
function filterVersions(versionNamesUnfiltered, options) {
if (options.onlyIncludeVersions) {
return versionNamesUnfiltered.filter((name) => options.onlyIncludeVersions.includes(name));
}
return versionNamesUnfiltered;
}
exports.filterVersions = filterVersions;
function getLastVersionName({ versionNames, options, }) {
return (options.lastVersion ??
versionNames.find((name) => name !== constants_1.CURRENT_VERSION_NAME) ??
constants_1.CURRENT_VERSION_NAME);
}
async function readVersionsMetadata({ context, options, }) {
const allVersionNames = await (0, files_1.readVersionNames)(context.siteDir, options);
(0, validation_1.validateVersionsOptions)(allVersionNames, options);
const versionNames = filterVersions(allVersionNames, options);
const lastVersionName = getLastVersionName({ versionNames, options });
const versionsMetadata = await Promise.all(versionNames.map((versionName) => createVersionMetadata({
versionName,
versionNames,
lastVersionName,
context,
options,
})));
return versionsMetadata;
}
exports.readVersionsMetadata = readVersionsMetadata;
function toFullVersion(version) {
const sidebarsUtils = (0, utils_2.createSidebarsUtils)(version.sidebars);
return {
...version,
sidebarsUtils,
categoryGeneratedIndices: (0, categoryGeneratedIndex_1.getCategoryGeneratedIndexMetadataList)({
docs: version.docs,
sidebarsUtils,
}),
};
}
exports.toFullVersion = toFullVersion;

View File

@@ -0,0 +1,17 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type { VersionsOptions } from '@docusaurus/plugin-content-docs';
export declare function validateVersionName(name: unknown): asserts name is string;
export declare function validateVersionNames(names: unknown): asserts names is string[];
/**
* @throws Throws for one of the following invalid options:
* - `lastVersion` is non-existent
* - `versions` includes unknown keys
* - `onlyIncludeVersions` is empty, contains unknown names, or doesn't include
* `latestVersion` (if provided)
*/
export declare function validateVersionsOptions(availableVersionNames: string[], options: VersionsOptions): void;

View File

@@ -0,0 +1,71 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateVersionsOptions = exports.validateVersionNames = exports.validateVersionName = void 0;
const tslib_1 = require("tslib");
const lodash_1 = tslib_1.__importDefault(require("lodash"));
function validateVersionName(name) {
if (typeof name !== 'string') {
throw new Error(`Versions should be strings. Found type "${typeof name}" for version ${JSON.stringify(name)}.`);
}
if (!name.trim()) {
throw new Error(`Invalid version name "${name}": version name must contain at least one non-whitespace character.`);
}
const errors = [
[/[/\\]/, 'should not include slash (/) or backslash (\\)'],
[/.{33,}/, 'cannot be longer than 32 characters'],
// eslint-disable-next-line no-control-regex
[/[<>:"|?*\x00-\x1F]/, 'should be a valid file path'],
[/^\.\.?$/, 'should not be "." or ".."'],
];
errors.forEach(([pattern, message]) => {
if (pattern.test(name)) {
throw new Error(`Invalid version name "${name}": version name ${message}.`);
}
});
}
exports.validateVersionName = validateVersionName;
function validateVersionNames(names) {
if (!Array.isArray(names)) {
throw new Error(`The versions file should contain an array of version names! Found content: ${JSON.stringify(names)}`);
}
names.forEach(validateVersionName);
}
exports.validateVersionNames = validateVersionNames;
/**
* @throws Throws for one of the following invalid options:
* - `lastVersion` is non-existent
* - `versions` includes unknown keys
* - `onlyIncludeVersions` is empty, contains unknown names, or doesn't include
* `latestVersion` (if provided)
*/
function validateVersionsOptions(availableVersionNames, options) {
const availableVersionNamesMsg = `Available version names are: ${availableVersionNames.join(', ')}`;
if (options.lastVersion &&
!availableVersionNames.includes(options.lastVersion)) {
throw new Error(`Docs option lastVersion: ${options.lastVersion} is invalid. ${availableVersionNamesMsg}`);
}
const unknownVersionConfigNames = lodash_1.default.difference(Object.keys(options.versions), availableVersionNames);
if (unknownVersionConfigNames.length > 0) {
throw new Error(`Invalid docs option "versions": unknown versions (${unknownVersionConfigNames.join(',')}) found. ${availableVersionNamesMsg}`);
}
if (options.onlyIncludeVersions) {
if (options.onlyIncludeVersions.length === 0) {
throw new Error(`Invalid docs option "onlyIncludeVersions": an empty array is not allowed, at least one version is needed.`);
}
const unknownOnlyIncludeVersionNames = lodash_1.default.difference(options.onlyIncludeVersions, availableVersionNames);
if (unknownOnlyIncludeVersionNames.length > 0) {
throw new Error(`Invalid docs option "onlyIncludeVersions": unknown versions (${unknownOnlyIncludeVersionNames.join(',')}) found. ${availableVersionNamesMsg}`);
}
if (options.lastVersion &&
!options.onlyIncludeVersions.includes(options.lastVersion)) {
throw new Error(`Invalid docs option "lastVersion": if you use both the "onlyIncludeVersions" and "lastVersion" options, then "lastVersion" must be present in the provided "onlyIncludeVersions" array.`);
}
}
}
exports.validateVersionsOptions = validateVersionsOptions;

View File

@@ -0,0 +1,69 @@
{
"name": "@docusaurus/plugin-content-docs",
"version": "3.1.1",
"description": "Docs plugin for Docusaurus.",
"main": "lib/index.js",
"sideEffects": false,
"exports": {
"./lib/*": "./lib/*",
"./src/*": "./src/*",
"./client": {
"type": "./lib/client/index.d.ts",
"default": "./lib/client/index.js"
},
"./server": {
"type": "./lib/server-export.d.ts",
"default": "./lib/server-export.js"
},
".": {
"types": "./src/plugin-content-docs.d.ts",
"default": "./lib/index.js"
}
},
"types": "src/plugin-content-docs.d.ts",
"scripts": {
"build": "tsc --build",
"watch": "tsc --build --watch"
},
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/facebook/docusaurus.git",
"directory": "packages/docusaurus-plugin-content-docs"
},
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.1.1",
"@docusaurus/logger": "3.1.1",
"@docusaurus/mdx-loader": "3.1.1",
"@docusaurus/module-type-aliases": "3.1.1",
"@docusaurus/types": "3.1.1",
"@docusaurus/utils": "3.1.1",
"@docusaurus/utils-validation": "3.1.1",
"@types/react-router-config": "^5.0.7",
"combine-promises": "^1.1.0",
"fs-extra": "^11.1.1",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"tslib": "^2.6.0",
"utility-types": "^3.10.0",
"webpack": "^5.88.1"
},
"devDependencies": {
"@types/js-yaml": "^4.0.5",
"@types/picomatch": "^2.3.0",
"commander": "^5.1.0",
"picomatch": "^2.3.1",
"shelljs": "^0.8.5"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"engines": {
"node": ">=18.0"
},
"gitHead": "8017f6a6776ba1bd7065e630a52fe2c2654e2f1b"
}

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {type SidebarsUtils, toNavigationLink} from './sidebars/utils';
import {createDocsByIdIndex} from './docs';
import type {
CategoryGeneratedIndexMetadata,
DocMetadataBase,
} from '@docusaurus/plugin-content-docs';
import type {SidebarItemCategoryWithGeneratedIndex} from './sidebars/types';
function getCategoryGeneratedIndexMetadata({
category,
sidebarsUtils,
docsById,
}: {
category: SidebarItemCategoryWithGeneratedIndex;
sidebarsUtils: SidebarsUtils;
docsById: {[docId: string]: DocMetadataBase};
}): CategoryGeneratedIndexMetadata {
const {sidebarName, previous, next} =
sidebarsUtils.getCategoryGeneratedIndexNavigation(category.link.permalink);
return {
title: category.link.title ?? category.label,
description: category.link.description,
image: category.link.image,
keywords: category.link.keywords,
slug: category.link.slug,
permalink: category.link.permalink,
sidebar: sidebarName!,
navigation: {
previous: toNavigationLink(previous, docsById),
next: toNavigationLink(next, docsById),
},
};
}
export function getCategoryGeneratedIndexMetadataList({
docs,
sidebarsUtils,
}: {
sidebarsUtils: SidebarsUtils;
docs: DocMetadataBase[];
}): CategoryGeneratedIndexMetadata[] {
const docsById = createDocsByIdIndex(docs);
const categoryGeneratedIndexItems =
sidebarsUtils.getCategoryGeneratedIndexList();
return categoryGeneratedIndexItems.map((category) =>
getCategoryGeneratedIndexMetadata({
category,
sidebarsUtils,
docsById,
}),
);
}

144
node_modules/@docusaurus/plugin-content-docs/src/cli.ts generated vendored Normal file
View File

@@ -0,0 +1,144 @@
/**
* 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 fs from 'fs-extra';
import path from 'path';
import logger from '@docusaurus/logger';
import {DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
import {
getVersionsFilePath,
getVersionDocsDirPath,
getVersionSidebarsPath,
getDocsDirPathLocalized,
readVersionsFile,
} from './versions/files';
import {validateVersionName} from './versions/validation';
import {loadSidebarsFile} from './sidebars';
import {CURRENT_VERSION_NAME} from './constants';
import type {PluginOptions} from '@docusaurus/plugin-content-docs';
import type {LoadContext} from '@docusaurus/types';
async function createVersionedSidebarFile({
siteDir,
pluginId,
sidebarPath,
version,
}: {
siteDir: string;
pluginId: string;
sidebarPath: string | false | undefined;
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
// We don't validate here, assuming the user has already built the version
const sidebars = await loadSidebarsFile(sidebarPath);
// 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) {
await fs.outputFile(
getVersionSidebarsPath(siteDir, pluginId, version),
`${JSON.stringify(sidebars, null, 2)}\n`,
'utf8',
);
}
}
// Tests depend on non-default export for mocking.
export async function cliDocsVersionCommand(
version: unknown,
{id: pluginId, path: docsPath, sidebarPath}: PluginOptions,
{siteDir, i18n}: LoadContext,
): Promise<void> {
// It wouldn't be very user-friendly to show a [default] log prefix,
// so we use [docs] instead of [default]
const pluginIdLogPrefix =
pluginId === DEFAULT_PLUGIN_ID ? '[docs]' : `[${pluginId}]`;
try {
validateVersionName(version);
} catch (err) {
logger.info`${pluginIdLogPrefix}: Invalid version name provided. Try something like: 1.0.0`;
throw err;
}
const versions = (await readVersionsFile(siteDir, pluginId)) ?? [];
// Check if version already exists.
if (versions.includes(version)) {
throw new Error(
`${pluginIdLogPrefix}: this version already exists! Use a version tag that does not already exist.`,
);
}
if (i18n.locales.length > 1) {
logger.info`Versioned docs will be created for the following locales: name=${i18n.locales}`;
}
await Promise.all(
i18n.locales.map(async (locale) => {
const localizationDir = path.resolve(
siteDir,
i18n.path,
i18n.localeConfigs[locale]!.path,
);
// Copy docs files.
const docsDir =
locale === i18n.defaultLocale
? path.resolve(siteDir, docsPath)
: getDocsDirPathLocalized({
localizationDir,
pluginId,
versionName: CURRENT_VERSION_NAME,
});
if (
!(await fs.pathExists(docsDir)) ||
(await fs.readdir(docsDir)).length === 0
) {
if (locale === i18n.defaultLocale) {
throw new Error(
logger.interpolate`${pluginIdLogPrefix}: no docs found in path=${docsDir}.`,
);
} else {
logger.warn`${pluginIdLogPrefix}: no docs found in path=${docsDir}. Skipping.`;
return;
}
}
const newVersionDir =
locale === i18n.defaultLocale
? getVersionDocsDirPath(siteDir, pluginId, version)
: getDocsDirPathLocalized({
localizationDir,
pluginId,
versionName: version,
});
await fs.copy(docsDir, newVersionDir);
}),
);
await createVersionedSidebarFile({
siteDir,
pluginId,
version,
sidebarPath,
});
// Update versions.json file.
versions.unshift(version);
await fs.outputFile(
getVersionsFilePath(siteDir, pluginId),
`${JSON.stringify(versions, null, 2)}\n`,
);
logger.success`name=${pluginIdLogPrefix}: version name=${version} created!`;
}

View File

@@ -0,0 +1,131 @@
/**
* 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 {matchPath} from '@docusaurus/router';
import type {
GlobalPluginData,
GlobalVersion,
GlobalDoc,
ActivePlugin,
ActiveDocContext,
DocVersionSuggestions,
} from '@docusaurus/plugin-content-docs/client';
import type {UseDataOptions} from '@docusaurus/types';
// This code is not part of the api surface, not in ./theme on purpose
// get the data of the plugin that is currently "active"
// ie the docs of that plugin are currently browsed
// it is useful to support multiple docs plugin instances
export function getActivePlugin(
allPluginData: {[pluginId: string]: GlobalPluginData},
pathname: string,
options: UseDataOptions = {},
): ActivePlugin | undefined {
const activeEntry = Object.entries(allPluginData)
// Route sorting: '/android/foo' should match '/android' instead of '/'
.sort((a, b) => b[1].path.localeCompare(a[1].path))
.find(
([, pluginData]) =>
!!matchPath(pathname, {
path: pluginData.path,
exact: false,
strict: false,
}),
);
const activePlugin: ActivePlugin | undefined = activeEntry
? {pluginId: activeEntry[0], pluginData: activeEntry[1]}
: undefined;
if (!activePlugin && options.failfast) {
throw new Error(
`Can't find active docs plugin for "${pathname}" pathname, while it was expected to be found. Maybe you tried to use a docs feature that can only be used on a docs-related page? Existing docs plugin paths are: ${Object.values(
allPluginData,
)
.map((plugin) => plugin.path)
.join(', ')}`,
);
}
return activePlugin;
}
export const getLatestVersion = (data: GlobalPluginData): GlobalVersion =>
data.versions.find((version) => version.isLast)!;
export function getActiveVersion(
data: GlobalPluginData,
pathname: string,
): GlobalVersion | undefined {
const lastVersion = getLatestVersion(data);
// Last version is a route like /docs/*,
// 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,
];
return orderedVersionsMetadata.find(
(version) =>
!!matchPath(pathname, {
path: version.path,
exact: false,
strict: false,
}),
);
}
export function getActiveDocContext(
data: GlobalPluginData,
pathname: string,
): ActiveDocContext {
const activeVersion = getActiveVersion(data, pathname);
const activeDoc = activeVersion?.docs.find(
(doc) =>
!!matchPath(pathname, {
path: doc.path,
exact: true,
strict: false,
}),
);
function getAlternateVersionDocs(
docId: string,
): ActiveDocContext['alternateDocVersions'] {
const result: ActiveDocContext['alternateDocVersions'] = {};
data.versions.forEach((version) => {
version.docs.forEach((doc) => {
if (doc.id === docId) {
result[version.name] = doc;
}
});
});
return result;
}
const alternateVersionDocs = activeDoc
? getAlternateVersionDocs(activeDoc.id)
: {};
return {
activeVersion,
activeDoc,
alternateDocVersions: alternateVersionDocs,
};
}
export function getDocVersionSuggestions(
data: GlobalPluginData,
pathname: string,
): DocVersionSuggestions {
const latestVersion = getLatestVersion(data);
const activeDocContext = getActiveDocContext(data, pathname);
const latestDocSuggestion: GlobalDoc | undefined =
activeDocContext.alternateDocVersions[latestVersion.name];
return {latestDocSuggestion, latestVersionSuggestion: latestVersion};
}

View File

@@ -0,0 +1,159 @@
/**
* 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 {useLocation} from '@docusaurus/router';
import {
useAllPluginInstancesData,
usePluginData,
} from '@docusaurus/useGlobalData';
import {
getActivePlugin,
getLatestVersion,
getActiveVersion,
getActiveDocContext,
getDocVersionSuggestions,
} from './docsClientUtils';
import type {UseDataOptions} from '@docusaurus/types';
export type ActivePlugin = {
pluginId: string;
pluginData: GlobalPluginData;
};
export type ActiveDocContext = {
activeVersion?: GlobalVersion;
activeDoc?: GlobalDoc;
alternateDocVersions: {[versionName: string]: GlobalDoc};
};
export type GlobalDoc = {
/**
* For generated index pages, this is the `slug`, **not** `permalink`
* (without base URL). Because slugs have leading slashes but IDs don't,
* there won't be clashes.
*/
id: string;
path: string;
sidebar?: string;
unlisted?: boolean;
};
export type GlobalVersion = {
name: string;
label: string;
isLast: boolean;
path: string;
/** The doc with `slug: /`, or first doc in first sidebar */
mainDocId: string;
docs: GlobalDoc[];
/** Unversioned IDs. In development, this list is empty. */
draftIds: string[];
sidebars?: {[sidebarId: string]: GlobalSidebar};
};
export type GlobalSidebar = {
link?: {
label: string;
path: string;
};
// ... we may add other things here later
};
export type GlobalPluginData = {
path: string;
versions: GlobalVersion[];
breadcrumbs: boolean;
};
export type DocVersionSuggestions = {
/** Suggest the latest version */
latestVersionSuggestion: GlobalVersion;
/** Suggest the same doc, in latest version (if one exists) */
latestDocSuggestion?: GlobalDoc;
};
// Important to use a constant object to avoid React useEffect executions etc.
// see https://github.com/facebook/docusaurus/issues/5089
const StableEmptyObject = {};
// 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 = (): {[pluginId: string]: GlobalPluginData} =>
(useAllPluginInstancesData('docusaurus-plugin-content-docs') as
| {
[pluginId: string]: GlobalPluginData;
}
| undefined) ?? StableEmptyObject;
export const useDocsData = (pluginId: string | undefined): GlobalPluginData =>
usePluginData('docusaurus-plugin-content-docs', pluginId, {
failfast: true,
}) as GlobalPluginData;
// TODO this feature should be provided by docusaurus core
export function useActivePlugin(
options: UseDataOptions = {},
): ActivePlugin | undefined {
const data = useAllDocsData();
const {pathname} = useLocation();
return getActivePlugin(data, pathname, options);
}
export function useActivePluginAndVersion(
options: UseDataOptions = {},
):
| {activePlugin: ActivePlugin; activeVersion: GlobalVersion | undefined}
| undefined {
const activePlugin = useActivePlugin(options);
const {pathname} = useLocation();
if (!activePlugin) {
return undefined;
}
const activeVersion = getActiveVersion(activePlugin.pluginData, pathname);
return {
activePlugin,
activeVersion,
};
}
/** Versions are returned ordered (most recent first). */
export function useVersions(pluginId: string | undefined): GlobalVersion[] {
const data = useDocsData(pluginId);
return data.versions;
}
export function useLatestVersion(pluginId: string | undefined): GlobalVersion {
const data = useDocsData(pluginId);
return getLatestVersion(data);
}
/**
* Returns `undefined` on doc-unrelated pages, because there's no version
* currently considered as active.
*/
export function useActiveVersion(
pluginId: string | undefined,
): GlobalVersion | undefined {
const data = useDocsData(pluginId);
const {pathname} = useLocation();
return getActiveVersion(data, pathname);
}
export function useActiveDocContext(
pluginId: string | undefined,
): ActiveDocContext {
const data = useDocsData(pluginId);
const {pathname} = useLocation();
return getActiveDocContext(data, pathname);
}
/**
* Useful to say "hey, you are not on the latest docs version, please switch"
*/
export function useDocVersionSuggestions(
pluginId: string | undefined,
): DocVersionSuggestions {
const data = useDocsData(pluginId);
const {pathname} = useLocation();
return getDocVersionSuggestions(data, pathname);
}

View File

@@ -0,0 +1,15 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** The name of the version that's actively worked on (e.g. `website/docs`) */
export const CURRENT_VERSION_NAME = 'current';
/** All doc versions are stored here by version names */
export const VERSIONED_DOCS_DIR = 'versioned_docs';
/** All doc versioned sidebars are stored here by version names */
export const VERSIONED_SIDEBARS_DIR = 'versioned_sidebars';
/** The version names. Should 1-1 map to the content of versioned docs dir. */
export const VERSIONS_JSON_FILE = 'versions.json';

View File

@@ -0,0 +1,459 @@
/**
* 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 path from 'path';
import fs from 'fs-extra';
import _ from 'lodash';
import logger from '@docusaurus/logger';
import {
aliasedSitePath,
getEditUrl,
getFolderContainingFile,
getContentPathList,
normalizeUrl,
parseMarkdownFile,
posixPath,
Globby,
normalizeFrontMatterTags,
isUnlisted,
isDraft,
} from '@docusaurus/utils';
import {getFileLastUpdate} from './lastUpdate';
import getSlug from './slug';
import {stripPathNumberPrefixes} from './numberPrefix';
import {validateDocFrontMatter} from './frontMatter';
import {toDocNavigationLink, toNavigationLink} from './sidebars/utils';
import type {
MetadataOptions,
PluginOptions,
CategoryIndexMatcher,
DocMetadataBase,
DocMetadata,
PropNavigationLink,
LastUpdateData,
VersionMetadata,
LoadedVersion,
FileChange,
} from '@docusaurus/plugin-content-docs';
import type {LoadContext} from '@docusaurus/types';
import type {SidebarsUtils} from './sidebars/utils';
import type {DocFile} from './types';
type LastUpdateOptions = Pick<
PluginOptions,
'showLastUpdateAuthor' | 'showLastUpdateTime'
>;
async function readLastUpdateData(
filePath: string,
options: LastUpdateOptions,
lastUpdateFrontMatter: FileChange | undefined,
): Promise<LastUpdateData> {
const {showLastUpdateAuthor, showLastUpdateTime} = options;
if (showLastUpdateAuthor || showLastUpdateTime) {
const frontMatterTimestamp = lastUpdateFrontMatter?.date
? new Date(lastUpdateFrontMatter.date).getTime() / 1000
: undefined;
if (lastUpdateFrontMatter?.author && lastUpdateFrontMatter.date) {
return {
lastUpdatedAt: frontMatterTimestamp,
lastUpdatedBy: lastUpdateFrontMatter.author,
};
}
// Use fake data in dev for faster development.
const fileLastUpdateData =
process.env.NODE_ENV === 'production'
? await getFileLastUpdate(filePath)
: {
author: 'Author',
timestamp: 1539502055,
};
const {author, timestamp} = fileLastUpdateData ?? {};
return {
lastUpdatedBy: showLastUpdateAuthor
? lastUpdateFrontMatter?.author ?? author
: undefined,
lastUpdatedAt: showLastUpdateTime
? frontMatterTimestamp ?? timestamp
: undefined,
};
}
return {};
}
export async function readDocFile(
versionMetadata: Pick<
VersionMetadata,
'contentPath' | 'contentPathLocalized'
>,
source: string,
): Promise<DocFile> {
const contentPath = await getFolderContainingFile(
getContentPathList(versionMetadata),
source,
);
const filePath = path.join(contentPath, source);
const content = await fs.readFile(filePath, 'utf-8');
return {source, content, contentPath, filePath};
}
export async function readVersionDocs(
versionMetadata: VersionMetadata,
options: Pick<
PluginOptions,
'include' | 'exclude' | 'showLastUpdateAuthor' | 'showLastUpdateTime'
>,
): Promise<DocFile[]> {
const sources = await Globby(options.include, {
cwd: versionMetadata.contentPath,
ignore: options.exclude,
});
return Promise.all(
sources.map((source) => readDocFile(versionMetadata, source)),
);
}
export type DocEnv = 'production' | 'development';
async function doProcessDocMetadata({
docFile,
versionMetadata,
context,
options,
env,
}: {
docFile: DocFile;
versionMetadata: VersionMetadata;
context: LoadContext;
options: MetadataOptions;
env: DocEnv;
}): Promise<DocMetadataBase> {
const {source, content, contentPath, filePath} = docFile;
const {
siteDir,
i18n,
siteConfig: {
markdown: {parseFrontMatter},
},
} = context;
const {
frontMatter: unsafeFrontMatter,
contentTitle,
excerpt,
} = await parseMarkdownFile({
filePath,
fileContent: content,
parseFrontMatter,
});
const frontMatter = validateDocFrontMatter(unsafeFrontMatter);
const {
custom_edit_url: customEditURL,
// 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,
last_update: lastUpdateFrontMatter,
} = frontMatter;
const lastUpdate = await readLastUpdateData(
filePath,
options,
lastUpdateFrontMatter,
);
// E.g. api/plugins/myDoc -> myDoc; myDoc -> myDoc
const sourceFileNameWithoutExtension = path.basename(
source,
path.extname(source),
);
// E.g. api/plugins/myDoc -> api/plugins; myDoc -> .
const sourceDirName = path.dirname(source);
const {filename: unprefixedFileName, numberPrefix} = parseNumberPrefixes
? options.numberPrefixParser(sourceFileNameWithoutExtension)
: {filename: sourceFileNameWithoutExtension, numberPrefix: undefined};
const baseID: string = frontMatter.id ?? unprefixedFileName;
if (baseID.includes('/')) {
throw new Error(`Document id "${baseID}" cannot include slash.`);
}
// For autogenerated sidebars, sidebar position can come from filename number
// prefix or front matter
const sidebarPosition: number | undefined =
frontMatter.sidebar_position ?? numberPrefix;
// TODO legacy retrocompatibility
// I think it's bad to affect the front matter id with the dirname?
function computeDirNameIdPrefix() {
if (sourceDirName === '.') {
return undefined;
}
// Eventually remove the number prefixes from intermediate directories
return parseNumberPrefixes
? stripPathNumberPrefixes(sourceDirName, options.numberPrefixParser)
: sourceDirName;
}
const id = [computeDirNameIdPrefix(), baseID].filter(Boolean).join('/');
const docSlug = getSlug({
baseID,
source,
sourceDirName,
frontMatterSlug: frontMatter.slug,
stripDirNumberPrefixes: parseNumberPrefixes,
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)
const title: string = frontMatter.title ?? contentTitle ?? baseID;
const description: string = frontMatter.description ?? excerpt ?? '';
const permalink = normalizeUrl([versionMetadata.path, docSlug]);
function getDocEditUrl() {
const relativeFilePath = path.relative(contentPath, filePath);
if (typeof options.editUrl === 'function') {
return options.editUrl({
version: versionMetadata.versionName,
versionDocsDirPath: posixPath(
path.relative(siteDir, versionMetadata.contentPath),
),
docPath: posixPath(relativeFilePath),
permalink,
locale: context.i18n.currentLocale,
});
} else if (typeof options.editUrl === 'string') {
const isLocalized = contentPath === versionMetadata.contentPathLocalized;
const baseVersionEditUrl =
isLocalized && options.editLocalizedFiles
? versionMetadata.editUrlLocalized
: versionMetadata.editUrl;
return getEditUrl(relativeFilePath, baseVersionEditUrl);
}
return undefined;
}
const draft = isDraft({env, frontMatter});
const unlisted = isUnlisted({env, frontMatter});
const formatDate = (locale: string, date: Date, calendar: string): string => {
try {
return new Intl.DateTimeFormat(locale, {
day: 'numeric',
month: 'short',
year: 'numeric',
timeZone: 'UTC',
calendar,
}).format(date);
} catch (err) {
logger.error`Can't format docs lastUpdatedAt date "${String(date)}"`;
throw err;
}
};
// Assign all of object properties during instantiation (if possible) for
// NodeJS optimization.
// Adding properties to object after instantiation will cause hidden
// class transitions.
return {
id,
title,
description,
source: aliasedSitePath(filePath, siteDir),
sourceDirName,
slug: docSlug,
permalink,
draft,
unlisted,
editUrl: customEditURL !== undefined ? customEditURL : getDocEditUrl(),
tags: normalizeFrontMatterTags(versionMetadata.tagsPath, frontMatter.tags),
version: versionMetadata.versionName,
lastUpdatedBy: lastUpdate.lastUpdatedBy,
lastUpdatedAt: lastUpdate.lastUpdatedAt,
formattedLastUpdatedAt: lastUpdate.lastUpdatedAt
? formatDate(
i18n.currentLocale,
new Date(lastUpdate.lastUpdatedAt * 1000),
i18n.localeConfigs[i18n.currentLocale]!.calendar,
)
: undefined,
sidebarPosition,
frontMatter,
};
}
export async function processDocMetadata(args: {
docFile: DocFile;
versionMetadata: VersionMetadata;
context: LoadContext;
options: MetadataOptions;
env: DocEnv;
}): Promise<DocMetadataBase> {
try {
return await doProcessDocMetadata(args);
} catch (err) {
throw new Error(
`Can't process doc metadata for doc at path path=${args.docFile.filePath} in version name=${args.versionMetadata.versionName}`,
{cause: err as Error},
);
}
}
function getUnlistedIds(docs: DocMetadataBase[]): Set<string> {
return new Set(docs.filter((doc) => doc.unlisted).map((doc) => doc.id));
}
export function addDocNavigation({
docs,
sidebarsUtils,
}: {
docs: DocMetadataBase[];
sidebarsUtils: SidebarsUtils;
}): LoadedVersion['docs'] {
const docsById = createDocsByIdIndex(docs);
const unlistedIds = getUnlistedIds(docs);
// Add sidebar/next/previous to the docs
function addNavData(doc: DocMetadataBase): DocMetadata {
const navigation = sidebarsUtils.getDocNavigation({
docId: doc.id,
displayedSidebar: doc.frontMatter.displayed_sidebar,
unlistedIds,
});
const toNavigationLinkByDocId = (
docId: string | null | undefined,
type: 'prev' | 'next',
): PropNavigationLink | undefined => {
if (!docId) {
return undefined;
}
const navDoc = docsById[docId];
if (!navDoc) {
// This could only happen if user provided the ID through front matter
throw new Error(
`Error when loading ${doc.id} in ${doc.sourceDirName}: the pagination_${type} front matter points to a non-existent ID ${docId}.`,
);
}
// Gracefully handle explicitly providing an unlisted doc ID in production
if (navDoc.unlisted) {
return undefined;
}
return toDocNavigationLink(navDoc);
};
const previous =
doc.frontMatter.pagination_prev !== undefined
? toNavigationLinkByDocId(doc.frontMatter.pagination_prev, 'prev')
: toNavigationLink(navigation.previous, docsById);
const next =
doc.frontMatter.pagination_next !== undefined
? toNavigationLinkByDocId(doc.frontMatter.pagination_next, 'next')
: toNavigationLink(navigation.next, docsById);
return {...doc, sidebar: navigation.sidebarName, previous, next};
}
const docsWithNavigation = docs.map(addNavData);
// Sort to ensure consistent output for tests
docsWithNavigation.sort((a, b) => a.id.localeCompare(b.id));
return docsWithNavigation;
}
/**
* The "main doc" is the "version entry point"
* We browse this doc by clicking on a version:
* - the "home" doc (at '/docs/')
* - the first doc of the first sidebar
* - a random doc (if no docs are in any sidebar... edge case)
*/
export function getMainDocId({
docs,
sidebarsUtils,
}: {
docs: DocMetadataBase[];
sidebarsUtils: SidebarsUtils;
}): string {
function getMainDoc(): DocMetadata {
const versionHomeDoc = docs.find((doc) => doc.slug === '/');
const firstDocIdOfFirstSidebar =
sidebarsUtils.getFirstDocIdOfFirstSidebar();
if (versionHomeDoc) {
return versionHomeDoc;
} else if (firstDocIdOfFirstSidebar) {
return docs.find((doc) => doc.id === firstDocIdOfFirstSidebar)!;
}
return docs[0]!;
}
return getMainDoc().id;
}
// By convention, Docusaurus considers some docs are "indexes":
// - index.md
// - readme.md
// - <folder>/<folder>.md
//
// This function is the default implementation of this convention
//
// Those index docs produce a different behavior
// - Slugs do not end with a weird "/index" suffix
// - Auto-generated sidebar categories link to them as intro
export const isCategoryIndex: CategoryIndexMatcher = ({
fileName,
directories,
}): boolean => {
const eligibleDocIndexNames = [
'index',
'readme',
directories[0]?.toLowerCase(),
];
return eligibleDocIndexNames.includes(fileName.toLowerCase());
};
/**
* `guides/sidebar/autogenerated.md` ->
* `'autogenerated', '.md', ['sidebar', 'guides']`
*/
export function toCategoryIndexMatcherParam({
source,
sourceDirName,
}: Pick<
DocMetadataBase,
'source' | 'sourceDirName'
>): Parameters<CategoryIndexMatcher>[0] {
// 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(),
};
}
// Docs are indexed by their id
export function createDocsByIdIndex<Doc extends {id: string}>(
docs: Doc[],
): {[docId: string]: Doc} {
return _.keyBy(docs, (d) => d.id);
}

View File

@@ -0,0 +1,65 @@
/**
* 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 {
JoiFrontMatter as Joi, // Custom instance for front matter
URISchema,
FrontMatterTagsSchema,
FrontMatterTOCHeadingLevels,
validateFrontMatter,
ContentVisibilitySchema,
} from '@docusaurus/utils-validation';
import type {DocFrontMatter} from '@docusaurus/plugin-content-docs';
const FrontMatterLastUpdateErrorMessage =
'{{#label}} does not look like a valid front matter FileChange object. Please use a FileChange object (with an author and/or date).';
// NOTE: we don't add any default value on purpose here
// We don't want default values to magically appear in doc metadata and props
// While the user did not provide those values explicitly
// We use default values in code instead
const DocFrontMatterSchema = Joi.object<DocFrontMatter>({
id: Joi.string(),
// See https://github.com/facebook/docusaurus/issues/4591#issuecomment-822372398
title: Joi.string().allow(''),
hide_title: Joi.boolean(),
hide_table_of_contents: Joi.boolean(),
keywords: Joi.array().items(Joi.string().required()),
image: URISchema,
// See https://github.com/facebook/docusaurus/issues/4591#issuecomment-822372398
description: Joi.string().allow(''),
slug: Joi.string(),
sidebar_label: Joi.string(),
sidebar_position: Joi.number(),
sidebar_class_name: Joi.string(),
sidebar_custom_props: Joi.object().unknown(),
displayed_sidebar: Joi.string().allow(null),
tags: FrontMatterTagsSchema,
pagination_label: Joi.string(),
custom_edit_url: URISchema.allow('', null),
parse_number_prefixes: Joi.boolean(),
pagination_next: Joi.string().allow(null),
pagination_prev: Joi.string().allow(null),
...FrontMatterTOCHeadingLevels,
last_update: Joi.object({
author: Joi.string(),
date: Joi.date().raw(),
})
.or('author', 'date')
.messages({
'object.missing': FrontMatterLastUpdateErrorMessage,
'object.base': FrontMatterLastUpdateErrorMessage,
}),
})
.unknown()
.concat(ContentVisibilitySchema);
export function validateDocFrontMatter(frontMatter: {
[key: string]: unknown;
}): DocFrontMatter {
return validateFrontMatter(frontMatter, DocFrontMatterSchema);
}

View File

@@ -0,0 +1,79 @@
/**
* 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 _ from 'lodash';
import {getMainDocId} from './docs';
import type {FullVersion} from './types';
import type {
CategoryGeneratedIndexMetadata,
DocMetadata,
} from '@docusaurus/plugin-content-docs';
import type {
GlobalVersion,
GlobalSidebar,
GlobalDoc,
} from '@docusaurus/plugin-content-docs/client';
import type {Sidebars} from './sidebars/types';
function toGlobalDataDoc(doc: DocMetadata): GlobalDoc {
return {
id: doc.id,
path: doc.permalink,
// optimize global data size: do not add unlisted: false/undefined
...(doc.unlisted && {unlisted: doc.unlisted}),
// TODO optimize size? remove attribute when no sidebar (breaking change?)
sidebar: doc.sidebar,
};
}
function toGlobalDataGeneratedIndex(
doc: CategoryGeneratedIndexMetadata,
): GlobalDoc {
return {
id: doc.slug,
path: doc.permalink,
sidebar: doc.sidebar,
};
}
function toGlobalSidebars(
sidebars: Sidebars,
version: FullVersion,
): {[sidebarId: string]: GlobalSidebar} {
return _.mapValues(sidebars, (sidebar, sidebarId) => {
const firstLink = version.sidebarsUtils.getFirstLink(sidebarId);
if (!firstLink) {
return {};
}
return {
link: {
path:
firstLink.type === 'generated-index'
? firstLink.permalink
: version.docs.find((doc) => doc.id === firstLink.id)!.permalink,
label: firstLink.label,
},
};
});
}
export function toGlobalDataVersion(version: FullVersion): GlobalVersion {
return {
name: version.versionName,
label: version.label,
isLast: version.isLast,
path: version.path,
mainDocId: getMainDocId(version),
docs: version.docs
.map(toGlobalDataDoc)
.concat(version.categoryGeneratedIndices.map(toGlobalDataGeneratedIndex)),
draftIds: version.drafts.map((doc) => doc.id),
sidebars: toGlobalSidebars(version.sidebars, version),
};
}

View File

@@ -0,0 +1,340 @@
/**
* 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 path from 'path';
import _ from 'lodash';
import logger from '@docusaurus/logger';
import {
normalizeUrl,
docuHash,
aliasedSitePath,
getContentPathList,
posixPath,
addTrailingPathSeparator,
createAbsoluteFilePathMatcher,
createSlugger,
DEFAULT_PLUGIN_ID,
} from '@docusaurus/utils';
import {loadSidebars, resolveSidebarPathOption} from './sidebars';
import {CategoryMetadataFilenamePattern} from './sidebars/generator';
import {
readVersionDocs,
processDocMetadata,
addDocNavigation,
type DocEnv,
createDocsByIdIndex,
} from './docs';
import {readVersionsMetadata, toFullVersion} from './versions';
import {cliDocsVersionCommand} from './cli';
import {VERSIONS_JSON_FILE} from './constants';
import {toGlobalDataVersion} from './globalData';
import {
translateLoadedContent,
getLoadedContentTranslationFiles,
} from './translations';
import {createAllRoutes} from './routes';
import {createSidebarsUtils} from './sidebars/utils';
import type {
PluginOptions,
DocMetadataBase,
VersionMetadata,
DocFrontMatter,
LoadedContent,
LoadedVersion,
} from '@docusaurus/plugin-content-docs';
import type {LoadContext, Plugin} from '@docusaurus/types';
import type {
SourceToPermalink,
DocFile,
DocsMarkdownOption,
FullVersion,
} from './types';
import type {RuleSetRule} from 'webpack';
export default async function pluginContentDocs(
context: LoadContext,
options: PluginOptions,
): Promise<Plugin<LoadedContent>> {
const {siteDir, generatedFilesDir, baseUrl, siteConfig} = context;
// Mutate options to resolve sidebar path according to siteDir
options.sidebarPath = resolveSidebarPathOption(siteDir, options.sidebarPath);
const versionsMetadata = await readVersionsMetadata({context, options});
const pluginId = options.id;
const pluginDataDirRoot = path.join(
generatedFilesDir,
'docusaurus-plugin-content-docs',
);
const dataDir = path.join(pluginDataDirRoot, pluginId);
const aliasedSource = (source: string) =>
`~docs/${posixPath(path.relative(pluginDataDirRoot, source))}`;
// TODO env should be injected into all plugins
const env = process.env.NODE_ENV as DocEnv;
return {
name: 'docusaurus-plugin-content-docs',
extendCli(cli) {
const isDefaultPluginId = pluginId === DEFAULT_PLUGIN_ID;
// Need to create one distinct command per plugin instance
// otherwise 2 instances would try to execute the command!
const command = isDefaultPluginId
? 'docs:version'
: `docs:version:${pluginId}`;
const commandDescription = isDefaultPluginId
? 'Tag a new docs version'
: `Tag a new docs version (${pluginId})`;
cli
.command(command)
.arguments('<version>')
.description(commandDescription)
.action((version: unknown) =>
cliDocsVersionCommand(version, options, context),
);
},
getTranslationFiles({content}) {
return getLoadedContentTranslationFiles(content);
},
getPathsToWatch() {
function getVersionPathsToWatch(version: VersionMetadata): string[] {
const result = [
...options.include.flatMap((pattern) =>
getContentPathList(version).map(
(docsDirPath) => `${docsDirPath}/${pattern}`,
),
),
`${version.contentPath}/**/${CategoryMetadataFilenamePattern}`,
];
if (typeof version.sidebarFilePath === 'string') {
result.unshift(version.sidebarFilePath);
}
return result;
}
return versionsMetadata.flatMap(getVersionPathsToWatch);
},
async loadContent() {
async function loadVersionDocsBase(
versionMetadata: VersionMetadata,
): Promise<DocMetadataBase[]> {
const docFiles = await readVersionDocs(versionMetadata, options);
if (docFiles.length === 0) {
throw new Error(
`Docs version "${
versionMetadata.versionName
}" has no docs! At least one doc should exist at "${path.relative(
siteDir,
versionMetadata.contentPath,
)}".`,
);
}
function processVersionDoc(docFile: DocFile) {
return processDocMetadata({
docFile,
versionMetadata,
context,
options,
env,
});
}
return Promise.all(docFiles.map(processVersionDoc));
}
async function doLoadVersion(
versionMetadata: VersionMetadata,
): Promise<LoadedVersion> {
const docsBase: DocMetadataBase[] = await loadVersionDocsBase(
versionMetadata,
);
// TODO we only ever need draftIds in further code, not full draft items
// To simplify and prevent mistakes, avoid exposing draft
// replace draft=>draftIds in content loaded
const [drafts, docs] = _.partition(docsBase, (doc) => doc.draft);
const sidebars = await loadSidebars(versionMetadata.sidebarFilePath, {
sidebarItemsGenerator: options.sidebarItemsGenerator,
numberPrefixParser: options.numberPrefixParser,
docs,
drafts,
version: versionMetadata,
sidebarOptions: {
sidebarCollapsed: options.sidebarCollapsed,
sidebarCollapsible: options.sidebarCollapsible,
},
categoryLabelSlugger: createSlugger(),
});
const sidebarsUtils = createSidebarsUtils(sidebars);
const docsById = createDocsByIdIndex(docs);
const allDocIds = Object.keys(docsById);
sidebarsUtils.checkLegacyVersionedSidebarNames({
sidebarFilePath: versionMetadata.sidebarFilePath as string,
versionMetadata,
});
sidebarsUtils.checkSidebarsDocIds({
allDocIds,
sidebarFilePath: versionMetadata.sidebarFilePath as string,
versionMetadata,
});
return {
...versionMetadata,
docs: addDocNavigation({
docs,
sidebarsUtils,
}),
drafts,
sidebars,
};
}
async function loadVersion(versionMetadata: VersionMetadata) {
try {
return await doLoadVersion(versionMetadata);
} catch (err) {
logger.error`Loading of version failed for version name=${versionMetadata.versionName}`;
throw err;
}
}
return {
loadedVersions: await Promise.all(versionsMetadata.map(loadVersion)),
};
},
translateContent({content, translationFiles}) {
return translateLoadedContent(content, translationFiles);
},
async contentLoaded({content, actions}) {
const versions: FullVersion[] = content.loadedVersions.map(toFullVersion);
await createAllRoutes({
baseUrl,
versions,
options,
actions,
aliasedSource,
});
actions.setGlobalData({
path: normalizeUrl([baseUrl, options.routeBasePath]),
versions: versions.map(toGlobalDataVersion),
breadcrumbs: options.breadcrumbs,
});
},
configureWebpack(_config, isServer, utils, content) {
const {
rehypePlugins,
remarkPlugins,
beforeDefaultRehypePlugins,
beforeDefaultRemarkPlugins,
} = options;
function getSourceToPermalink(): SourceToPermalink {
const allDocs = content.loadedVersions.flatMap((v) => v.docs);
return Object.fromEntries(
allDocs.map(({source, permalink}) => [source, permalink]),
);
}
const docsMarkdownOptions: DocsMarkdownOption = {
siteDir,
sourceToPermalink: getSourceToPermalink(),
versionsMetadata,
onBrokenMarkdownLink: (brokenMarkdownLink) => {
logger.report(
siteConfig.onBrokenMarkdownLinks,
)`Docs markdown link couldn't be resolved: (url=${brokenMarkdownLink.link}) in path=${brokenMarkdownLink.filePath} for version number=${brokenMarkdownLink.contentPaths.versionName}`;
},
};
function createMDXLoaderRule(): RuleSetRule {
const contentDirs = versionsMetadata
.flatMap(getContentPathList)
// Trailing slash is important, see https://github.com/facebook/docusaurus/pull/3970
.map(addTrailingPathSeparator);
return {
test: /\.mdx?$/i,
include: contentDirs,
use: [
{
loader: require.resolve('@docusaurus/mdx-loader'),
options: {
admonitions: options.admonitions,
remarkPlugins,
rehypePlugins,
beforeDefaultRehypePlugins,
beforeDefaultRemarkPlugins,
staticDirs: siteConfig.staticDirectories.map((dir) =>
path.resolve(siteDir, dir),
),
siteDir,
isMDXPartial: createAbsoluteFilePathMatcher(
options.exclude,
contentDirs,
),
metadataPath: (mdxPath: string) => {
// Note that metadataPath must be the same/in-sync as
// the path from createData for each MDX.
const aliasedPath = aliasedSitePath(mdxPath, siteDir);
return path.join(dataDir, `${docuHash(aliasedPath)}.json`);
},
// Assets allow to convert some relative images paths to
// require(...) calls
createAssets: ({
frontMatter,
}: {
frontMatter: DocFrontMatter;
}) => ({
image: frontMatter.image,
}),
markdownConfig: siteConfig.markdown,
},
},
{
loader: path.resolve(__dirname, './markdown/index.js'),
options: docsMarkdownOptions,
},
].filter(Boolean),
};
}
return {
ignoreWarnings: [
// Suppress warnings about non-existing of versions file.
(e) =>
e.message.includes("Can't resolve") &&
e.message.includes(VERSIONS_JSON_FILE),
],
resolve: {
alias: {
'~docs': pluginDataDirRoot,
},
},
module: {
rules: [createMDXLoaderRule()],
},
};
},
};
}
export {validateOptions} from './options';

View File

@@ -0,0 +1,51 @@
/**
* 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 logger from '@docusaurus/logger';
import {
getFileCommitDate,
FileNotTrackedError,
GitNotFoundError,
} from '@docusaurus/utils';
let showedGitRequirementError = false;
let showedFileNotTrackedError = false;
export async function getFileLastUpdate(
filePath: string,
): Promise<{timestamp: number; author: string} | null> {
if (!filePath) {
return null;
}
// Wrap in try/catch in case the shell commands fail
// (e.g. project doesn't use Git, etc).
try {
const result = getFileCommitDate(filePath, {
age: 'newest',
includeAuthor: true,
});
return {timestamp: result.timestamp, author: result.author};
} catch (err) {
if (err instanceof GitNotFoundError) {
if (!showedGitRequirementError) {
logger.warn('Sorry, the docs plugin last update options require Git.');
showedGitRequirementError = true;
}
} else if (err instanceof FileNotTrackedError) {
if (!showedFileNotTrackedError) {
logger.warn(
'Cannot infer the update date for some files, as they are not tracked by git.',
);
showedFileNotTrackedError = true;
}
} else {
logger.warn(err);
}
return null;
}
}

View File

@@ -0,0 +1,20 @@
/**
* 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 {linkify} from './linkify';
import type {DocsMarkdownOption} from '../types';
import type {LoaderContext} from 'webpack';
export default function markdownLoader(
this: LoaderContext<DocsMarkdownOption>,
source: string,
): void {
const fileString = source;
const callback = this.async();
const options = this.getOptions();
return callback(null, linkify(fileString, this.resourcePath, options));
}

View File

@@ -0,0 +1,47 @@
/**
* 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 {replaceMarkdownLinks, getContentPathList} from '@docusaurus/utils';
import type {DocsMarkdownOption} from '../types';
function getVersion(filePath: string, options: DocsMarkdownOption) {
const versionFound = options.versionsMetadata.find((version) =>
getContentPathList(version).some((docsDirPath) =>
filePath.startsWith(docsDirPath),
),
);
// At this point, this should never happen, because the MDX loaders' paths are
// literally using the version content paths; but if we allow sourcing content
// from outside the docs directory (through the `include` option, for example;
// is there a compelling use-case?), this would actually be testable
if (!versionFound) {
throw new Error(
`Unexpected error: Markdown file at "${filePath}" does not belong to any docs version!`,
);
}
return versionFound;
}
export function linkify(
fileString: string,
filePath: string,
options: DocsMarkdownOption,
): string {
const {siteDir, sourceToPermalink, onBrokenMarkdownLink} = options;
const {newContent, brokenMarkdownLinks} = replaceMarkdownLinks({
siteDir,
fileString,
filePath,
contentPaths: getVersion(filePath, options),
sourceToPermalink,
});
brokenMarkdownLinks.forEach((l) => onBrokenMarkdownLink(l));
return newContent;
}

View File

@@ -0,0 +1,62 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type {NumberPrefixParser} from '@docusaurus/plugin-content-docs';
// Best-effort to avoid parsing some patterns as number prefix
// ignore common date-like patterns: https://github.com/facebook/docusaurus/issues/4640
// ignore common versioning patterns: https://github.com/facebook/docusaurus/issues/4653
// Both of them would look like 7.0-foo or 2021-11-foo
// 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 either a
// version or a float. User can configure her own NumberPrefixParser if she
// wants 8.0 to be interpreted as a float
const ignoredPrefixPattern = /^\d+[-_.]\d+/;
const numberPrefixPattern =
/^(?<numberPrefix>\d+)\s*[-_.]+\s*(?<suffix>[^-_.\s].*)$/;
// 0-myDoc => {filename: myDoc, numberPrefix: 0}
// 003 - myDoc => {filename: myDoc, numberPrefix: 3}
export const DefaultNumberPrefixParser: NumberPrefixParser = (
filename: string,
) => {
if (ignoredPrefixPattern.test(filename)) {
return {filename, numberPrefix: undefined};
}
const match = numberPrefixPattern.exec(filename);
if (!match) {
return {filename, numberPrefix: undefined};
}
return {
filename: match.groups!.suffix!,
numberPrefix: parseInt(match.groups!.numberPrefix!, 10),
};
};
export const DisabledNumberPrefixParser: NumberPrefixParser = (
filename: string,
) => ({filename, numberPrefix: undefined});
// 0-myDoc => myDoc
export function stripNumberPrefix(
str: string,
parser: NumberPrefixParser,
): string {
return parser(str).filename;
}
// 0-myFolder/0-mySubfolder/0-myDoc => myFolder/mySubfolder/myDoc
export function stripPathNumberPrefixes(
path: string,
parser: NumberPrefixParser,
): string {
return path
.split('/')
.map((segment) => stripNumberPrefix(segment, parser))
.join('/');
}

View File

@@ -0,0 +1,173 @@
/**
* 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 logger from '@docusaurus/logger';
import {
Joi,
RemarkPluginsSchema,
RehypePluginsSchema,
AdmonitionsSchema,
RouteBasePathSchema,
URISchema,
} from '@docusaurus/utils-validation';
import {GlobExcludeDefault} from '@docusaurus/utils';
import {DefaultSidebarItemsGenerator} from './sidebars/generator';
import {
DefaultNumberPrefixParser,
DisabledNumberPrefixParser,
} from './numberPrefix';
import type {OptionValidationContext} from '@docusaurus/types';
import type {PluginOptions, Options} from '@docusaurus/plugin-content-docs';
export const DEFAULT_OPTIONS: Omit<PluginOptions, 'id' | 'sidebarPath'> = {
path: 'docs', // Path to data on filesystem, relative to site dir.
routeBasePath: 'docs', // URL Route.
tagsBasePath: 'tags', // URL Tags Route.
include: ['**/*.{md,mdx}'], // Extensions to include.
exclude: GlobExcludeDefault,
sidebarItemsGenerator: DefaultSidebarItemsGenerator,
numberPrefixParser: DefaultNumberPrefixParser,
docsRootComponent: '@theme/DocsRoot',
docVersionRootComponent: '@theme/DocVersionRoot',
docRootComponent: '@theme/DocRoot',
docItemComponent: '@theme/DocItem',
docTagDocListComponent: '@theme/DocTagDocListPage',
docTagsListComponent: '@theme/DocTagsListPage',
docCategoryGeneratedIndexComponent: '@theme/DocCategoryGeneratedIndexPage',
remarkPlugins: [],
rehypePlugins: [],
beforeDefaultRemarkPlugins: [],
beforeDefaultRehypePlugins: [],
showLastUpdateTime: false,
showLastUpdateAuthor: false,
admonitions: true,
includeCurrentVersion: true,
disableVersioning: false,
lastVersion: undefined,
versions: {},
editCurrentVersion: false,
editLocalizedFiles: false,
sidebarCollapsible: true,
sidebarCollapsed: true,
breadcrumbs: true,
};
const VersionOptionsSchema = Joi.object({
path: Joi.string().allow('').optional(),
label: Joi.string().optional(),
banner: Joi.string().equal('none', 'unreleased', 'unmaintained').optional(),
badge: Joi.boolean().optional(),
className: Joi.string().optional(),
noIndex: Joi.boolean().optional(),
});
const VersionsOptionsSchema = Joi.object()
.pattern(Joi.string().required(), VersionOptionsSchema)
.default(DEFAULT_OPTIONS.versions);
const OptionsSchema = Joi.object<PluginOptions>({
path: Joi.string().default(DEFAULT_OPTIONS.path),
editUrl: Joi.alternatives().try(URISchema, Joi.function()),
editCurrentVersion: Joi.boolean().default(DEFAULT_OPTIONS.editCurrentVersion),
editLocalizedFiles: Joi.boolean().default(DEFAULT_OPTIONS.editLocalizedFiles),
routeBasePath: RouteBasePathSchema.default(DEFAULT_OPTIONS.routeBasePath),
tagsBasePath: Joi.string().default(DEFAULT_OPTIONS.tagsBasePath),
// @ts-expect-error: deprecated
homePageId: Joi.any().forbidden().messages({
'any.unknown':
'The docs plugin option homePageId is not supported anymore. To make a doc the "home", please add "slug: /" in its front matter. See: https://docusaurus.io/docs/next/docs-introduction#home-page-docs',
}),
include: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.include),
exclude: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.exclude),
sidebarPath: Joi.alternatives().try(
Joi.boolean().invalid(true),
Joi.string(),
),
sidebarItemsGenerator: Joi.function().default(
() => DEFAULT_OPTIONS.sidebarItemsGenerator,
),
sidebarCollapsible: Joi.boolean().default(DEFAULT_OPTIONS.sidebarCollapsible),
sidebarCollapsed: Joi.boolean().default(DEFAULT_OPTIONS.sidebarCollapsed),
numberPrefixParser: Joi.alternatives()
.try(
Joi.function(),
// Convert boolean values to functions
Joi.alternatives().conditional(Joi.boolean(), {
then: Joi.custom((val: boolean) =>
val ? DefaultNumberPrefixParser : DisabledNumberPrefixParser,
),
}),
)
.default(() => DEFAULT_OPTIONS.numberPrefixParser),
docsRootComponent: Joi.string().default(DEFAULT_OPTIONS.docsRootComponent),
docVersionRootComponent: Joi.string().default(
DEFAULT_OPTIONS.docVersionRootComponent,
),
docRootComponent: Joi.string().default(DEFAULT_OPTIONS.docRootComponent),
docItemComponent: Joi.string().default(DEFAULT_OPTIONS.docItemComponent),
docTagsListComponent: Joi.string().default(
DEFAULT_OPTIONS.docTagsListComponent,
),
docTagDocListComponent: Joi.string().default(
DEFAULT_OPTIONS.docTagDocListComponent,
),
docCategoryGeneratedIndexComponent: Joi.string().default(
DEFAULT_OPTIONS.docCategoryGeneratedIndexComponent,
),
remarkPlugins: RemarkPluginsSchema.default(DEFAULT_OPTIONS.remarkPlugins),
rehypePlugins: RehypePluginsSchema.default(DEFAULT_OPTIONS.rehypePlugins),
beforeDefaultRemarkPlugins: RemarkPluginsSchema.default(
DEFAULT_OPTIONS.beforeDefaultRemarkPlugins,
),
beforeDefaultRehypePlugins: RehypePluginsSchema.default(
DEFAULT_OPTIONS.beforeDefaultRehypePlugins,
),
admonitions: AdmonitionsSchema.default(DEFAULT_OPTIONS.admonitions),
showLastUpdateTime: Joi.bool().default(DEFAULT_OPTIONS.showLastUpdateTime),
showLastUpdateAuthor: Joi.bool().default(
DEFAULT_OPTIONS.showLastUpdateAuthor,
),
includeCurrentVersion: Joi.bool().default(
DEFAULT_OPTIONS.includeCurrentVersion,
),
onlyIncludeVersions: Joi.array().items(Joi.string().required()).optional(),
disableVersioning: Joi.bool().default(DEFAULT_OPTIONS.disableVersioning),
lastVersion: Joi.string().optional(),
versions: VersionsOptionsSchema,
breadcrumbs: Joi.bool().default(DEFAULT_OPTIONS.breadcrumbs),
});
export function validateOptions({
validate,
options: userOptions,
}: OptionValidationContext<Options, PluginOptions>): PluginOptions {
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
if (typeof options.sidebarCollapsed === 'undefined') {
options = {
...options,
sidebarCollapsed: false,
};
}
if (options.sidebarCollapsed) {
logger.warn`The docs plugin config is inconsistent. It does not make sense to use code=${'sidebarCollapsible: false'} and code=${'sidebarCollapsed: true'} at the same time. code=${'sidebarCollapsed: true'} will be ignored.`;
options = {
...options,
sidebarCollapsed: false,
};
}
}
const normalizedOptions = validate(OptionsSchema, options);
return normalizedOptions;
}

View File

@@ -0,0 +1,656 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/// <reference types="@docusaurus/module-type-aliases" />
declare module '@docusaurus/plugin-content-docs' {
import type {MDXOptions, LoadedMDXContent} from '@docusaurus/mdx-loader';
import type {
ContentPaths,
FrontMatterTag,
TagsListItem,
TagModule,
Tag,
} from '@docusaurus/utils';
import type {Plugin, LoadContext} from '@docusaurus/types';
import type {Overwrite, Required} from 'utility-types';
export type Assets = {
image?: string;
};
export type FileChange = {
author?: string;
/** Date can be any
* [parsable date string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse).
*/
date?: Date | string;
};
/**
* Custom callback for parsing number prefixes from file/folder names.
*/
export type NumberPrefixParser = (filename: string) => {
/** File name without number prefix, without any other modification. */
filename: string;
/** The number prefix. Can be float, integer, negative, or anything. */
numberPrefix?: number;
};
export type CategoryIndexMatcher = (param: {
/** The file name, without extension */
fileName: string;
/**
* The list of directories, from lowest level to highest.
* If there's no dir name, directories is ['.']
*/
directories: string[];
/** The extension, with a leading dot */
extension: string;
}) => boolean;
export type EditUrlFunction = (editUrlParams: {
/** Name of the version. */
version: string;
/**
* Path of the version's root content path, relative to the site directory.
* Usually the same as `options.path` but can be localized or versioned.
*/
versionDocsDirPath: string;
/** Path of the doc file, relative to `versionDocsDirPath`. */
docPath: string;
/** @see {@link DocMetadata.permalink} */
permalink: string;
/** Locale name. */
locale: string;
}) => string | undefined;
export type MetadataOptions = {
/**
* URL route for the docs section of your site. **DO NOT** include a
* trailing slash. Use `/` for shipping docs without base path.
*/
routeBasePath: string;
/**
* Base URL to edit your site. The final URL is computed by `editUrl +
* relativeDocPath`. Using a function allows more nuanced control for each
* file. Omitting this variable entirely will disable edit links.
*/
editUrl?: string | EditUrlFunction;
/**
* The edit URL will always target the current version doc instead of older
* versions. Ignored when `editUrl` is a function.
*/
editCurrentVersion: boolean;
/**
* The edit URL will target the localized file, instead of the original
* unlocalized file. Ignored when `editUrl` is a function.
*/
editLocalizedFiles: boolean;
/** Whether to display the last date the doc was updated. */
showLastUpdateTime?: boolean;
/** Whether to display the author who last updated the doc. */
showLastUpdateAuthor?: boolean;
/**
* Custom parsing logic to extract number prefixes from file names. Use
* `false` to disable this behavior and leave the docs untouched, and `true`
* to use the default parser.
*
* @param filename One segment of the path, without any slashes.
* @see https://docusaurus.io/docs/sidebar#using-number-prefixes
*/
numberPrefixParser: NumberPrefixParser;
/** Enable or disable the breadcrumbs on doc pages. */
breadcrumbs: boolean;
};
export type PathOptions = {
/**
* Path to the docs content directory on the file system, relative to site
* directory.
*/
path: string;
/**
* Path to sidebar configuration. Use `false` to disable sidebars, or
* `undefined` to create a fully autogenerated sidebar.
*/
sidebarPath?: string | false | undefined;
};
// TODO support custom version banner?
// {type: "error", content: "html content"}
export type VersionBanner = 'unreleased' | 'unmaintained';
export type VersionOptions = {
/**
* The base path of the version, will be appended to `baseUrl` +
* `routeBasePath`.
*/
path?: string;
/** The label of the version to be used in badges, dropdowns, etc. */
label?: string;
/** The banner to show at the top of a doc of that version. */
banner?: 'none' | VersionBanner;
/** Show a badge with the version label at the top of each doc. */
badge?: boolean;
/** Prevents search engines from indexing this version */
noIndex?: boolean;
/** Add a custom class name to the <html> element of each doc. */
className?: string;
};
export type VersionsOptions = {
/**
* The version navigated to in priority and displayed by default for docs
* navbar items.
*
* @see https://docusaurus.io/docs/versioning#terminology
*/
lastVersion?: string;
/** Only include a subset of all available versions. */
onlyIncludeVersions?: string[];
/**
* Explicitly disable versioning even when multiple versions exist. This
* will make the site only include the current version. Will error if
* `includeCurrentVersion: false` and `disableVersioning: true`.
*/
disableVersioning: boolean;
/** Include the current version of your docs. */
includeCurrentVersion: boolean;
/** Independent customization of each version's properties. */
versions: {[versionName: string]: VersionOptions};
};
export type SidebarOptions = {
/**
* Whether sidebar categories are collapsible by default.
*
* @see https://docusaurus.io/docs/sidebar#collapsible-categories
*/
sidebarCollapsible: boolean;
/**
* Whether sidebar categories are collapsed by default.
*
* @see https://docusaurus.io/docs/sidebar#expanded-categories-by-default
*/
sidebarCollapsed: boolean;
};
export type PluginOptions = MetadataOptions &
PathOptions &
VersionsOptions &
MDXOptions &
SidebarOptions & {
/** Plugin ID. */
id: string;
/**
* Array of glob patterns matching Markdown files to be built, relative to
* the content path.
*/
include: string[];
/**
* Array of glob patterns matching Markdown files to be excluded. Serves
* as refinement based on the `include` option.
*/
exclude: string[];
/**
* Parent component of all the docs plugin pages (including all versions).
* Stays mounted when navigation between docs pages and versions.
*/
docsRootComponent: string;
/**
* Parent component of all docs pages of an individual version:
* - docs pages with sidebars
* - tags pages
* Stays mounted when navigation between pages of that specific version.
*/
docVersionRootComponent: string;
/**
* Parent component of all docs pages with sidebars:
* - regular docs pages
* - category generated index pages
* Stays mounted when navigation between such pages.
*/
docRootComponent: string;
/** Main doc container, with TOC, pagination, etc. */
docItemComponent: string;
/** Root component of the "docs containing tag X" page. */
docTagDocListComponent: string;
/** Root component of the tags list page */
docTagsListComponent: string;
/** Root component of the generated category index page. */
docCategoryGeneratedIndexComponent: string;
sidebarItemsGenerator: import('./sidebars/types').SidebarItemsGeneratorOption;
/**
* URL route for the tags section of your doc version. Will be appended to
* `routeBasePath`. **DO NOT** include a trailing slash.
*/
tagsBasePath: string;
};
export type Options = Partial<
Overwrite<
PluginOptions,
{
/**
* Custom parsing logic to extract number prefixes from file names. Use
* `false` to disable this behavior and leave the docs untouched, and
* `true` to use the default parser.
*
* @param filename One segment of the path, without any slashes.
* @see https://docusaurus.io/docs/sidebar#using-number-prefixes
*/
numberPrefixParser: PluginOptions['numberPrefixParser'] | boolean;
}
>
>;
export type SidebarsConfig = import('./sidebars/types').SidebarsConfig;
export type VersionMetadata = ContentPaths & {
/** A name like `1.0.0`. Acquired from `versions.json`. */
versionName: string;
/** Like `Version 1.0.0`. Can be configured through `versions.label`. */
label: string;
/**
* Version's base path in the form of `/<baseUrl>/<routeBasePath>/1.0.0`.
* Can be configured through `versions.path`.
*/
path: string;
/** Tags base path in the form of `<versionPath>/tags`. */
tagsPath: string;
/**
* The base URL to which the doc file path will be appended. Will be
* `undefined` if `editUrl` is `undefined` or a function.
*/
editUrl?: string | undefined;
/**
* The base URL to which the localized doc file path will be appended. Will
* be `undefined` if `editUrl` is `undefined` or a function.
*/
editUrlLocalized?: string | undefined;
/**
* "unmaintained" is the version before latest; "unreleased" is the version
* after latest. `null` is the latest version without a banner. Can be
* configured with `versions.banner`: `banner: "none"` will be transformed
* to `null` here.
*/
banner: VersionBanner | null;
/** Show a badge with the version label at the top of each doc. */
badge: boolean;
/** Prevents search engines from indexing this version */
noIndex: boolean;
/** Add a custom class name to the <html> element of each doc. */
className: string;
/**
* Whether this version is the "last" version. Can be configured with
* `lastVersion` option.
*/
isLast: boolean;
/**
* Like `versioned_sidebars/1.0.0.json`. Versioned sidebars file may be
* nonexistent since we don't create empty files.
*/
sidebarFilePath: string | false | undefined;
/**
* Will be -1 for the latest docs, and `undefined` for everything else.
* Because `/docs/foo` should always be after `/docs/<versionName>/foo`.
*/
routePriority: number | undefined;
};
export type DocFrontMatter = {
/**
* The last part of the doc ID (will be refactored in the future to be the
* full ID instead)
* @see {@link DocMetadata.id}
*/
id?: string;
/**
* Will override the default title collected from h1 heading.
* @see {@link DocMetadata.title}
*/
title?: string;
/**
* Front matter tags, unnormalized.
* @see {@link DocMetadata.tags}
*/
tags?: FrontMatterTag[];
/**
* If there isn't a Markdown h1 heading (which, if there is, we don't
* remove), this front matter will cause the front matter title to not be
* displayed in the doc page.
*/
hide_title?: boolean;
/** Hide the TOC on the right. */
hide_table_of_contents?: boolean;
/** Used in the head meta. */
keywords?: string[];
/** Used in the head meta. Should use `assets.image` in priority. */
image?: string;
/**
* Will override the default excerpt.
* @see {@link DocMetadata.description}
*/
description?: string;
/**
* Custom slug appended after /<baseUrl>/<routeBasePath>/<versionPath>
* @see {@link DocMetadata.slug}
*/
slug?: string;
/** Customizes the sidebar label for this doc. Will default to its title. */
sidebar_label?: string;
/**
* Controls the position of a doc inside the generated sidebar slice when
* using autogenerated sidebar items.
*
* @see https://docusaurus.io/docs/sidebar#autogenerated-sidebar-metadata
*/
sidebar_position?: number;
/**
* Gives the corresponding sidebar label a special class name when using
* autogenerated sidebars.
*/
sidebar_class_name?: string;
/**
* Will be propagated to the final sidebars data structure. Useful if you
* have swizzled sidebar-related code or simply querying doc data through
* sidebars.
*/
sidebar_custom_props?: {[key: string]: unknown};
/**
* Changes the sidebar association of the current doc. Use `null` to make
* the current doc not associated to any sidebar.
*/
displayed_sidebar?: string | null;
/**
* Customizes the pagination label for this doc. Will default to the sidebar
* label.
*/
pagination_label?: string;
/** Overrides the default URL computed for this doc. */
custom_edit_url?: string | null;
/**
* Whether number prefix parsing is disabled on this doc.
* @see https://docusaurus.io/docs/sidebar#using-number-prefixes
*/
parse_number_prefixes?: boolean;
/**
* Minimum TOC heading level. Must be between 2 and 6 and lower or equal to
* the max value.
*/
toc_min_heading_level?: number;
/** Maximum TOC heading level. Must be between 2 and 6. */
toc_max_heading_level?: number;
/**
* The ID of the documentation you want the "Next" pagination to link to.
* Use `null` to disable showing "Next" for this page.
* @see {@link DocMetadata.next}
*/
pagination_next?: string | null;
/**
* The ID of the documentation you want the "Previous" pagination to link
* to. Use `null` to disable showing "Previous" for this page.
* @see {@link DocMetadata.prev}
*/
pagination_prev?: string | null;
/** Should this doc be excluded from production builds? */
draft?: boolean;
/** Should this doc be accessible but hidden in production builds? */
unlisted?: boolean;
/** Allows overriding the last updated author and/or date. */
last_update?: FileChange;
};
export type LastUpdateData = {
/** A timestamp in **seconds**, directly acquired from `git log`. */
lastUpdatedAt?: number;
/** `lastUpdatedAt` formatted as a date according to the current locale. */
formattedLastUpdatedAt?: string;
/** The author's name directly acquired from `git log`. */
lastUpdatedBy?: string;
};
export type DocMetadataBase = LastUpdateData & {
/**
* The document id.
* Multiple documents can have the same id, when in different versions.
*/
id: string;
/** The name of the version this doc belongs to. */
version: string;
/**
* Used to generate the page h1 heading, tab title, and pagination title.
*/
title: string;
/**
* Description used in the meta. Could be an empty string (empty content)
*/
description: string;
/** Path to the Markdown source, with `@site` alias. */
source: string;
/**
* Posix path relative to the content path. Can be `"."`.
* e.g. "folder/subfolder/subsubfolder"
*/
sourceDirName: string;
/** `permalink` without base URL or version path. */
slug: string;
/** Full URL to this doc, with base URL and version path. */
permalink: string;
/**
* Draft docs will be excluded for production environment.
*/
draft: boolean;
/**
* Unlisted docs are accessible when directly visible, but will be hidden
* from the sidebar and pagination in production.
*/
unlisted: boolean;
/**
* Position in an autogenerated sidebar slice, acquired through front matter
* or number prefix.
*/
sidebarPosition?: number;
/**
* Acquired from the options; can be customized with front matter.
* `custom_edit_url` will always lead to it being null, but you should treat
* `undefined` and `null` as equivalent.
*/
editUrl?: string | null;
/** Tags, normalized. */
tags: Tag[];
/** Front matter, as-is. */
frontMatter: DocFrontMatter & {[key: string]: unknown};
};
export type DocMetadata = DocMetadataBase &
PropNavigation & {
/** Name of the sidebar this doc is associated with. */
sidebar?: string;
};
export type CategoryGeneratedIndexMetadata = Required<
Omit<
import('./sidebars/types').SidebarItemCategoryLinkGeneratedIndex,
'type'
>,
'title'
> & {
navigation: PropNavigation;
/**
* Name of the sidebar this doc is associated with. Unlike
* `DocMetadata.sidebar`, this will always be defined, because a generated
* index can only be generated from a category.
*/
sidebar: string;
};
export type PropNavigationLink = {
readonly title: string;
readonly permalink: string;
};
export type PropNavigation = {
/**
* Used in pagination. Content is just a subset of another doc's metadata.
*/
readonly previous?: PropNavigationLink;
/**
* Used in pagination. Content is just a subset of another doc's metadata.
*/
readonly next?: PropNavigationLink;
};
export type PropVersionDoc = Pick<
DocMetadata,
'id' | 'title' | 'description' | 'sidebar'
>;
export type PropVersionDocs = {
[docId: string]: PropVersionDoc;
};
export type PropDocContent = LoadedMDXContent<
DocFrontMatter,
DocMetadata,
Assets
>;
export type PropVersionMetadata = Pick<
VersionMetadata,
'label' | 'banner' | 'badge' | 'className' | 'isLast' | 'noIndex'
> & {
/** ID of the docs plugin this version belongs to. */
pluginId: string;
/** Name of this version. */
version: string;
/** Sidebars contained in this version. */
docsSidebars: PropSidebars;
/** Docs contained in this version. */
docs: PropVersionDocs;
};
export type PropCategoryGeneratedIndex = Omit<
CategoryGeneratedIndexMetadata,
'sidebar'
>;
export type PropSidebarItemLink =
import('./sidebars/types').PropSidebarItemLink;
export type PropSidebarItemHtml =
import('./sidebars/types').PropSidebarItemHtml;
export type PropSidebarItemCategory =
import('./sidebars/types').PropSidebarItemCategory;
export type PropSidebarItem = import('./sidebars/types').PropSidebarItem;
export type PropSidebarBreadcrumbsItem =
import('./sidebars/types').PropSidebarBreadcrumbsItem;
export type PropSidebar = import('./sidebars/types').PropSidebar;
export type PropSidebars = import('./sidebars/types').PropSidebars;
export type PropTagDocListDoc = Pick<
DocMetadata,
'id' | 'title' | 'description' | 'permalink'
>;
export type PropTagDocList = TagModule & {items: PropTagDocListDoc[]};
export type PropTagsListPage = {
tags: TagsListItem[];
};
export type LoadedVersion = VersionMetadata & {
docs: DocMetadata[];
drafts: DocMetadata[];
sidebars: import('./sidebars/types').Sidebars;
};
export type LoadedContent = {
loadedVersions: LoadedVersion[];
};
export default function pluginContentDocs(
context: LoadContext,
options: PluginOptions,
): Promise<Plugin<LoadedContent>>;
}
declare module '@theme/DocItem' {
import type {PropDocContent} from '@docusaurus/plugin-content-docs';
export type DocumentRoute = {
readonly component: () => JSX.Element;
readonly exact: boolean;
readonly path: string;
readonly sidebar?: string;
};
export interface Props {
readonly route: DocumentRoute;
readonly content: PropDocContent;
}
export default function DocItem(props: Props): JSX.Element;
}
declare module '@theme/DocCategoryGeneratedIndexPage' {
import type {PropCategoryGeneratedIndex} from '@docusaurus/plugin-content-docs';
export interface Props {
readonly categoryGeneratedIndex: PropCategoryGeneratedIndex;
}
export default function DocCategoryGeneratedIndexPage(
props: Props,
): JSX.Element;
}
declare module '@theme/DocTagsListPage' {
import type {PropTagsListPage} from '@docusaurus/plugin-content-docs';
export interface Props extends PropTagsListPage {}
export default function DocTagsListPage(props: Props): JSX.Element;
}
declare module '@theme/DocTagDocListPage' {
import type {PropTagDocList} from '@docusaurus/plugin-content-docs';
export interface Props {
readonly tag: PropTagDocList;
}
export default function DocTagDocListPage(props: Props): JSX.Element;
}
declare module '@theme/DocBreadcrumbs' {
export default function DocBreadcrumbs(): JSX.Element;
}
declare module '@theme/DocsRoot' {
import type {RouteConfigComponentProps} from 'react-router-config';
import type {Required} from 'utility-types';
export interface Props extends Required<RouteConfigComponentProps, 'route'> {}
export default function DocsRoot(props: Props): JSX.Element;
}
declare module '@theme/DocVersionRoot' {
import type {PropVersionMetadata} from '@docusaurus/plugin-content-docs';
import type {RouteConfigComponentProps} from 'react-router-config';
import type {Required} from 'utility-types';
export interface Props extends Required<RouteConfigComponentProps, 'route'> {
readonly version: PropVersionMetadata;
}
export default function DocVersionRoot(props: Props): JSX.Element;
}
declare module '@theme/DocRoot' {
import type {RouteConfigComponentProps} from 'react-router-config';
import type {Required} from 'utility-types';
export interface Props extends Required<RouteConfigComponentProps, 'route'> {}
export default function DocRoot(props: Props): JSX.Element;
}

View File

@@ -0,0 +1,226 @@
/**
* 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 _ from 'lodash';
import {createDocsByIdIndex} from './docs';
import type {VersionTag, VersionTags} from './types';
import type {
SidebarItemDoc,
SidebarItem,
SidebarItemCategory,
SidebarItemCategoryLink,
} from './sidebars/types';
import type {
PropSidebars,
PropVersionMetadata,
PropSidebarItem,
PropSidebarItemCategory,
PropTagDocList,
PropTagDocListDoc,
PropTagsListPage,
PropSidebarItemLink,
PropVersionDocs,
DocMetadata,
LoadedVersion,
} from '@docusaurus/plugin-content-docs';
export function toSidebarDocItemLinkProp({
item,
doc,
}: {
item: SidebarItemDoc;
doc: Pick<
DocMetadata,
'id' | 'title' | 'permalink' | 'unlisted' | 'frontMatter'
>;
}): PropSidebarItemLink {
const {
id,
title,
permalink,
frontMatter: {
sidebar_label: sidebarLabel,
sidebar_custom_props: customProps,
},
unlisted,
} = doc;
return {
type: 'link',
label: sidebarLabel ?? item.label ?? title,
href: permalink,
className: item.className,
customProps: item.customProps ?? customProps,
docId: id,
unlisted,
};
}
export function toSidebarsProp(loadedVersion: LoadedVersion): PropSidebars {
const docsById = createDocsByIdIndex(loadedVersion.docs);
function getDocById(docId: string): DocMetadata {
const docMetadata = docsById[docId];
if (!docMetadata) {
throw new Error(
`Invalid sidebars file. The document with id "${docId}" was used in the sidebar, but no document with this id could be found.
Available document ids are:
- ${Object.keys(docsById).sort().join('\n- ')}`,
);
}
return docMetadata;
}
const convertDocLink = (item: SidebarItemDoc): PropSidebarItemLink => {
const doc = getDocById(item.id);
return toSidebarDocItemLinkProp({item, doc});
};
function getCategoryLinkHref(
link: SidebarItemCategoryLink | undefined,
): string | undefined {
switch (link?.type) {
case 'doc':
return getDocById(link.id).permalink;
case 'generated-index':
return link.permalink;
default:
return undefined;
}
}
function getCategoryLinkUnlisted(
link: SidebarItemCategoryLink | undefined,
): boolean {
if (link?.type === 'doc') {
return getDocById(link.id).unlisted;
}
return false;
}
function getCategoryLinkCustomProps(
link: SidebarItemCategoryLink | undefined,
) {
switch (link?.type) {
case 'doc':
return getDocById(link.id).frontMatter.sidebar_custom_props;
default:
return undefined;
}
}
function convertCategory(item: SidebarItemCategory): PropSidebarItemCategory {
const {link, ...rest} = item;
const href = getCategoryLinkHref(link);
const linkUnlisted = getCategoryLinkUnlisted(link);
const customProps = item.customProps ?? getCategoryLinkCustomProps(link);
return {
...rest,
items: item.items.map(normalizeItem),
...(href && {href}),
...(linkUnlisted && {linkUnlisted}),
...(customProps && {customProps}),
};
}
function normalizeItem(item: SidebarItem): PropSidebarItem {
switch (item.type) {
case 'category':
return convertCategory(item);
case 'ref':
case 'doc':
return convertDocLink(item);
case 'link':
default:
return item;
}
}
// Transform the sidebar so that all sidebar item will be in the
// form of 'link' or 'category' only.
// This is what will be passed as props to the UI component.
return _.mapValues(loadedVersion.sidebars, (items) =>
items.map(normalizeItem),
);
}
function toVersionDocsProp(loadedVersion: LoadedVersion): PropVersionDocs {
return Object.fromEntries(
loadedVersion.docs.map((doc) => [
doc.id,
{
id: doc.id,
title: doc.title,
description: doc.description,
sidebar: doc.sidebar,
},
]),
);
}
export function toVersionMetadataProp(
pluginId: string,
loadedVersion: LoadedVersion,
): PropVersionMetadata {
return {
pluginId,
version: loadedVersion.versionName,
label: loadedVersion.label,
banner: loadedVersion.banner,
badge: loadedVersion.badge,
noIndex: loadedVersion.noIndex,
className: loadedVersion.className,
isLast: loadedVersion.isLast,
docsSidebars: toSidebarsProp(loadedVersion),
docs: toVersionDocsProp(loadedVersion),
};
}
export function toTagDocListProp({
allTagsPath,
tag,
docs,
}: {
allTagsPath: string;
tag: VersionTag;
docs: DocMetadata[];
}): PropTagDocList {
function toDocListProp(): PropTagDocListDoc[] {
const list = _.compact(
tag.docIds.map((id) => docs.find((doc) => doc.id === id)),
);
// Sort docs by title
list.sort((doc1, doc2) => doc1.title.localeCompare(doc2.title));
return list.map((doc) => ({
id: doc.id,
title: doc.title,
description: doc.description,
permalink: doc.permalink,
}));
}
return {
label: tag.label,
permalink: tag.permalink,
allTagsPath,
count: tag.docIds.length,
items: toDocListProp(),
unlisted: tag.unlisted,
};
}
export function toTagsListTagsProp(
versionTags: VersionTags,
): PropTagsListPage['tags'] {
return Object.values(versionTags)
.filter((tagValue) => !tagValue.unlisted)
.map((tagValue) => ({
label: tagValue.label,
permalink: tagValue.permalink,
count: tagValue.docIds.length,
}));
}

View File

@@ -0,0 +1,255 @@
/**
* 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 _ from 'lodash';
import logger from '@docusaurus/logger';
import {docuHash, createSlugger, normalizeUrl} from '@docusaurus/utils';
import {
toTagDocListProp,
toTagsListTagsProp,
toVersionMetadataProp,
} from './props';
import {getVersionTags} from './tags';
import type {PluginContentLoadedActions, RouteConfig} from '@docusaurus/types';
import type {FullVersion, VersionTag} from './types';
import type {
CategoryGeneratedIndexMetadata,
PluginOptions,
PropTagsListPage,
} from '@docusaurus/plugin-content-docs';
async function buildVersionCategoryGeneratedIndexRoutes({
version,
actions,
options,
aliasedSource,
}: BuildVersionRoutesParam): Promise<RouteConfig[]> {
const slugs = createSlugger();
async function buildCategoryGeneratedIndexRoute(
categoryGeneratedIndex: CategoryGeneratedIndexMetadata,
): Promise<RouteConfig> {
const {sidebar, ...prop} = categoryGeneratedIndex;
const propFileName = slugs.slug(
`${version.path}-${categoryGeneratedIndex.sidebar}-category-${categoryGeneratedIndex.title}`,
);
const propData = await actions.createData(
`${docuHash(`category/${propFileName}`)}.json`,
JSON.stringify(prop, null, 2),
);
return {
path: categoryGeneratedIndex.permalink,
component: options.docCategoryGeneratedIndexComponent,
exact: true,
modules: {
categoryGeneratedIndex: aliasedSource(propData),
},
// Same as doc, this sidebar route attribute permits to associate this
// subpage to the given sidebar
...(sidebar && {sidebar}),
};
}
return Promise.all(
version.categoryGeneratedIndices.map(buildCategoryGeneratedIndexRoute),
);
}
async function buildVersionDocRoutes({
version,
actions,
options,
}: BuildVersionRoutesParam): Promise<RouteConfig[]> {
return Promise.all(
version.docs.map(async (metadataItem) => {
await actions.createData(
// Note that this created data path must be in sync with
// metadataPath provided to mdx-loader.
`${docuHash(metadataItem.source)}.json`,
JSON.stringify(metadataItem, null, 2),
);
const docRoute: RouteConfig = {
path: metadataItem.permalink,
component: options.docItemComponent,
exact: true,
modules: {
content: metadataItem.source,
},
// Because the parent (DocRoot) comp need to access it easily
// This permits to render the sidebar once without unmount/remount when
// navigating (and preserve sidebar state)
...(metadataItem.sidebar && {
sidebar: metadataItem.sidebar,
}),
};
return docRoute;
}),
);
}
async function buildVersionSidebarRoute(param: BuildVersionRoutesParam) {
const [docRoutes, categoryGeneratedIndexRoutes] = await Promise.all([
buildVersionDocRoutes(param),
buildVersionCategoryGeneratedIndexRoutes(param),
]);
const subRoutes = [...docRoutes, ...categoryGeneratedIndexRoutes];
return {
path: param.version.path,
exact: false,
component: param.options.docRootComponent,
routes: subRoutes,
};
}
async function buildVersionTagsRoutes(
param: BuildVersionRoutesParam,
): Promise<RouteConfig[]> {
const {version, options, actions, aliasedSource} = param;
const versionTags = getVersionTags(version.docs);
async function buildTagsListRoute(): Promise<RouteConfig | null> {
// Don't create a tags list page if there's no tag
if (Object.keys(versionTags).length === 0) {
return null;
}
const tagsProp: PropTagsListPage['tags'] = toTagsListTagsProp(versionTags);
const tagsPropPath = await actions.createData(
`${docuHash(`tags-list-${version.versionName}-prop`)}.json`,
JSON.stringify(tagsProp, null, 2),
);
return {
path: version.tagsPath,
exact: true,
component: options.docTagsListComponent,
modules: {
tags: aliasedSource(tagsPropPath),
},
};
}
async function buildTagDocListRoute(tag: VersionTag): Promise<RouteConfig> {
const tagProps = toTagDocListProp({
allTagsPath: version.tagsPath,
tag,
docs: version.docs,
});
const tagPropPath = await actions.createData(
`${docuHash(`tag-${tag.permalink}`)}.json`,
JSON.stringify(tagProps, null, 2),
);
return {
path: tag.permalink,
component: options.docTagDocListComponent,
exact: true,
modules: {
tag: aliasedSource(tagPropPath),
},
};
}
const [tagsListRoute, allTagsDocListRoutes] = await Promise.all([
buildTagsListRoute(),
Promise.all(Object.values(versionTags).map(buildTagDocListRoute)),
]);
return _.compact([tagsListRoute, ...allTagsDocListRoutes]);
}
type BuildVersionRoutesParam = Omit<BuildAllRoutesParam, 'versions'> & {
version: FullVersion;
};
async function buildVersionRoutes(
param: BuildVersionRoutesParam,
): Promise<RouteConfig> {
const {version, actions, options, aliasedSource} = param;
async function buildVersionSubRoutes() {
const [sidebarRoute, tagsRoutes] = await Promise.all([
buildVersionSidebarRoute(param),
buildVersionTagsRoutes(param),
]);
return [sidebarRoute, ...tagsRoutes];
}
async function doBuildVersionRoutes(): Promise<RouteConfig> {
const versionProp = toVersionMetadataProp(options.id, version);
const versionPropPath = await actions.createData(
`${docuHash(`version-${version.versionName}-metadata-prop`)}.json`,
JSON.stringify(versionProp, null, 2),
);
const subRoutes = await buildVersionSubRoutes();
return {
path: version.path,
exact: false,
component: options.docVersionRootComponent,
routes: subRoutes,
modules: {
version: aliasedSource(versionPropPath),
},
priority: version.routePriority,
};
}
try {
return await doBuildVersionRoutes();
} catch (err) {
logger.error`Can't create version routes for version name=${version.versionName}`;
throw err;
}
}
type BuildAllRoutesParam = Omit<CreateAllRoutesParam, 'actions'> & {
actions: Omit<PluginContentLoadedActions, 'addRoute' | 'setGlobalData'>;
};
// TODO we want this buildAllRoutes function to be easily testable
// Ideally, we should avoid side effects here (ie not injecting actions)
export async function buildAllRoutes(
param: BuildAllRoutesParam,
): Promise<RouteConfig[]> {
const subRoutes = await Promise.all(
param.versions.map((version) =>
buildVersionRoutes({
...param,
version,
}),
),
);
// all docs routes are wrapped under a single parent route, this ensures
// the theme layout never unmounts/remounts when navigating between versions
return [
{
path: normalizeUrl([param.baseUrl, param.options.routeBasePath]),
exact: false,
component: param.options.docsRootComponent,
routes: subRoutes,
},
];
}
type CreateAllRoutesParam = {
baseUrl: string;
versions: FullVersion[];
options: PluginOptions;
actions: PluginContentLoadedActions;
aliasedSource: (str: string) => string;
};
export async function createAllRoutes(
param: CreateAllRoutesParam,
): Promise<void> {
const routes = await buildAllRoutes(param);
routes.forEach(param.actions.addRoute);
}

View File

@@ -0,0 +1,26 @@
/**
* 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.
*/
// APIs available to Node.js
// Those are undocumented but used by some third-party plugins
// For this reason it's preferable to avoid doing breaking changes
// See also https://github.com/facebook/docusaurus/pull/6477
export {
CURRENT_VERSION_NAME,
VERSIONED_DOCS_DIR,
VERSIONED_SIDEBARS_DIR,
VERSIONS_JSON_FILE,
} from './constants';
export {
filterVersions,
getDefaultVersionBanner,
getVersionBadge,
getVersionBanner,
} from './versions';
export {readVersionNames} from './versions/files';

View File

@@ -0,0 +1,10 @@
# Sidebars
This part is very complicated and hard to navigate. Sidebars are loaded through the following steps:
1. **Loading**. The sidebars file is read. Returns `SidebarsConfig`.
2. **Normalization**. The shorthands are expanded. This step is very lenient about the sidebars' shapes. Returns `NormalizedSidebars`.
3. **Validation**. The normalized sidebars are validated. This step happens after normalization, because the normalized sidebars are easier to validate, and allows us to repeatedly validate & generate in the future.
4. **Generation**. This step is done through the "processor" (naming is hard). The `autogenerated` items are unwrapped. In the future, steps 3 and 4 may be repeatedly done until all autogenerated items are unwrapped. Returns `ProcessedSidebars`.
- **Important**: this step should only care about unwrapping autogenerated items, not filtering them, writing additional metadata, applying defaults, etc.—everything will be handled in the post-processor. Important because the generator is exposed to the end-user and we want it to be easy to be reasoned about.
5. **Post-processing**. Defaults are applied (collapsed states), category links are resolved, empty categories are flattened. Returns `Sidebars`.

View File

@@ -0,0 +1,297 @@
/**
* 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 path from 'path';
import _ from 'lodash';
import logger from '@docusaurus/logger';
import {addTrailingSlash} from '@docusaurus/utils';
import {createDocsByIdIndex, toCategoryIndexMatcherParam} from '../docs';
import type {
SidebarItemDoc,
SidebarItemsGenerator,
SidebarItemsGeneratorDoc,
NormalizedSidebarItemCategory,
NormalizedSidebarItem,
SidebarItemCategoryLinkConfig,
} from './types';
const BreadcrumbSeparator = '/';
// Just an alias to the make code more explicit
function getLocalDocId(docId: string): string {
return _.last(docId.split('/'))!;
}
export const CategoryMetadataFilenameBase = '_category_';
export const CategoryMetadataFilenamePattern = '_category_.{json,yml,yaml}';
type WithPosition<T> = T & {
position?: number;
/** The source is the file/folder name */
source?: string;
};
/**
* 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's source file name, and value
* is the doc ID
*/
type Dir = {
[item: string]: Dir | string;
};
// Comment for this feature: https://github.com/facebook/docusaurus/issues/3464#issuecomment-818670449
export const DefaultSidebarItemsGenerator: SidebarItemsGenerator = ({
numberPrefixParser,
isCategoryIndex,
docs: allDocs,
item: {dirName: autogenDir},
categoriesMetadata,
}) => {
const docsById = createDocsByIdIndex(allDocs);
const findDoc = (docId: string): SidebarItemsGeneratorDoc | undefined =>
docsById[docId];
const getDoc = (docId: string): SidebarItemsGeneratorDoc => {
const doc = findDoc(docId);
if (!doc) {
throw new Error(
`Can't find any doc with ID ${docId}.
Available doc IDs:
- ${Object.keys(docsById).join('\n- ')}`,
);
}
return doc;
};
/**
* Step 1. Extract the docs that are in the autogen dir.
*/
function getAutogenDocs(): SidebarItemsGeneratorDoc[] {
function isInAutogeneratedDir(doc: SidebarItemsGeneratorDoc) {
return (
// Doc at the root of the autogenerated sidebar dir
doc.sourceDirName === autogenDir ||
// Autogen dir is . and doc is in subfolder
autogenDir === '.' ||
// Autogen dir is not . and doc is in subfolder
// "api/myDoc" startsWith "api/" (note "api2/myDoc" is not included)
doc.sourceDirName.startsWith(addTrailingSlash(autogenDir))
);
}
const docs = allDocs.filter(isInAutogeneratedDir);
if (docs.length === 0) {
logger.warn`No docs found in path=${autogenDir}: can't auto-generate a sidebar.`;
}
return docs;
}
/**
* 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)
// 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()
function getRelativeBreadcrumb(doc: SidebarItemsGeneratorDoc): string[] {
return autogenDir === doc.sourceDirName
? []
: doc.sourceDirName
.replace(addTrailingSlash(autogenDir), '')
.split(BreadcrumbSeparator);
}
const treeRoot: Dir = {};
docs.forEach((doc) => {
const breadcrumb = getRelativeBreadcrumb(doc);
// We walk down the file's path to generate the fs structure
let currentDir = treeRoot;
breadcrumb.forEach((dir) => {
if (typeof currentDir[dir] === 'undefined') {
currentDir[dir] = {}; // Create new folder.
}
currentDir = currentDir[dir] as Dir; // Go into the subdirectory.
});
// We've walked through the path. Register the file in this directory.
currentDir[path.basename(doc.source)] = doc.id;
});
return treeRoot;
}
/**
* 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,
): WithPosition<NormalizedSidebarItem>[] {
function createDocItem(
id: string,
fullPath: string,
fileName: string,
): WithPosition<SidebarItemDoc> {
const {
sidebarPosition: position,
frontMatter: {
sidebar_label: label,
sidebar_class_name: className,
sidebar_custom_props: customProps,
},
} = getDoc(id);
return {
type: 'doc',
id,
position,
source: fileName,
// We don't want these fields to magically appear in the generated
// sidebar
...(label !== undefined && {label}),
...(className !== undefined && {className}),
...(customProps !== undefined && {customProps}),
};
}
function createCategoryItem(
dir: Dir,
fullPath: string,
folderName: string,
): WithPosition<NormalizedSidebarItemCategory> {
const categoryMetadata =
categoriesMetadata[path.posix.join(autogenDir, fullPath)];
const allItems = Object.entries(dir).map(([key, content]) =>
dirToItem(content, key, `${fullPath}/${key}`),
);
// Try to match a doc inside the category folder,
// using the "local id" (myDoc) or "qualified id" (dirName/myDoc)
function findDocByLocalId(localId: string): SidebarItemDoc | undefined {
return allItems.find(
(item): item is SidebarItemDoc =>
item.type === 'doc' && getLocalDocId(item.id) === localId,
);
}
function findConventionalCategoryDocLink(): SidebarItemDoc | undefined {
return allItems.find((item): item is SidebarItemDoc => {
if (item.type !== 'doc') {
return false;
}
const doc = getDoc(item.id);
return isCategoryIndex(toCategoryIndexMatcherParam(doc));
});
}
// In addition to the ID, this function also retrieves metadata of the
// linked doc that could be used as fallback values for category metadata
function getCategoryLinkedDocMetadata():
| {
id: string;
position?: number;
label?: string;
customProps?: {[key: string]: unknown};
className?: string;
}
| undefined {
const link = categoryMetadata?.link;
if (link !== undefined && link?.type !== 'doc') {
// If a link is explicitly specified, we won't apply conventions
return undefined;
}
const id = link
? findDocByLocalId(link.id)?.id ?? getDoc(link.id).id
: findConventionalCategoryDocLink()?.id;
if (!id) {
return undefined;
}
const doc = getDoc(id);
return {
id,
position: doc.sidebarPosition,
label: doc.frontMatter.sidebar_label ?? doc.title,
customProps: doc.frontMatter.sidebar_custom_props,
className: doc.frontMatter.sidebar_class_name,
};
}
const categoryLinkedDoc = getCategoryLinkedDocMetadata();
const link: SidebarItemCategoryLinkConfig | null | undefined =
categoryLinkedDoc
? {
type: 'doc',
id: categoryLinkedDoc.id, // We "remap" a potentially "local id" to a "qualified id"
}
: categoryMetadata?.link;
// If a doc is linked, remove it from the category subItems
const items = allItems.filter(
(item) => !(item.type === 'doc' && item.id === categoryLinkedDoc?.id),
);
const className =
categoryMetadata?.className ?? categoryLinkedDoc?.className;
const customProps =
categoryMetadata?.customProps ?? categoryLinkedDoc?.customProps;
const {filename, numberPrefix} = numberPrefixParser(folderName);
return {
type: 'category',
label: categoryMetadata?.label ?? categoryLinkedDoc?.label ?? filename,
collapsible: categoryMetadata?.collapsible,
collapsed: categoryMetadata?.collapsed,
position:
categoryMetadata?.position ??
categoryLinkedDoc?.position ??
numberPrefix,
source: folderName,
...(customProps !== undefined && {customProps}),
...(className !== undefined && {className}),
items,
...(link && {link}),
};
}
function dirToItem(
dir: Dir | string, // The directory item to be transformed.
itemKey: string, // File/folder name; for categories, it's used to generate the next `relativePath`.
fullPath: string, // `dir`'s full path relative to the autogen dir.
): WithPosition<NormalizedSidebarItem> {
return typeof dir === 'object'
? createCategoryItem(dir, fullPath, itemKey)
: createDocItem(dir, fullPath, itemKey);
}
return Object.entries(fsModel).map(([key, content]) =>
dirToItem(content, key, key),
);
}
/**
* 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<NormalizedSidebarItem>[],
): NormalizedSidebarItem[] {
const processedSidebarItems = sidebarItems.map((item) => {
if (item.type === 'category') {
return {...item, items: sortItems(item.items)};
}
return item;
});
const sortedSidebarItems = _.sortBy(processedSidebarItems, [
'position',
'source',
]);
return sortedSidebarItems.map(({position, source, ...item}) => item);
}
// TODO: the whole code is designed for pipeline operator
const docs = getAutogenDocs();
const fsModel = treeify(docs);
const sidebarWithPosition = generateSidebar(fsModel);
const sortedSidebar = sortItems(sidebarWithPosition);
return sortedSidebar;
};

View File

@@ -0,0 +1,129 @@
/**
* 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 fs from 'fs-extra';
import path from 'path';
import _ from 'lodash';
import logger from '@docusaurus/logger';
import {loadFreshModule, Globby} from '@docusaurus/utils';
import Yaml from 'js-yaml';
import combinePromises from 'combine-promises';
import {validateSidebars, validateCategoryMetadataFile} from './validation';
import {normalizeSidebars} from './normalization';
import {processSidebars} from './processor';
import {postProcessSidebars} from './postProcessor';
import type {PluginOptions} from '@docusaurus/plugin-content-docs';
import type {SidebarsConfig, Sidebars, SidebarProcessorParams} from './types';
export const DefaultSidebars: SidebarsConfig = {
defaultSidebar: [
{
type: 'autogenerated',
dirName: '.',
},
],
};
export const DisabledSidebars: SidebarsConfig = {};
// If a path is provided, make it absolute
export function resolveSidebarPathOption(
siteDir: string,
sidebarPathOption: PluginOptions['sidebarPath'],
): PluginOptions['sidebarPath'] {
return sidebarPathOption
? path.resolve(siteDir, sidebarPathOption)
: sidebarPathOption;
}
async function readCategoriesMetadata(contentPath: string) {
const categoryFiles = await Globby('**/_category_.{json,yml,yaml}', {
cwd: contentPath,
});
const categoryToFile = _.groupBy(categoryFiles, path.dirname);
return combinePromises(
_.mapValues(categoryToFile, async (files, folder) => {
const filePath = files[0]!;
if (files.length > 1) {
logger.warn`There are more than one category metadata files for path=${folder}: ${files.join(
', ',
)}. The behavior is undetermined.`;
}
const content = await fs.readFile(
path.join(contentPath, filePath),
'utf-8',
);
try {
return validateCategoryMetadataFile(Yaml.load(content));
} catch (err) {
logger.error`The docs sidebar category metadata file path=${filePath} looks invalid!`;
throw err;
}
}),
);
}
async function loadSidebarsFileUnsafe(
sidebarFilePath: string | false | undefined,
): Promise<SidebarsConfig> {
// false => no sidebars
if (sidebarFilePath === false) {
return DisabledSidebars;
}
// undefined => defaults to autogenerated sidebars
if (typeof sidebarFilePath === 'undefined') {
return DefaultSidebars;
}
// Non-existent sidebars file: no sidebars
// Note: this edge case can happen on versioned docs, not current version
// We avoid creating empty versioned sidebars file with the CLI
if (!(await fs.pathExists(sidebarFilePath))) {
return DisabledSidebars;
}
// We don't want sidebars to be cached because of hot reloading.
const module = await loadFreshModule(sidebarFilePath);
// TODO unsafe, need to refactor and improve validation
return module as SidebarsConfig;
}
export async function loadSidebarsFile(
sidebarFilePath: string | false | undefined,
): Promise<SidebarsConfig> {
const sidebars = await loadSidebarsFileUnsafe(sidebarFilePath);
// TODO unsafe, need to refactor and improve validation
return sidebars as SidebarsConfig;
}
export async function loadSidebars(
sidebarFilePath: string | false | undefined,
options: SidebarProcessorParams,
): Promise<Sidebars> {
try {
const sidebarsConfig = await loadSidebarsFileUnsafe(sidebarFilePath);
const normalizedSidebars = normalizeSidebars(sidebarsConfig);
validateSidebars(normalizedSidebars);
const categoriesMetadata = await readCategoriesMetadata(
options.version.contentPath,
);
const processedSidebars = await processSidebars(
normalizedSidebars,
categoriesMetadata,
options,
);
return postProcessSidebars(processedSidebars, options);
} catch (err) {
logger.error`Sidebars file at path=${
sidebarFilePath as string
} failed to be loaded.`;
throw err;
}
}

View File

@@ -0,0 +1,91 @@
/**
* 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 _ from 'lodash';
import logger from '@docusaurus/logger';
import {isCategoriesShorthand} from './utils';
import type {
NormalizedSidebarItem,
NormalizedSidebar,
NormalizedSidebars,
SidebarCategoriesShorthand,
SidebarItemCategoryConfig,
SidebarItemConfig,
SidebarConfig,
SidebarsConfig,
NormalizedSidebarItemCategory,
} from './types';
function normalizeCategoriesShorthand(
sidebar: SidebarCategoriesShorthand,
): SidebarItemCategoryConfig[] {
return Object.entries(sidebar).map(([label, items]) => ({
type: 'category',
label,
items,
}));
}
/**
* Normalizes recursively item and all its children. Ensures that at the end
* each item will be an object with the corresponding type.
*/
export function normalizeItem(
item: SidebarItemConfig,
): NormalizedSidebarItem[] {
if (typeof item === 'string') {
return [{type: 'doc', id: item}];
}
if (isCategoriesShorthand(item)) {
// This will never throw anyways
return normalizeSidebar(item, 'sidebar items slice');
}
if (
(item.type === 'doc' || item.type === 'ref') &&
typeof item.label === 'string'
) {
return [{...item, translatable: true}];
}
if (item.type === 'category') {
const normalizedCategory: NormalizedSidebarItemCategory = {
...item,
items: normalizeSidebar(
item.items,
logger.interpolate`code=${'items'} of the category name=${item.label}`,
),
};
return [normalizedCategory];
}
return [item];
}
function normalizeSidebar(
sidebar: SidebarConfig,
place: string,
): NormalizedSidebar {
if (!Array.isArray(sidebar) && !isCategoriesShorthand(sidebar)) {
throw new Error(
logger.interpolate`Invalid sidebar items collection code=${JSON.stringify(
sidebar,
)} in ${place}: it must either be an array of sidebar items or a shorthand notation (which doesn't contain a code=${'type'} property). See url=${'https://docusaurus.io/docs/sidebar/items'} for all valid syntaxes.`,
);
}
const normalizedSidebar = Array.isArray(sidebar)
? sidebar
: normalizeCategoriesShorthand(sidebar);
return normalizedSidebar.flatMap((subItem) => normalizeItem(subItem));
}
export function normalizeSidebars(
sidebars: SidebarsConfig,
): NormalizedSidebars {
return _.mapValues(sidebars, (sidebar, id) =>
normalizeSidebar(sidebar, logger.interpolate`sidebar name=${id}`),
);
}

View File

@@ -0,0 +1,111 @@
/**
* 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 _ from 'lodash';
import {normalizeUrl} from '@docusaurus/utils';
import type {
SidebarItem,
Sidebars,
SidebarProcessorParams,
ProcessedSidebarItemCategory,
ProcessedSidebarItem,
ProcessedSidebars,
SidebarItemCategoryLink,
} from './types';
export type SidebarPostProcessorParams = SidebarProcessorParams & {
draftIds: Set<string>;
};
function normalizeCategoryLink(
category: ProcessedSidebarItemCategory,
params: SidebarPostProcessorParams,
): SidebarItemCategoryLink | undefined {
if (category.link?.type === 'doc' && params.draftIds.has(category.link.id)) {
return undefined;
}
if (category.link?.type === 'generated-index') {
// Default slug logic can be improved
const getDefaultSlug = () =>
`/category/${params.categoryLabelSlugger.slug(category.label)}`;
const slug = category.link.slug ?? getDefaultSlug();
const permalink = normalizeUrl([params.version.path, slug]);
return {
...category.link,
slug,
permalink,
};
}
return category.link;
}
function postProcessSidebarItem(
item: ProcessedSidebarItem,
params: SidebarPostProcessorParams,
): SidebarItem | null {
if (item.type === 'category') {
// Fail-fast if there's actually no subitems, no because all subitems are
// drafts. This is likely a configuration mistake.
if (item.items.length === 0 && !item.link) {
throw new Error(
`Sidebar category ${item.label} has neither any subitem nor a link. This makes this item not able to link to anything.`,
);
}
const category = {
...item,
collapsed: item.collapsed ?? params.sidebarOptions.sidebarCollapsed,
collapsible: item.collapsible ?? params.sidebarOptions.sidebarCollapsible,
link: normalizeCategoryLink(item, params),
items: item.items
.map((subItem) => postProcessSidebarItem(subItem, params))
.filter((v): v is SidebarItem => Boolean(v)),
};
// If the current category doesn't have subitems, we render a normal link
// instead.
if (category.items.length === 0) {
// Doesn't make sense to render an empty generated index page, so we
// filter the entire category out as well.
if (
!category.link ||
category.link.type === 'generated-index' ||
params.draftIds.has(category.link.id)
) {
return null;
}
return {
type: 'doc',
label: category.label,
id: category.link.id,
};
}
// A non-collapsible category can't be collapsed!
if (!category.collapsible) {
category.collapsed = false;
}
return category;
}
if (
(item.type === 'doc' || item.type === 'ref') &&
params.draftIds.has(item.id)
) {
return null;
}
return item;
}
export function postProcessSidebars(
sidebars: ProcessedSidebars,
params: SidebarProcessorParams,
): Sidebars {
const draftIds = new Set(params.drafts.map((d) => d.id));
return _.mapValues(sidebars, (sidebar) =>
sidebar
.map((item) => postProcessSidebarItem(item, {...params, draftIds}))
.filter((v): v is SidebarItem => Boolean(v)),
);
}

View File

@@ -0,0 +1,122 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import _ from 'lodash';
import combinePromises from 'combine-promises';
import {DefaultSidebarItemsGenerator} from './generator';
import {validateSidebars} from './validation';
import {isCategoryIndex} from '../docs';
import type {
DocMetadataBase,
VersionMetadata,
} from '@docusaurus/plugin-content-docs';
import type {
NormalizedSidebarItem,
NormalizedSidebar,
NormalizedSidebars,
SidebarItemsGeneratorDoc,
SidebarItemsGeneratorVersion,
SidebarItemAutogenerated,
ProcessedSidebarItem,
ProcessedSidebar,
ProcessedSidebars,
SidebarProcessorParams,
CategoryMetadataFile,
} from './types';
function toSidebarItemsGeneratorDoc(
doc: DocMetadataBase,
): SidebarItemsGeneratorDoc {
return _.pick(doc, [
'id',
'title',
'frontMatter',
'source',
'sourceDirName',
'sidebarPosition',
]);
}
function toSidebarItemsGeneratorVersion(
version: VersionMetadata,
): SidebarItemsGeneratorVersion {
return _.pick(version, ['versionName', 'contentPath']);
}
// Handle the generation of autogenerated sidebar items and other
// post-processing checks
async function processSidebar(
unprocessedSidebar: NormalizedSidebar,
categoriesMetadata: {[filePath: string]: CategoryMetadataFile},
params: SidebarProcessorParams,
): Promise<ProcessedSidebar> {
const {sidebarItemsGenerator, numberPrefixParser, docs, version} = params;
// Just a minor lazy transformation optimization
const getSidebarItemsGeneratorDocsAndVersion = _.memoize(() => ({
docs: docs.map(toSidebarItemsGeneratorDoc),
version: toSidebarItemsGeneratorVersion(version),
}));
async function processAutoGeneratedItem(
item: SidebarItemAutogenerated,
): Promise<ProcessedSidebarItem[]> {
const generatedItems = await sidebarItemsGenerator({
item,
numberPrefixParser,
defaultSidebarItemsGenerator: DefaultSidebarItemsGenerator,
isCategoryIndex,
...getSidebarItemsGeneratorDocsAndVersion(),
categoriesMetadata,
});
// Process again... weird but sidebar item generated might generate some
// auto-generated items?
// TODO repeatedly process & unwrap autogenerated items until there are no
// more autogenerated items, or when loop count (e.g. 10) is reached
return processItems(generatedItems);
}
async function processItem(
item: NormalizedSidebarItem,
): Promise<ProcessedSidebarItem[]> {
if (item.type === 'category') {
return [
{
...item,
items: (await Promise.all(item.items.map(processItem))).flat(),
},
];
}
if (item.type === 'autogenerated') {
return processAutoGeneratedItem(item);
}
return [item];
}
async function processItems(
items: NormalizedSidebarItem[],
): Promise<ProcessedSidebarItem[]> {
return (await Promise.all(items.map(processItem))).flat();
}
const processedSidebar = await processItems(unprocessedSidebar);
return processedSidebar;
}
export async function processSidebars(
unprocessedSidebars: NormalizedSidebars,
categoriesMetadata: {[filePath: string]: CategoryMetadataFile},
params: SidebarProcessorParams,
): Promise<ProcessedSidebars> {
const processedSidebars = await combinePromises(
_.mapValues(unprocessedSidebars, (unprocessedSidebar) =>
processSidebar(unprocessedSidebar, categoriesMetadata, params),
),
);
validateSidebars(processedSidebars);
return processedSidebars;
}

View File

@@ -0,0 +1,289 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type {Optional, Required} from 'utility-types';
import type {
NumberPrefixParser,
SidebarOptions,
CategoryIndexMatcher,
DocMetadataBase,
VersionMetadata,
} from '@docusaurus/plugin-content-docs';
import type {Slugger} from '@docusaurus/utils';
// Makes all properties visible when hovering over the type
type Expand<T extends {[x: string]: unknown}> = {[P in keyof T]: T[P]};
export type SidebarItemBase = {
className?: string;
customProps?: {[key: string]: unknown};
};
export type SidebarItemDoc = SidebarItemBase & {
type: 'doc' | 'ref';
label?: string;
id: string;
/**
* This is an internal marker. Items with labels defined in the config needs
* to be translated with JSON
*/
translatable?: true;
};
export type SidebarItemHtml = SidebarItemBase & {
type: 'html';
value: string;
defaultStyle?: boolean;
};
export type SidebarItemLink = SidebarItemBase & {
type: 'link';
href: string;
label: string;
autoAddBaseUrl?: boolean;
description?: string;
};
export type SidebarItemAutogenerated = SidebarItemBase & {
type: 'autogenerated';
dirName: string;
};
type SidebarItemCategoryBase = SidebarItemBase & {
type: 'category';
label: string;
collapsed: boolean;
collapsible: boolean;
description?: string;
};
export type SidebarItemCategoryLinkDoc = {type: 'doc'; id: string};
export type SidebarItemCategoryLinkGeneratedIndexConfig = {
type: 'generated-index';
slug?: string;
title?: string;
description?: string;
image?: string;
keywords?: string | readonly string[];
};
export type SidebarItemCategoryLinkGeneratedIndex = {
type: 'generated-index';
slug: string;
permalink: string;
title?: string;
description?: string;
image?: string;
keywords?: string | readonly string[];
};
export type SidebarItemCategoryLinkConfig =
| SidebarItemCategoryLinkDoc
| SidebarItemCategoryLinkGeneratedIndexConfig;
export type SidebarItemCategoryLink =
| SidebarItemCategoryLinkDoc
| SidebarItemCategoryLinkGeneratedIndex;
// The user-given configuration in sidebars.js, before normalization
export type SidebarItemCategoryConfig = Expand<
Optional<SidebarItemCategoryBase, 'collapsed' | 'collapsible'> & {
items: SidebarCategoriesShorthand | SidebarItemConfig[];
link?: SidebarItemCategoryLinkConfig;
}
>;
export type SidebarCategoriesShorthand = {
[sidebarCategory: string]: SidebarCategoriesShorthand | SidebarItemConfig[];
};
export type SidebarItemConfig =
| Omit<SidebarItemDoc, 'translatable'>
| SidebarItemHtml
| SidebarItemLink
| SidebarItemAutogenerated
| SidebarItemCategoryConfig
| string
| SidebarCategoriesShorthand;
export type SidebarConfig = SidebarCategoriesShorthand | SidebarItemConfig[];
export type SidebarsConfig = {
[sidebarId: string]: SidebarConfig;
};
// Normalized but still has 'autogenerated', which will be handled in processing
export type NormalizedSidebarItemCategory = Expand<
Optional<SidebarItemCategoryBase, 'collapsed' | 'collapsible'> & {
items: NormalizedSidebarItem[];
link?: SidebarItemCategoryLinkConfig;
}
>;
export type NormalizedSidebarItem =
| SidebarItemDoc
| SidebarItemHtml
| SidebarItemLink
| NormalizedSidebarItemCategory
| SidebarItemAutogenerated;
export type NormalizedSidebar = NormalizedSidebarItem[];
export type NormalizedSidebars = {
[sidebarId: string]: NormalizedSidebar;
};
export type ProcessedSidebarItemCategory = Expand<
Optional<SidebarItemCategoryBase, 'collapsed' | 'collapsible'> & {
items: ProcessedSidebarItem[];
link?: SidebarItemCategoryLinkConfig;
}
>;
export type ProcessedSidebarItem =
| SidebarItemDoc
| SidebarItemHtml
| SidebarItemLink
| ProcessedSidebarItemCategory;
export type ProcessedSidebar = ProcessedSidebarItem[];
export type ProcessedSidebars = {
[sidebarId: string]: ProcessedSidebar;
};
export type SidebarItemCategory = Expand<
SidebarItemCategoryBase & {
items: SidebarItem[];
link?: SidebarItemCategoryLink;
}
>;
export type SidebarItemCategoryWithLink = Required<SidebarItemCategory, 'link'>;
export type SidebarItemCategoryWithGeneratedIndex =
SidebarItemCategoryWithLink & {link: SidebarItemCategoryLinkGeneratedIndex};
export type SidebarItem =
| SidebarItemDoc
| SidebarItemHtml
| SidebarItemLink
| SidebarItemCategory;
// A sidebar item that is part of the previous/next ordered navigation
export type SidebarNavigationItem =
| SidebarItemDoc
| SidebarItemCategoryWithLink;
export type Sidebar = SidebarItem[];
export type SidebarItemType = SidebarItem['type'];
export type Sidebars = {
[sidebarId: string]: Sidebar;
};
// Doc links have been resolved to URLs, ready to be passed to the theme
export type PropSidebarItemCategory = Expand<
SidebarItemCategoryBase & {
items: PropSidebarItem[];
href?: string;
// Weird name => it would have been more convenient to have link.unlisted
// Note it is the category link that is unlisted, not the category itself
// We want to prevent users from clicking on an unlisted category link
// We can't use "href: undefined" otherwise sidebar item is not highlighted
linkUnlisted?: boolean;
}
>;
export type PropSidebarItemLink = SidebarItemLink & {
docId?: string;
unlisted?: boolean;
};
export type PropSidebarItemHtml = SidebarItemHtml;
export type PropSidebarItem =
| PropSidebarItemLink
| PropSidebarItemCategory
| PropSidebarItemHtml;
export type PropSidebar = PropSidebarItem[];
export type PropSidebars = {
[sidebarId: string]: PropSidebar;
};
export type PropSidebarBreadcrumbsItem =
| PropSidebarItemLink
| PropSidebarItemCategory;
export type CategoryMetadataFile = {
label?: string;
position?: number;
collapsed?: boolean;
collapsible?: boolean;
className?: string;
link?: SidebarItemCategoryLinkConfig | null;
customProps?: {[key: string]: unknown};
// 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
};
// Reduce API surface for options.sidebarItemsGenerator
// The user-provided generator fn should receive only a subset of metadata
// A change to any of these metadata can be considered as a breaking change
export type SidebarItemsGeneratorDoc = Pick<
DocMetadataBase,
| 'id'
| 'title'
| 'frontMatter'
| 'source'
| 'sourceDirName'
| 'sidebarPosition'
>;
export type SidebarItemsGeneratorVersion = Pick<
VersionMetadata,
'versionName' | 'contentPath'
>;
export type SidebarItemsGeneratorArgs = {
/** The sidebar item with type "autogenerated" to be transformed. */
item: SidebarItemAutogenerated;
/** Useful metadata for the version this sidebar belongs to. */
version: SidebarItemsGeneratorVersion;
/** All the docs of that version (unfiltered). */
docs: SidebarItemsGeneratorDoc[];
/** Number prefix parser configured for this plugin. */
numberPrefixParser: NumberPrefixParser;
/** The default category index matcher which you can override. */
isCategoryIndex: CategoryIndexMatcher;
/**
* Key is the path relative to the doc content directory, value is the
* category metadata file's content.
*/
categoriesMetadata: {[filePath: string]: CategoryMetadataFile};
};
export type SidebarItemsGenerator = (
generatorArgs: SidebarItemsGeneratorArgs,
) => NormalizedSidebar | Promise<NormalizedSidebar>;
export type SidebarItemsGeneratorOption = (
generatorArgs: {
/**
* Useful to re-use/enhance the default sidebar generation logic from
* Docusaurus.
* @see https://github.com/facebook/docusaurus/issues/4640#issuecomment-822292320
*/
defaultSidebarItemsGenerator: SidebarItemsGenerator;
} & SidebarItemsGeneratorArgs,
) => NormalizedSidebar | Promise<NormalizedSidebar>;
export type SidebarProcessorParams = {
sidebarItemsGenerator: SidebarItemsGeneratorOption;
numberPrefixParser: NumberPrefixParser;
docs: DocMetadataBase[];
drafts: DocMetadataBase[];
version: VersionMetadata;
categoryLabelSlugger: Slugger;
sidebarOptions: SidebarOptions;
};

View File

@@ -0,0 +1,520 @@
/**
* 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 _ from 'lodash';
import {toMessageRelativeFilePath} from '@docusaurus/utils';
import type {
Sidebars,
Sidebar,
SidebarItem,
SidebarItemCategory,
SidebarItemLink,
SidebarItemDoc,
SidebarItemType,
SidebarCategoriesShorthand,
SidebarItemConfig,
SidebarItemCategoryWithGeneratedIndex,
SidebarNavigationItem,
} from './types';
import type {
DocMetadataBase,
PropNavigationLink,
VersionMetadata,
} from '@docusaurus/plugin-content-docs';
export function isCategoriesShorthand(
item: SidebarItemConfig,
): item is SidebarCategoriesShorthand {
return typeof item === 'object' && !item.type;
}
export function transformSidebarItems(
sidebar: Sidebar,
updateFn: (item: SidebarItem) => SidebarItem,
): Sidebar {
function transformRecursive(item: SidebarItem): SidebarItem {
if (item.type === 'category') {
return updateFn({
...item,
items: item.items.map(transformRecursive),
});
}
return updateFn(item);
}
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
*/
function flattenSidebarItems(items: SidebarItem[]): SidebarItem[] {
function flattenRecursive(item: SidebarItem): SidebarItem[] {
return item.type === 'category'
? [item, ...item.items.flatMap(flattenRecursive)]
: [item];
}
return items.flatMap(flattenRecursive);
}
function collectSidebarItemsOfType<
Type extends SidebarItemType,
Item extends SidebarItem & {type: SidebarItemType},
>(type: Type, sidebar: Sidebar): Item[] {
return flattenSidebarItems(sidebar).filter(
(item) => item.type === type,
) as Item[];
}
export function collectSidebarDocItems(sidebar: Sidebar): SidebarItemDoc[] {
return collectSidebarItemsOfType('doc', sidebar);
}
export function collectSidebarCategories(
sidebar: Sidebar,
): SidebarItemCategory[] {
return collectSidebarItemsOfType('category', sidebar);
}
export function collectSidebarLinks(sidebar: Sidebar): SidebarItemLink[] {
return collectSidebarItemsOfType('link', sidebar);
}
export function collectSidebarRefs(sidebar: Sidebar): SidebarItemDoc[] {
return collectSidebarItemsOfType('ref', sidebar);
}
// /!\ docId order matters for navigation!
export function collectSidebarDocIds(sidebar: Sidebar): string[] {
return flattenSidebarItems(sidebar).flatMap((item) => {
if (item.type === 'category') {
return item.link?.type === 'doc' ? [item.link.id] : [];
}
if (item.type === 'doc') {
return [item.id];
}
return [];
});
}
export function collectSidebarNavigation(
sidebar: Sidebar,
): SidebarNavigationItem[] {
return flattenSidebarItems(sidebar).flatMap((item) => {
if (item.type === 'category' && item.link) {
return [item as SidebarNavigationItem];
}
if (item.type === 'doc') {
return [item];
}
return [];
});
}
export function collectSidebarsDocIds(sidebars: Sidebars): {
[sidebarId: string]: string[];
} {
return _.mapValues(sidebars, collectSidebarDocIds);
}
export function collectSidebarsNavigations(sidebars: Sidebars): {
[sidebarId: string]: SidebarNavigationItem[];
} {
return _.mapValues(sidebars, collectSidebarNavigation);
}
export type SidebarNavigation = {
sidebarName: string | undefined;
previous: SidebarNavigationItem | undefined;
next: SidebarNavigationItem | undefined;
};
// A convenient and performant way to query the sidebars content
export type SidebarsUtils = {
sidebars: Sidebars;
getFirstDocIdOfFirstSidebar: () => string | undefined;
getSidebarNameByDocId: (docId: string) => string | undefined;
getDocNavigation: (params: {
docId: string;
displayedSidebar: string | null | undefined;
unlistedIds: Set<string>;
}) => SidebarNavigation;
getCategoryGeneratedIndexList: () => SidebarItemCategoryWithGeneratedIndex[];
getCategoryGeneratedIndexNavigation: (
categoryGeneratedIndexPermalink: string,
) => SidebarNavigation;
/**
* This function may return undefined. This is usually a user mistake, because
* it means this sidebar will never be displayed; however, we can still use
* `displayed_sidebar` to make it displayed. Pretty weird but valid use-case
*/
getFirstLink: (sidebarId: string) =>
| {
type: 'doc';
id: string;
label: string;
}
| {
type: 'generated-index';
permalink: string;
label: string;
}
| undefined;
checkLegacyVersionedSidebarNames: ({
versionMetadata,
}: {
sidebarFilePath: string;
versionMetadata: VersionMetadata;
}) => void;
checkSidebarsDocIds: ({
allDocIds,
sidebarFilePath,
versionMetadata,
}: {
allDocIds: string[];
sidebarFilePath: string;
versionMetadata: VersionMetadata;
}) => void;
};
export function createSidebarsUtils(sidebars: Sidebars): SidebarsUtils {
const sidebarNameToDocIds = collectSidebarsDocIds(sidebars);
const sidebarNameToNavigationItems = collectSidebarsNavigations(sidebars);
// Reverse mapping
const docIdToSidebarName = Object.fromEntries(
Object.entries(sidebarNameToDocIds).flatMap(([sidebarName, docIds]) =>
docIds.map((docId) => [docId, sidebarName]),
),
);
function getFirstDocIdOfFirstSidebar(): string | undefined {
return Object.values(sidebarNameToDocIds)[0]?.[0];
}
function getSidebarNameByDocId(docId: string): string | undefined {
return docIdToSidebarName[docId];
}
function emptySidebarNavigation(): SidebarNavigation {
return {
sidebarName: undefined,
previous: undefined,
next: undefined,
};
}
function getDocNavigation({
docId,
displayedSidebar,
unlistedIds,
}: {
docId: string;
displayedSidebar: string | null | undefined;
unlistedIds: Set<string>;
}): SidebarNavigation {
const sidebarName =
displayedSidebar === undefined
? getSidebarNameByDocId(docId)
: displayedSidebar;
if (!sidebarName) {
return emptySidebarNavigation();
}
let navigationItems = sidebarNameToNavigationItems[sidebarName];
if (!navigationItems) {
throw new Error(
`Doc with ID ${docId} wants to display sidebar ${sidebarName} but a sidebar with this name doesn't exist`,
);
}
// Filter unlisted items from navigation
navigationItems = navigationItems.filter((item) => {
if (item.type === 'doc' && unlistedIds.has(item.id)) {
return false;
}
if (
item.type === 'category' &&
item.link.type === 'doc' &&
unlistedIds.has(item.link.id)
) {
return false;
}
return true;
});
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};
}
return {
sidebarName,
previous: navigationItems[currentItemIndex - 1],
next: navigationItems[currentItemIndex + 1],
};
}
function getCategoryGeneratedIndexList(): SidebarItemCategoryWithGeneratedIndex[] {
return Object.values(sidebarNameToNavigationItems)
.flat()
.flatMap((item) => {
if (item.type === 'category' && item.link.type === 'generated-index') {
return [item as SidebarItemCategoryWithGeneratedIndex];
}
return [];
});
}
/**
* We identity the category generated index by its permalink (should be
* unique). More reliable than using object identity
*/
function getCategoryGeneratedIndexNavigation(
categoryGeneratedIndexPermalink: string,
): SidebarNavigation {
function isCurrentCategoryGeneratedIndexItem(
item: SidebarNavigationItem,
): boolean {
return (
item.type === 'category' &&
item.link.type === 'generated-index' &&
item.link.permalink === categoryGeneratedIndexPermalink
);
}
const sidebarName = Object.entries(sidebarNameToNavigationItems).find(
([, navigationItems]) =>
navigationItems.find(isCurrentCategoryGeneratedIndexItem),
)![0];
const navigationItems = sidebarNameToNavigationItems[sidebarName]!;
const currentItemIndex = navigationItems.findIndex(
isCurrentCategoryGeneratedIndexItem,
);
return {
sidebarName,
previous: navigationItems[currentItemIndex - 1],
next: navigationItems[currentItemIndex + 1],
};
}
// TODO remove in Docusaurus v4
function getLegacyVersionedPrefix(versionMetadata: VersionMetadata): string {
return `version-${versionMetadata.versionName}/`;
}
// In early v2, sidebar names used to be versioned
// example: "version-2.0.0-alpha.66/my-sidebar-name"
// In v3 it's not the case anymore and we throw an error to explain
// TODO remove in Docusaurus v4
function checkLegacyVersionedSidebarNames({
versionMetadata,
sidebarFilePath,
}: {
versionMetadata: VersionMetadata;
sidebarFilePath: string;
}): void {
const illegalPrefix = getLegacyVersionedPrefix(versionMetadata);
const legacySidebarNames = Object.keys(sidebars).filter((sidebarName) =>
sidebarName.startsWith(illegalPrefix),
);
if (legacySidebarNames.length > 0) {
throw new Error(
`Invalid sidebar file at "${toMessageRelativeFilePath(
sidebarFilePath,
)}".
These legacy versioned sidebar names are not supported anymore in Docusaurus v3:
- ${legacySidebarNames.sort().join('\n- ')}
The sidebar names you should now use are:
- ${legacySidebarNames
.sort()
.map((legacyName) => legacyName.split('/').splice(1).join('/'))
.join('\n- ')}
Please remove the "${illegalPrefix}" prefix from your versioned sidebar file.
This breaking change is documented on Docusaurus v3 release notes: https://docusaurus.io/blog/releases/3.0
`,
);
}
}
// throw a better error message for Docusaurus v3 breaking change
// TODO this can be removed in Docusaurus v4
function handleLegacyVersionedDocIds({
invalidDocIds,
sidebarFilePath,
versionMetadata,
}: {
invalidDocIds: string[];
sidebarFilePath: string;
versionMetadata: VersionMetadata;
}) {
const illegalPrefix = getLegacyVersionedPrefix(versionMetadata);
// In older v2.0 alpha/betas, versioned docs had a legacy versioned prefix
// Example: "version-1.4/my-doc-id"
//
const legacyVersionedDocIds = invalidDocIds.filter((docId) =>
docId.startsWith(illegalPrefix),
);
if (legacyVersionedDocIds.length > 0) {
throw new Error(
`Invalid sidebar file at "${toMessageRelativeFilePath(
sidebarFilePath,
)}".
These legacy versioned document ids are not supported anymore in Docusaurus v3:
- ${legacyVersionedDocIds.sort().join('\n- ')}
The document ids you should now use are:
- ${legacyVersionedDocIds
.sort()
.map((legacyId) => legacyId.split('/').splice(1).join('/'))
.join('\n- ')}
Please remove the "${illegalPrefix}" prefix from your versioned sidebar file.
This breaking change is documented on Docusaurus v3 release notes: https://docusaurus.io/blog/releases/3.0
`,
);
}
}
function checkSidebarsDocIds({
allDocIds,
sidebarFilePath,
versionMetadata,
}: {
allDocIds: string[];
sidebarFilePath: string;
versionMetadata: VersionMetadata;
}) {
const allSidebarDocIds = Object.values(sidebarNameToDocIds).flat();
const invalidDocIds = _.difference(allSidebarDocIds, allDocIds);
if (invalidDocIds.length > 0) {
handleLegacyVersionedDocIds({
invalidDocIds,
sidebarFilePath,
versionMetadata,
});
throw new Error(
`Invalid sidebar file at "${toMessageRelativeFilePath(
sidebarFilePath,
)}".
These sidebar document ids do not exist:
- ${invalidDocIds.sort().join('\n- ')}
Available document ids are:
- ${_.uniq(allDocIds).sort().join('\n- ')}
`,
);
}
}
function getFirstLink(sidebar: Sidebar):
| {
type: 'doc';
id: string;
label: string;
}
| {
type: 'generated-index';
permalink: string;
label: string;
}
| undefined {
for (const item of sidebar) {
if (item.type === 'doc') {
return {
type: 'doc',
id: item.id,
label: item.label ?? item.id,
};
} else if (item.type === 'category') {
if (item.link?.type === 'doc') {
return {
type: 'doc',
id: item.link.id,
label: item.label,
};
} else if (item.link?.type === 'generated-index') {
return {
type: 'generated-index',
permalink: item.link.permalink,
label: item.label,
};
}
const firstSubItem = getFirstLink(item.items);
if (firstSubItem) {
return firstSubItem;
}
}
}
return undefined;
}
return {
sidebars,
getFirstDocIdOfFirstSidebar,
getSidebarNameByDocId,
getDocNavigation,
getCategoryGeneratedIndexList,
getCategoryGeneratedIndexNavigation,
checkLegacyVersionedSidebarNames,
checkSidebarsDocIds,
getFirstLink: (id) => getFirstLink(sidebars[id]!),
};
}
export function toDocNavigationLink(doc: DocMetadataBase): PropNavigationLink {
const {
title,
permalink,
frontMatter: {
pagination_label: paginationLabel,
sidebar_label: sidebarLabel,
},
} = doc;
return {title: paginationLabel ?? sidebarLabel ?? title, permalink};
}
export function toNavigationLink(
navigationItem: SidebarNavigationItem | undefined,
docsById: {[docId: string]: DocMetadataBase},
): PropNavigationLink | undefined {
function getDocById(docId: string) {
const doc = docsById[docId];
if (!doc) {
throw new Error(
`Can't create navigation link: no doc found with id=${docId}`,
);
}
return doc;
}
if (!navigationItem) {
return undefined;
}
if (navigationItem.type === 'category') {
return navigationItem.link.type === 'doc'
? toDocNavigationLink(getDocById(navigationItem.link.id))
: {
title: navigationItem.label,
permalink: navigationItem.link.permalink,
};
}
return toDocNavigationLink(getDocById(navigationItem.id));
}

View File

@@ -0,0 +1,186 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {Joi, URISchema} from '@docusaurus/utils-validation';
import type {
SidebarItemConfig,
SidebarItemBase,
SidebarItemAutogenerated,
SidebarItemDoc,
SidebarItemHtml,
SidebarItemLink,
SidebarItemCategoryConfig,
SidebarItemCategoryLink,
SidebarItemCategoryLinkDoc,
SidebarItemCategoryLinkGeneratedIndex,
NormalizedSidebars,
NormalizedSidebarItem,
NormalizedSidebarItemCategory,
CategoryMetadataFile,
} from './types';
// 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
const sidebarItemBaseSchema = Joi.object<SidebarItemBase>({
className: Joi.string(),
customProps: Joi.object().unknown(),
});
const sidebarItemAutogeneratedSchema =
sidebarItemBaseSchema.append<SidebarItemAutogenerated>({
type: 'autogenerated',
dirName: Joi.string()
.required()
.pattern(/^[^/](?:.*[^/])?$/)
.message(
'"dirName" must be a dir path relative to the docs folder root, and should not start or end with slash',
),
});
const sidebarItemDocSchema = sidebarItemBaseSchema.append<SidebarItemDoc>({
type: Joi.string().valid('doc', 'ref').required(),
id: Joi.string().required(),
label: Joi.string(),
translatable: Joi.boolean(),
});
const sidebarItemHtmlSchema = sidebarItemBaseSchema.append<SidebarItemHtml>({
type: 'html',
value: Joi.string().required(),
defaultStyle: Joi.boolean(),
});
const sidebarItemLinkSchema = sidebarItemBaseSchema.append<SidebarItemLink>({
type: 'link',
href: URISchema.required(),
autoAddBaseUrl: Joi.boolean(),
label: Joi.string()
.required()
.messages({'any.unknown': '"label" must be a string'}),
description: Joi.string().optional().messages({
'any.unknown': '"description" must be a string',
}),
});
const sidebarItemCategoryLinkSchema = Joi.object<SidebarItemCategoryLink>()
.allow(null)
.when('.type', {
switch: [
{
is: 'doc',
then: Joi.object<SidebarItemCategoryLinkDoc>({
type: 'doc',
id: Joi.string().required(),
}),
},
{
is: 'generated-index',
then: Joi.object<SidebarItemCategoryLinkGeneratedIndex>({
type: 'generated-index',
slug: Joi.string().optional(),
// 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(),
keywords: [Joi.string(), Joi.array().items(Joi.string())],
}),
},
{
is: Joi.required(),
then: Joi.forbidden().messages({
'any.unknown': 'Unknown sidebar category link type "{.type}".',
}),
},
],
});
const sidebarItemCategorySchema =
sidebarItemBaseSchema.append<SidebarItemCategoryConfig>({
type: 'category',
label: Joi.string()
.required()
.messages({'any.unknown': '"label" must be a string'}),
items: Joi.array()
.required()
.messages({'any.unknown': '"items" must be an array'}),
// TODO: Joi doesn't allow mutual recursion. See https://github.com/sideway/joi/issues/2611
// .items(Joi.link('#sidebarItemSchema')),
link: sidebarItemCategoryLinkSchema,
collapsed: Joi.boolean().messages({
'any.unknown': '"collapsed" must be a boolean',
}),
collapsible: Joi.boolean().messages({
'any.unknown': '"collapsible" must be a boolean',
}),
description: Joi.string().optional().messages({
'any.unknown': '"description" must be a string',
}),
});
const sidebarItemSchema = Joi.object<SidebarItemConfig>().when('.type', {
switch: [
{is: 'link', then: sidebarItemLinkSchema},
{
is: Joi.string().valid('doc', 'ref').required(),
then: sidebarItemDocSchema,
},
{is: 'html', then: sidebarItemHtmlSchema},
{is: 'autogenerated', then: sidebarItemAutogeneratedSchema},
{is: 'category', then: sidebarItemCategorySchema},
{
is: Joi.any().required(),
then: Joi.forbidden().messages({
'any.unknown': 'Unknown sidebar item type "{.type}".',
}),
},
],
});
// .id('sidebarItemSchema');
function validateSidebarItem(
item: unknown,
): asserts item is NormalizedSidebarItem {
// TODO: remove once with proper Joi support
// Because we can't use Joi to validate nested items (see above), we do it
// manually
Joi.assert(item, sidebarItemSchema);
if ((item as NormalizedSidebarItem).type === 'category') {
(item as NormalizedSidebarItemCategory).items.forEach(validateSidebarItem);
}
}
export function validateSidebars(sidebars: {
[sidebarId: string]: unknown;
}): asserts sidebars is NormalizedSidebars {
Object.values(sidebars as NormalizedSidebars).forEach((sidebar) => {
sidebar.forEach(validateSidebarItem);
});
}
const categoryMetadataFileSchema = Joi.object<CategoryMetadataFile>({
label: Joi.string(),
position: Joi.number(),
collapsed: Joi.boolean(),
collapsible: Joi.boolean(),
className: Joi.string(),
link: sidebarItemCategoryLinkSchema,
customProps: Joi.object().unknown(),
});
export function validateCategoryMetadataFile(
unsafeContent: unknown,
): CategoryMetadataFile {
const {error, value} = categoryMetadataFileSchema.validate(unsafeContent);
if (error) {
throw error;
}
return value;
}

View File

@@ -0,0 +1,83 @@
/**
* 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 {
addLeadingSlash,
addTrailingSlash,
isValidPathname,
resolvePathname,
} from '@docusaurus/utils';
import {
DefaultNumberPrefixParser,
stripPathNumberPrefixes,
} from './numberPrefix';
import {isCategoryIndex, toCategoryIndexMatcherParam} from './docs';
import type {
NumberPrefixParser,
DocMetadataBase,
} from '@docusaurus/plugin-content-docs';
export default function getSlug({
baseID,
frontMatterSlug,
source,
sourceDirName,
stripDirNumberPrefixes = true,
numberPrefixParser = DefaultNumberPrefixParser,
}: {
baseID: string;
frontMatterSlug?: string;
source: DocMetadataBase['source'];
sourceDirName: DocMetadataBase['sourceDirName'];
stripDirNumberPrefixes?: boolean;
numberPrefixParser?: NumberPrefixParser;
}): string {
function getDirNameSlug(): string {
const dirNameStripped = stripDirNumberPrefixes
? stripPathNumberPrefixes(sourceDirName, numberPrefixParser)
: sourceDirName;
const resolveDirname =
sourceDirName === '.'
? '/'
: addLeadingSlash(addTrailingSlash(dirNameStripped));
return resolveDirname;
}
function computeSlug(): string {
if (frontMatterSlug?.startsWith('/')) {
return frontMatterSlug;
}
const dirNameSlug = getDirNameSlug();
if (
!frontMatterSlug &&
isCategoryIndex(toCategoryIndexMatcherParam({source, sourceDirName}))
) {
return dirNameSlug;
}
const baseSlug = frontMatterSlug ?? baseID;
return resolvePathname(baseSlug, getDirNameSlug());
}
function ensureValidSlug(slug: string): string {
if (!isValidPathname(slug)) {
throw new Error(
`We couldn't compute a valid slug for document with ID "${baseID}" in "${sourceDirName}" directory.
The slug we computed looks invalid: ${slug}.
Maybe your slug front matter is incorrect or there are special characters in the file path?
By using front matter to set a custom slug, you should be able to fix this error:
---
slug: /my/customDocPath
---
`,
);
}
return slug;
}
return ensureValidSlug(computeSlug());
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import _ from 'lodash';
import {getTagVisibility, groupTaggedItems} from '@docusaurus/utils';
import type {VersionTags} from './types';
import type {DocMetadata} from '@docusaurus/plugin-content-docs';
export function getVersionTags(docs: DocMetadata[]): VersionTags {
const groups = groupTaggedItems(docs, (doc) => doc.tags);
return _.mapValues(groups, ({tag, items: tagDocs}) => {
const tagVisibility = getTagVisibility({
items: tagDocs,
isUnlisted: (item) => item.unlisted,
});
return {
label: tag.label,
docIds: tagVisibility.listedItems.map((item) => item.id),
permalink: tag.permalink,
unlisted: tagVisibility.unlisted,
};
});
}

View File

@@ -0,0 +1,266 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import _ from 'lodash';
import {mergeTranslations} from '@docusaurus/utils';
import {CURRENT_VERSION_NAME} from './constants';
import {
collectSidebarCategories,
transformSidebarItems,
collectSidebarLinks,
collectSidebarDocItems,
collectSidebarRefs,
} from './sidebars/utils';
import type {
LoadedVersion,
LoadedContent,
} from '@docusaurus/plugin-content-docs';
import type {
Sidebar,
SidebarItemCategory,
SidebarItemCategoryLink,
Sidebars,
} from './sidebars/types';
import type {
TranslationFileContent,
TranslationFile,
TranslationMessage,
} from '@docusaurus/types';
function getVersionFileName(versionName: string): string {
if (versionName === CURRENT_VERSION_NAME) {
return versionName;
}
// I don't like this "version-" prefix,
// but it's for consistency with site/versioned_docs
return `version-${versionName}`;
}
function getSidebarTranslationFileContent(
sidebar: Sidebar,
sidebarName: string,
): TranslationFileContent {
type TranslationMessageEntry = [string, TranslationMessage];
const categories = collectSidebarCategories(sidebar);
const categoryContent: TranslationFileContent = Object.fromEntries(
categories.flatMap((category) => {
const entries: TranslationMessageEntry[] = [];
entries.push([
`sidebar.${sidebarName}.category.${category.label}`,
{
message: category.label,
description: `The label for category ${category.label} in sidebar ${sidebarName}`,
},
]);
if (category.link?.type === 'generated-index') {
if (category.link.title) {
entries.push([
`sidebar.${sidebarName}.category.${category.label}.link.generated-index.title`,
{
message: category.link.title,
description: `The generated-index page title for category ${category.label} in sidebar ${sidebarName}`,
},
]);
}
if (category.link.description) {
entries.push([
`sidebar.${sidebarName}.category.${category.label}.link.generated-index.description`,
{
message: category.link.description,
description: `The generated-index page description for category ${category.label} in sidebar ${sidebarName}`,
},
]);
}
}
return entries;
}),
);
const links = collectSidebarLinks(sidebar);
const linksContent: TranslationFileContent = Object.fromEntries(
links.map((link) => [
`sidebar.${sidebarName}.link.${link.label}`,
{
message: link.label,
description: `The label for link ${link.label} in sidebar ${sidebarName}, linking to ${link.href}`,
},
]),
);
const docs = collectSidebarDocItems(sidebar)
.concat(collectSidebarRefs(sidebar))
.filter((item) => item.translatable);
const docLinksContent: TranslationFileContent = Object.fromEntries(
docs.map((doc) => [
`sidebar.${sidebarName}.doc.${doc.label!}`,
{
message: doc.label!,
description: `The label for the doc item ${doc.label!} in sidebar ${sidebarName}, linking to the doc ${
doc.id
}`,
},
]),
);
return mergeTranslations([categoryContent, linksContent, docLinksContent]);
}
function translateSidebar({
sidebar,
sidebarName,
sidebarsTranslations,
}: {
sidebar: Sidebar;
sidebarName: string;
sidebarsTranslations: TranslationFileContent;
}): Sidebar {
function transformSidebarCategoryLink(
category: SidebarItemCategory,
): SidebarItemCategoryLink | undefined {
if (!category.link) {
return undefined;
}
if (category.link.type === 'generated-index') {
const title =
sidebarsTranslations[
`sidebar.${sidebarName}.category.${category.label}.link.generated-index.title`
]?.message ?? category.link.title;
const description =
sidebarsTranslations[
`sidebar.${sidebarName}.category.${category.label}.link.generated-index.description`
]?.message ?? category.link.description;
return {
...category.link,
title,
description,
};
}
return category.link;
}
return transformSidebarItems(sidebar, (item) => {
if (item.type === 'category') {
const link = transformSidebarCategoryLink(item);
return {
...item,
label:
sidebarsTranslations[`sidebar.${sidebarName}.category.${item.label}`]
?.message ?? item.label,
...(link && {link}),
};
}
if (item.type === 'link') {
return {
...item,
label:
sidebarsTranslations[`sidebar.${sidebarName}.link.${item.label}`]
?.message ?? item.label,
};
}
if ((item.type === 'doc' || item.type === 'ref') && item.translatable) {
return {
...item,
label:
sidebarsTranslations[`sidebar.${sidebarName}.doc.${item.label!}`]
?.message ?? item.label,
};
}
return item;
});
}
function getSidebarsTranslations(
version: LoadedVersion,
): TranslationFileContent {
return mergeTranslations(
Object.entries(version.sidebars).map(([sidebarName, sidebar]) =>
getSidebarTranslationFileContent(sidebar, sidebarName),
),
);
}
function translateSidebars(
version: LoadedVersion,
sidebarsTranslations: TranslationFileContent,
): Sidebars {
return _.mapValues(version.sidebars, (sidebar, sidebarName) =>
translateSidebar({
sidebar,
sidebarName,
sidebarsTranslations,
}),
);
}
function getVersionTranslationFiles(version: LoadedVersion): TranslationFile[] {
const versionTranslations: TranslationFileContent = {
'version.label': {
message: version.label,
description: `The label for version ${version.versionName}`,
},
};
const sidebarsTranslations: TranslationFileContent =
getSidebarsTranslations(version);
return [
{
path: getVersionFileName(version.versionName),
content: mergeTranslations([versionTranslations, sidebarsTranslations]),
},
];
}
function translateVersion(
version: LoadedVersion,
translationFiles: {[fileName: string]: TranslationFile},
): LoadedVersion {
const versionTranslations =
translationFiles[getVersionFileName(version.versionName)]!.content;
return {
...version,
label: versionTranslations['version.label']?.message ?? version.label,
sidebars: translateSidebars(version, versionTranslations),
};
}
function getVersionsTranslationFiles(
versions: LoadedVersion[],
): TranslationFile[] {
return versions.flatMap(getVersionTranslationFiles);
}
function translateVersions(
versions: LoadedVersion[],
translationFiles: {[fileName: string]: TranslationFile},
): LoadedVersion[] {
return versions.map((version) => translateVersion(version, translationFiles));
}
export function getLoadedContentTranslationFiles(
loadedContent: LoadedContent,
): TranslationFile[] {
return getVersionsTranslationFiles(loadedContent.loadedVersions);
}
export function translateLoadedContent(
loadedContent: LoadedContent,
translationFiles: TranslationFile[],
): LoadedContent {
const translationFilesMap: {[fileName: string]: TranslationFile} = _.keyBy(
translationFiles,
(f) => f.path,
);
return {
loadedVersions: translateVersions(
loadedContent.loadedVersions,
translationFilesMap,
),
};
}

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import type {BrokenMarkdownLink, Tag} from '@docusaurus/utils';
import type {
VersionMetadata,
LoadedVersion,
CategoryGeneratedIndexMetadata,
} from '@docusaurus/plugin-content-docs';
import type {SidebarsUtils} from './sidebars/utils';
export type DocFile = {
contentPath: string; // /!\ may be localized
filePath: string; // /!\ may be localized
source: string;
content: string;
};
export type SourceToPermalink = {
[source: string]: string;
};
export type VersionTag = Tag & {
/** All doc ids having this tag. */
docIds: string[];
unlisted: boolean;
};
export type VersionTags = {
[permalink: string]: VersionTag;
};
export type FullVersion = LoadedVersion & {
sidebarsUtils: SidebarsUtils;
categoryGeneratedIndices: CategoryGeneratedIndexMetadata[];
};
export type DocBrokenMarkdownLink = BrokenMarkdownLink<VersionMetadata>;
export type DocsMarkdownOption = {
versionsMetadata: VersionMetadata[];
siteDir: string;
sourceToPermalink: SourceToPermalink;
onBrokenMarkdownLink: (brokenMarkdownLink: DocBrokenMarkdownLink) => void;
};

View File

@@ -0,0 +1,216 @@
/**
* 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 path from 'path';
import fs from 'fs-extra';
import {getPluginI18nPath, DEFAULT_PLUGIN_ID} from '@docusaurus/utils';
import {
VERSIONS_JSON_FILE,
VERSIONED_DOCS_DIR,
VERSIONED_SIDEBARS_DIR,
CURRENT_VERSION_NAME,
} from '../constants';
import {validateVersionNames} from './validation';
import type {
PluginOptions,
VersionMetadata,
} from '@docusaurus/plugin-content-docs';
import type {VersionContext} from './index';
/** Add a prefix like `community_version-1.0.0`. No-op for default instance. */
function addPluginIdPrefix(fileOrDir: string, pluginId: string): string {
return pluginId === DEFAULT_PLUGIN_ID
? fileOrDir
: `${pluginId}_${fileOrDir}`;
}
/** `[siteDir]/community_versioned_docs/version-1.0.0` */
export function getVersionDocsDirPath(
siteDir: string,
pluginId: string,
versionName: string,
): string {
return path.join(
siteDir,
addPluginIdPrefix(VERSIONED_DOCS_DIR, pluginId),
`version-${versionName}`,
);
}
/** `[siteDir]/community_versioned_sidebars/version-1.0.0-sidebars.json` */
export function getVersionSidebarsPath(
siteDir: string,
pluginId: string,
versionName: string,
): string {
return path.join(
siteDir,
addPluginIdPrefix(VERSIONED_SIDEBARS_DIR, pluginId),
`version-${versionName}-sidebars.json`,
);
}
export function getDocsDirPathLocalized({
localizationDir,
pluginId,
versionName,
}: {
localizationDir: string;
pluginId: string;
versionName: string;
}): string {
return getPluginI18nPath({
localizationDir,
pluginName: 'docusaurus-plugin-content-docs',
pluginId,
subPaths: [
versionName === CURRENT_VERSION_NAME
? CURRENT_VERSION_NAME
: `version-${versionName}`,
],
});
}
/** `community` => `[siteDir]/community_versions.json` */
export function getVersionsFilePath(siteDir: string, pluginId: string): string {
return path.join(siteDir, addPluginIdPrefix(VERSIONS_JSON_FILE, pluginId));
}
/**
* Reads the plugin's respective `versions.json` file, and returns its content.
*
* @throws Throws if validation fails, i.e. `versions.json` doesn't contain an
* array of valid version names.
*/
export async function readVersionsFile(
siteDir: string,
pluginId: string,
): Promise<string[] | null> {
const versionsFilePath = getVersionsFilePath(siteDir, pluginId);
if (await fs.pathExists(versionsFilePath)) {
const content: unknown = await fs.readJSON(versionsFilePath);
validateVersionNames(content);
return content;
}
return null;
}
/**
* Reads the `versions.json` file, and returns an ordered list of version names.
*
* - If `disableVersioning` is turned on, it will return `["current"]` (requires
* `includeCurrentVersion` to be true);
* - If `includeCurrentVersion` is turned on, "current" will be inserted at the
* beginning, if not already there.
*
* You need to use {@link filterVersions} after this.
*
* @throws Throws an error if `disableVersioning: true` but `versions.json`
* doesn't exist (i.e. site is not versioned)
* @throws Throws an error if versions list is empty (empty `versions.json` or
* `disableVersioning` is true, and not including current version)
*/
export async function readVersionNames(
siteDir: string,
options: PluginOptions,
): Promise<string[]> {
const versionFileContent = await readVersionsFile(siteDir, options.id);
if (!versionFileContent && options.disableVersioning) {
throw new Error(
`Docs: using "disableVersioning: true" option on a non-versioned site does not make sense.`,
);
}
const versions = options.disableVersioning ? [] : versionFileContent ?? [];
// We add the current version at the beginning, unless:
// - user don't want to; or
// - it's already been explicitly added to versions.json
if (
options.includeCurrentVersion &&
!versions.includes(CURRENT_VERSION_NAME)
) {
versions.unshift(CURRENT_VERSION_NAME);
}
if (versions.length === 0) {
throw new Error(
`It is not possible to use docs without any version. No version is included because you have requested to not include ${path.resolve(
options.path,
)} through "includeCurrentVersion: false", while ${
options.disableVersioning
? 'versioning is disabled with "disableVersioning: true"'
: `the versions file is empty/non-existent`
}.`,
);
}
return versions;
}
/**
* Gets the path-related version metadata.
*
* @throws Throws if the resolved docs folder or sidebars file doesn't exist.
* Does not throw if a versioned sidebar is missing (since we don't create empty
* files).
*/
export async function getVersionMetadataPaths({
versionName,
context,
options,
}: VersionContext): Promise<
Pick<
VersionMetadata,
'contentPath' | 'contentPathLocalized' | 'sidebarFilePath'
>
> {
const isCurrent = versionName === CURRENT_VERSION_NAME;
const contentPathLocalized = getDocsDirPathLocalized({
localizationDir: context.localizationDir,
pluginId: options.id,
versionName,
});
const contentPath = isCurrent
? path.resolve(context.siteDir, options.path)
: getVersionDocsDirPath(context.siteDir, options.id, versionName);
const sidebarFilePath = isCurrent
? options.sidebarPath
: getVersionSidebarsPath(context.siteDir, options.id, versionName);
if (!(await fs.pathExists(contentPath))) {
throw new Error(
`The docs folder does not exist for version "${versionName}". A docs folder is expected to be found at ${path.relative(
context.siteDir,
contentPath,
)}.`,
);
}
// 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 (
versionName === CURRENT_VERSION_NAME &&
typeof sidebarFilePath === 'string' &&
!(await fs.pathExists(sidebarFilePath))
) {
throw new Error(`The path to the sidebar file does not exist at "${path.relative(
context.siteDir,
sidebarFilePath,
)}".
Please set the docs "sidebarPath" field in your config file to:
- a sidebars path that exists
- false: to disable the sidebar
- undefined: for Docusaurus to generate it automatically`);
}
return {contentPath, contentPathLocalized, sidebarFilePath};
}

View File

@@ -0,0 +1,270 @@
/**
* 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 path from 'path';
import {normalizeUrl, posixPath} from '@docusaurus/utils';
import {CURRENT_VERSION_NAME} from '../constants';
import {validateVersionsOptions} from './validation';
import {
getDocsDirPathLocalized,
getVersionMetadataPaths,
readVersionNames,
} from './files';
import {createSidebarsUtils} from '../sidebars/utils';
import {getCategoryGeneratedIndexMetadataList} from '../categoryGeneratedIndex';
import type {FullVersion} from '../types';
import type {LoadContext} from '@docusaurus/types';
import type {
LoadedVersion,
PluginOptions,
VersionBanner,
VersionMetadata,
} from '@docusaurus/plugin-content-docs';
export type VersionContext = {
/** The version name to get banner of. */
versionName: string;
/** All versions, ordered from newest to oldest. */
versionNames: string[];
lastVersionName: string;
context: LoadContext;
options: PluginOptions;
};
function getVersionEditUrls({
contentPath,
contentPathLocalized,
context,
options,
}: Pick<VersionMetadata, 'contentPath' | 'contentPathLocalized'> & {
context: LoadContext;
options: PluginOptions;
}): Pick<VersionMetadata, 'editUrl' | 'editUrlLocalized'> {
// If the user is using the functional form of editUrl,
// she has total freedom and we can't compute a "version edit url"
if (!options.editUrl || typeof options.editUrl === 'function') {
return {editUrl: undefined, editUrlLocalized: undefined};
}
const editDirPath = options.editCurrentVersion ? options.path : contentPath;
const editDirPathLocalized = options.editCurrentVersion
? getDocsDirPathLocalized({
localizationDir: context.localizationDir,
versionName: CURRENT_VERSION_NAME,
pluginId: options.id,
})
: contentPathLocalized;
const versionPathSegment = posixPath(
path.relative(context.siteDir, path.resolve(context.siteDir, editDirPath)),
);
const versionPathSegmentLocalized = posixPath(
path.relative(
context.siteDir,
path.resolve(context.siteDir, editDirPathLocalized),
),
);
const editUrl = normalizeUrl([options.editUrl, versionPathSegment]);
const editUrlLocalized = normalizeUrl([
options.editUrl,
versionPathSegmentLocalized,
]);
return {editUrl, editUrlLocalized};
}
/**
* The default version banner depends on the version's relative position to the
* latest version. More recent ones are "unreleased", and older ones are
* "unmaintained".
*/
export function getDefaultVersionBanner({
versionName,
versionNames,
lastVersionName,
}: VersionContext): VersionBanner | null {
// Current version: good, no banner
if (versionName === lastVersionName) {
return null;
}
// Upcoming versions: unreleased banner
if (
versionNames.indexOf(versionName) < versionNames.indexOf(lastVersionName)
) {
return 'unreleased';
}
// Older versions: display unmaintained banner
return 'unmaintained';
}
export function getVersionBanner(
context: VersionContext,
): VersionMetadata['banner'] {
const {versionName, options} = context;
const versionBannerOption = options.versions[versionName]?.banner;
if (versionBannerOption) {
return versionBannerOption === 'none' ? null : versionBannerOption;
}
return getDefaultVersionBanner(context);
}
export function getVersionBadge({
versionName,
versionNames,
options,
}: VersionContext): VersionMetadata['badge'] {
// If site is not versioned or only one version is included
// we don't show the version badge by default
// See https://github.com/facebook/docusaurus/issues/3362
const defaultVersionBadge = versionNames.length !== 1;
return options.versions[versionName]?.badge ?? defaultVersionBadge;
}
export function getVersionNoIndex({
versionName,
options,
}: VersionContext): VersionMetadata['noIndex'] {
return options.versions[versionName]?.noIndex ?? false;
}
function getVersionClassName({
versionName,
options,
}: VersionContext): VersionMetadata['className'] {
const defaultVersionClassName = `docs-version-${versionName}`;
return options.versions[versionName]?.className ?? defaultVersionClassName;
}
function getVersionLabel({
versionName,
options,
}: VersionContext): VersionMetadata['label'] {
const defaultVersionLabel =
versionName === CURRENT_VERSION_NAME ? 'Next' : versionName;
return options.versions[versionName]?.label ?? defaultVersionLabel;
}
function getVersionPathPart({
versionName,
options,
lastVersionName,
}: VersionContext): string {
function getDefaultVersionPathPart() {
if (versionName === lastVersionName) {
return '';
}
return versionName === CURRENT_VERSION_NAME ? 'next' : versionName;
}
return options.versions[versionName]?.path ?? getDefaultVersionPathPart();
}
async function createVersionMetadata(
context: VersionContext,
): Promise<VersionMetadata> {
const {versionName, lastVersionName, options, context: loadContext} = context;
const {sidebarFilePath, contentPath, contentPathLocalized} =
await getVersionMetadataPaths(context);
const versionPathPart = getVersionPathPart(context);
const routePath = normalizeUrl([
loadContext.baseUrl,
options.routeBasePath,
versionPathPart,
]);
const versionEditUrls = getVersionEditUrls({
contentPath,
contentPathLocalized,
context: loadContext,
options,
});
return {
versionName,
label: getVersionLabel(context),
banner: getVersionBanner(context),
badge: getVersionBadge(context),
noIndex: getVersionNoIndex(context),
className: getVersionClassName(context),
path: routePath,
tagsPath: normalizeUrl([routePath, options.tagsBasePath]),
...versionEditUrls,
isLast: versionName === lastVersionName,
routePriority: versionPathPart === '' ? -1 : undefined,
sidebarFilePath,
contentPath,
contentPathLocalized,
};
}
/**
* Filter versions according to provided options (i.e. `onlyIncludeVersions`).
*
* Note: we preserve the order in which versions are provided; the order of the
* `onlyIncludeVersions` array does not matter
*/
export function filterVersions(
versionNamesUnfiltered: string[],
options: PluginOptions,
): string[] {
if (options.onlyIncludeVersions) {
return versionNamesUnfiltered.filter((name) =>
options.onlyIncludeVersions!.includes(name),
);
}
return versionNamesUnfiltered;
}
function getLastVersionName({
versionNames,
options,
}: Pick<VersionContext, 'versionNames' | 'options'>) {
return (
options.lastVersion ??
versionNames.find((name) => name !== CURRENT_VERSION_NAME) ??
CURRENT_VERSION_NAME
);
}
export async function readVersionsMetadata({
context,
options,
}: {
context: LoadContext;
options: PluginOptions;
}): Promise<VersionMetadata[]> {
const allVersionNames = await readVersionNames(context.siteDir, options);
validateVersionsOptions(allVersionNames, options);
const versionNames = filterVersions(allVersionNames, options);
const lastVersionName = getLastVersionName({versionNames, options});
const versionsMetadata = await Promise.all(
versionNames.map((versionName) =>
createVersionMetadata({
versionName,
versionNames,
lastVersionName,
context,
options,
}),
),
);
return versionsMetadata;
}
export function toFullVersion(version: LoadedVersion): FullVersion {
const sidebarsUtils = createSidebarsUtils(version.sidebars);
return {
...version,
sidebarsUtils,
categoryGeneratedIndices: getCategoryGeneratedIndexMetadataList({
docs: version.docs,
sidebarsUtils,
}),
};
}

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