feat: doc tags (same as blog tags) (#3646)

* [v2] tags to doc, same as tags to blog - [IN PROGRESS]

- Addition of plugin-content-docs

- Addition of DocTagsListPage in `docusaurus-theme-classic`

! Error exists for this commit towards the theme aspect and help required.

Commit towards #3434

* docs: make tags list page work

* temp: disable onBrokenLinks

* theme bootstrap: create DocTagsListPage

* DocTagsPage added and functionality too

- individual doc tag page added to show docs for that specific tag

* Added all Docs Tags Link

* add some shared tag utils

* move tag tests to _dogfooding

* fix type

* fix some tests

* fix blog test

* refactor blog post tags handling

* better yaml tag examples

* better dogfood md files

* refactor and factorize theme tag components

* finish DocTagDocListPage

* Extract DocItemFooter + add inline tag list

* minor fix

* better typings

* fix versions.test.ts tests

* add tests for doc tags

* fix tests

* test toTagDocListProp

* move shared theme code to tagUtils

* Add new theme translation keys

* move common theme code to tagUtils + add tests

* update-code-translations should handle theme-common

* update french translation

* revert add translation

* fix pluralization problem in theme.docs.tagDocListPageTitle

* add theme component configuration options

* add more tags tests

* add documentation for docs tagging

Co-authored-by: slorber <lorber.sebastien@gmail.com>
This commit is contained in:
Isaac Philip 2021-08-19 14:01:15 +05:30 committed by GitHub
parent f666de7e59
commit f9c79cbd58
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
81 changed files with 1874 additions and 381 deletions

View file

@ -33,6 +33,11 @@ module.exports = {
},
setupFiles: ['./jest/stylelint-rule-test.js', './jest/polyfills.js'],
moduleNameMapper: {
// TODO we need to allow Jest to resolve core Webpack aliases automatically
'@docusaurus/router': 'react-router-dom',
'@docusaurus/Translate': '@docusaurus/core/lib/client/exports/Translate',
'@docusaurus/Interpolate':
'@docusaurus/core/lib/client/exports/Interpolate',
'@generated/codeTranslations': '<rootDir>/jest/emptyModule.js',
},
};

8
jest/emptyModule.js Normal file
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.
*/
module.exports = {};

View file

@ -9,15 +9,16 @@ import {
JoiFrontMatter as Joi, // Custom instance for frontmatter
URISchema,
validateFrontMatter,
FrontMatterTagsSchema,
} from '@docusaurus/utils-validation';
import {Tag} from './types';
import type {FrontMatterTag} from '@docusaurus/utils';
export type BlogPostFrontMatter = {
/* eslint-disable camelcase */
id?: string;
title?: string;
description?: string;
tags?: (string | Tag)[];
tags?: FrontMatterTag[];
slug?: string;
draft?: boolean;
date?: Date | string; // Yaml automagically convert some string patterns as Date, but not all
@ -38,23 +39,11 @@ export type BlogPostFrontMatter = {
/* eslint-enable camelcase */
};
// NOTE: we don't add any default value on purpose here
// We don't want default values to magically appear in doc metadatas and props
// While the user did not provide those values explicitly
// We use default values in code instead
const BlogTagSchema = Joi.alternatives().try(
Joi.string().required(),
Joi.object<Tag>({
label: Joi.string().required(),
permalink: Joi.string().required(),
}),
);
const BlogFrontMatterSchema = Joi.object<BlogPostFrontMatter>({
id: Joi.string(),
title: Joi.string().allow(''),
description: Joi.string().allow(''),
tags: Joi.array().items(BlogTagSchema),
tags: FrontMatterTagsSchema,
draft: Joi.boolean(),
date: Joi.date().raw(),

View file

@ -16,6 +16,7 @@ import {
BlogPost,
BlogContentPaths,
BlogMarkdownLoaderOptions,
BlogTags,
} from './types';
import {
parseMarkdownFile,
@ -26,6 +27,8 @@ import {
posixPath,
replaceMarkdownLinks,
Globby,
normalizeFrontMatterTags,
groupTaggedItems,
} from '@docusaurus/utils';
import {LoadContext} from '@docusaurus/types';
import {validateBlogPostFrontMatter} from './blogFrontMatter';
@ -43,6 +46,20 @@ export function getSourceToPermalink(
);
}
export function getBlogTags(blogPosts: BlogPost[]): BlogTags {
const groups = groupTaggedItems(
blogPosts,
(blogPost) => blogPost.metadata.tags,
);
return mapValues(groups, (group) => {
return {
name: group.tag.label,
items: group.items.map((item) => item.id),
permalink: group.tag.permalink,
};
});
}
const DATE_FILENAME_REGEX = /^(?<date>\d{4}[-/]\d{1,2}[-/]\d{1,2})[-/]?(?<text>.*?)(\/index)?.mdx?$/;
type ParsedBlogFileName = {
@ -240,6 +257,8 @@ async function processBlogSourceFile(
return undefined;
}
const tagsBasePath = normalizeUrl([baseUrl, options.routeBasePath, 'tags']); // make this configurable?
return {
id: frontMatter.slug ?? title,
metadata: {
@ -250,7 +269,7 @@ async function processBlogSourceFile(
description,
date,
formattedDate,
tags: frontMatter.tags ?? [],
tags: normalizeFrontMatterTags(tagsBasePath, frontMatter.tags),
readingTime: showReadingTime ? readingTime(content).minutes : undefined,
truncated: truncateMarker?.test(content) || false,
},

View file

@ -22,7 +22,7 @@ import {
STATIC_DIR_NAME,
DEFAULT_PLUGIN_ID,
} from '@docusaurus/core/lib/constants';
import {flatten, take, kebabCase} from 'lodash';
import {flatten, take} from 'lodash';
import {
PluginOptions,
@ -51,6 +51,7 @@ import {
generateBlogPosts,
getContentPathList,
getSourceToPermalink,
getBlogTags,
} from './blogUtils';
export default function pluginContentBlog(
@ -65,7 +66,7 @@ export default function pluginContentBlog(
const {
siteDir,
siteConfig: {onBrokenMarkdownLinks},
siteConfig: {onBrokenMarkdownLinks, baseUrl},
generatedFilesDir,
i18n: {currentLocale},
} = context;
@ -151,17 +152,14 @@ export default function pluginContentBlog(
const postsPerPage =
postsPerPageOption === 'ALL' ? totalCount : postsPerPageOption;
const numberOfPages = Math.ceil(totalCount / postsPerPage);
const {
siteConfig: {baseUrl = ''},
} = context;
const basePageUrl = normalizeUrl([baseUrl, routeBasePath]);
const baseBlogUrl = normalizeUrl([baseUrl, routeBasePath]);
const blogListPaginated: BlogPaginated[] = [];
function blogPaginationPermalink(page: number) {
return page > 0
? normalizeUrl([basePageUrl, `page/${page + 1}`])
: basePageUrl;
? normalizeUrl([baseBlogUrl, `page/${page + 1}`])
: baseBlogUrl;
}
for (let page = 0; page < numberOfPages; page += 1) {
@ -186,41 +184,9 @@ export default function pluginContentBlog(
});
}
const blogTags: BlogTags = {};
const tagsPath = normalizeUrl([basePageUrl, 'tags']);
blogPosts.forEach((blogPost) => {
const {tags} = blogPost.metadata;
if (!tags || tags.length === 0) {
// TODO: Extract tags out into a separate plugin.
// eslint-disable-next-line no-param-reassign
blogPost.metadata.tags = [];
return;
}
const blogTags: BlogTags = getBlogTags(blogPosts);
// eslint-disable-next-line no-param-reassign
blogPost.metadata.tags = tags.map((tag) => {
if (typeof tag === 'string') {
const normalizedTag = kebabCase(tag);
const permalink = normalizeUrl([tagsPath, normalizedTag]);
if (!blogTags[normalizedTag]) {
blogTags[normalizedTag] = {
// Will only use the name of the first occurrence of the tag.
name: tag.toLowerCase(),
items: [],
permalink,
};
}
blogTags[normalizedTag].items.push(blogPost.id);
return {
label: tag,
permalink,
};
}
return tag;
});
});
const tagsPath = normalizeUrl([baseBlogUrl, 'tags']);
const blogTagsListPath =
Object.keys(blogTags).length > 0 ? tagsPath : null;
@ -348,6 +314,7 @@ export default function pluginContentBlog(
Object.keys(blogTags).map(async (tag) => {
const {name, items, permalink} = blogTags[tag];
// Refactor all this, see docs implementation
tagsModule[tag] = {
allTagsPath: blogTagsListPath,
slug: tag,
@ -535,7 +502,6 @@ export default function pluginContentBlog(
const feedTypes = options.feedOptions.type;
const {
siteConfig: {title},
baseUrl,
} = context;
const feedsConfig = {
rss: {

View file

@ -6,7 +6,8 @@
*/
import type {RemarkAndRehypePluginOptions} from '@docusaurus/mdx-loader';
import {
import type {Tag} from '@docusaurus/utils';
import type {
BrokenMarkdownLink,
ContentPaths,
} from '@docusaurus/utils/lib/markdownLinks';
@ -96,7 +97,7 @@ export interface MetaData {
description: string;
date: Date;
formattedDate: string;
tags: (Tag | string)[];
tags: Tag[];
title: string;
readingTime?: number;
prevItem?: Paginator;
@ -110,11 +111,6 @@ export interface Paginator {
permalink: string;
}
export interface Tag {
label: string;
permalink: string;
}
export interface BlogItemsToMetadata {
[key: string]: MetaData;
}

View file

@ -3,6 +3,11 @@ id: baz
title: baz
slug: bazSlug.html
pagination_label: baz pagination_label
tags:
- tag 1
- tag-1
- label: tag 2
permalink: tag2-custom-permalink
---
# Baz markdown title

View file

@ -2,6 +2,7 @@
id: hello
title: Hello, World !
sidebar_label: Hello sidebar_label
tags: [tag-1, tag 3]
---
Hi, Endilie here :)

View file

@ -1,4 +1,10 @@
---
slug: barSlug
tags:
- barTag 1
- barTag-2
- label: barTag 3
permalink: barTag-3-permalink
---
This is `next` version of bar.

View file

@ -177,6 +177,7 @@ Object {
\\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/bar\\",
\\"permalink\\": \\"/docs/foo/bar\\",
\\"tags\\": [],
\\"version\\": \\"current\\",
\\"frontMatter\\": {
\\"id\\": \\"bar\\",
@ -199,12 +200,30 @@ Object {
\\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/bazSlug.html\\",
\\"permalink\\": \\"/docs/foo/bazSlug.html\\",
\\"tags\\": [
{
\\"label\\": \\"tag 1\\",
\\"permalink\\": \\"/docs/tags/tag-1\\"
},
{
\\"label\\": \\"tag 2\\",
\\"permalink\\": \\"/docs/tags/tag2-custom-permalink\\"
}
],
\\"version\\": \\"current\\",
\\"frontMatter\\": {
\\"id\\": \\"baz\\",
\\"title\\": \\"baz\\",
\\"slug\\": \\"bazSlug.html\\",
\\"pagination_label\\": \\"baz pagination_label\\"
\\"pagination_label\\": \\"baz pagination_label\\",
\\"tags\\": [
\\"tag 1\\",
\\"tag-1\\",
{
\\"label\\": \\"tag 2\\",
\\"permalink\\": \\"tag2-custom-permalink\\"
}
]
},
\\"sidebar\\": \\"docs\\",
\\"previous\\": {
@ -226,6 +245,7 @@ Object {
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/headingAsTitle\\",
\\"permalink\\": \\"/docs/headingAsTitle\\",
\\"tags\\": [],
\\"version\\": \\"current\\",
\\"frontMatter\\": {}
}",
@ -239,11 +259,25 @@ Object {
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/\\",
\\"permalink\\": \\"/docs/\\",
\\"tags\\": [
{
\\"label\\": \\"tag-1\\",
\\"permalink\\": \\"/docs/tags/tag-1\\"
},
{
\\"label\\": \\"tag 3\\",
\\"permalink\\": \\"/docs/tags/tag-3\\"
}
],
\\"version\\": \\"current\\",
\\"frontMatter\\": {
\\"id\\": \\"hello\\",
\\"title\\": \\"Hello, World !\\",
\\"sidebar_label\\": \\"Hello sidebar_label\\"
\\"sidebar_label\\": \\"Hello sidebar_label\\",
\\"tags\\": [
\\"tag-1\\",
\\"tag 3\\"
]
},
\\"sidebar\\": \\"docs\\",
\\"previous\\": {
@ -262,6 +296,7 @@ Object {
\\"slug\\": \\"/ipsum\\",
\\"permalink\\": \\"/docs/ipsum\\",
\\"editUrl\\": null,
\\"tags\\": [],
\\"version\\": \\"current\\",
\\"frontMatter\\": {
\\"custom_edit_url\\": null
@ -278,6 +313,7 @@ Object {
\\"slug\\": \\"/lorem\\",
\\"permalink\\": \\"/docs/lorem\\",
\\"editUrl\\": \\"https://github.com/customUrl/docs/lorem.md\\",
\\"tags\\": [],
\\"version\\": \\"current\\",
\\"frontMatter\\": {
\\"custom_edit_url\\": \\"https://github.com/customUrl/docs/lorem.md\\",
@ -294,6 +330,7 @@ Object {
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootAbsoluteSlug\\",
\\"permalink\\": \\"/docs/rootAbsoluteSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\",
\\"frontMatter\\": {
\\"slug\\": \\"/rootAbsoluteSlug\\"
@ -309,6 +346,7 @@ Object {
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootRelativeSlug\\",
\\"permalink\\": \\"/docs/rootRelativeSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\",
\\"frontMatter\\": {
\\"slug\\": \\"rootRelativeSlug\\"
@ -324,6 +362,7 @@ Object {
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/hey/rootResolvedSlug\\",
\\"permalink\\": \\"/docs/hey/rootResolvedSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\",
\\"frontMatter\\": {
\\"slug\\": \\"./hey/ho/../rootResolvedSlug\\"
@ -339,6 +378,7 @@ Object {
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootTryToEscapeSlug\\",
\\"permalink\\": \\"/docs/rootTryToEscapeSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\",
\\"frontMatter\\": {
\\"slug\\": \\"../../../../../../../../rootTryToEscapeSlug\\"
@ -354,6 +394,7 @@ Object {
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/absoluteSlug\\",
\\"permalink\\": \\"/docs/absoluteSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\",
\\"frontMatter\\": {
\\"slug\\": \\"/absoluteSlug\\"
@ -369,6 +410,7 @@ Object {
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/relativeSlug\\",
\\"permalink\\": \\"/docs/slugs/relativeSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\",
\\"frontMatter\\": {
\\"slug\\": \\"relativeSlug\\"
@ -384,6 +426,7 @@ Object {
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/hey/resolvedSlug\\",
\\"permalink\\": \\"/docs/slugs/hey/resolvedSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\",
\\"frontMatter\\": {
\\"slug\\": \\"./hey/ho/../resolvedSlug\\"
@ -399,11 +442,74 @@ Object {
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/tryToEscapeSlug\\",
\\"permalink\\": \\"/docs/tryToEscapeSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\",
\\"frontMatter\\": {
\\"slug\\": \\"../../../../../../../../tryToEscapeSlug\\"
}
}",
"tag-docs-tags-tag-1-b3f.json": "{
\\"name\\": \\"tag 1\\",
\\"permalink\\": \\"/docs/tags/tag-1\\",
\\"docs\\": [
{
\\"id\\": \\"foo/baz\\",
\\"title\\": \\"baz\\",
\\"description\\": \\"Images\\",
\\"permalink\\": \\"/docs/foo/bazSlug.html\\"
},
{
\\"id\\": \\"hello\\",
\\"title\\": \\"Hello, World !\\",
\\"description\\": \\"Hi, Endilie here :)\\",
\\"permalink\\": \\"/docs/\\"
}
],
\\"allTagsPath\\": \\"/docs/tags\\"
}",
"tag-docs-tags-tag-2-custom-permalink-825.json": "{
\\"name\\": \\"tag 2\\",
\\"permalink\\": \\"/docs/tags/tag2-custom-permalink\\",
\\"docs\\": [
{
\\"id\\": \\"foo/baz\\",
\\"title\\": \\"baz\\",
\\"description\\": \\"Images\\",
\\"permalink\\": \\"/docs/foo/bazSlug.html\\"
}
],
\\"allTagsPath\\": \\"/docs/tags\\"
}",
"tag-docs-tags-tag-3-ab5.json": "{
\\"name\\": \\"tag 3\\",
\\"permalink\\": \\"/docs/tags/tag-3\\",
\\"docs\\": [
{
\\"id\\": \\"hello\\",
\\"title\\": \\"Hello, World !\\",
\\"description\\": \\"Hi, Endilie here :)\\",
\\"permalink\\": \\"/docs/\\"
}
],
\\"allTagsPath\\": \\"/docs/tags\\"
}",
"tags-list-current-prop-15a.json": "[
{
\\"name\\": \\"tag 1\\",
\\"permalink\\": \\"/docs/tags/tag-1\\",
\\"count\\": 2
},
{
\\"name\\": \\"tag 2\\",
\\"permalink\\": \\"/docs/tags/tag2-custom-permalink\\",
\\"count\\": 1
},
{
\\"name\\": \\"tag 3\\",
\\"permalink\\": \\"/docs/tags/tag-3\\",
\\"count\\": 1
}
]",
"version-current-metadata-prop-751.json": "{
\\"pluginId\\": \\"default\\",
\\"version\\": \\"current\\",
@ -560,6 +666,38 @@ Object {
exports[`simple website content: route config 1`] = `
Array [
Object {
"component": "@theme/DocTagsListPage",
"exact": true,
"modules": Object {
"tags": "~docs/tags-list-current-prop-15a.json",
},
"path": "/docs/tags",
},
Object {
"component": "@theme/DocTagDocListPage",
"exact": true,
"modules": Object {
"tag": "~docs/tag-docs-tags-tag-1-b3f.json",
},
"path": "/docs/tags/tag-1",
},
Object {
"component": "@theme/DocTagDocListPage",
"exact": true,
"modules": Object {
"tag": "~docs/tag-docs-tags-tag-3-ab5.json",
},
"path": "/docs/tags/tag-3",
},
Object {
"component": "@theme/DocTagDocListPage",
"exact": true,
"modules": Object {
"tag": "~docs/tag-docs-tags-tag-2-custom-permalink-825.json",
},
"path": "/docs/tags/tag2-custom-permalink",
},
Object {
"component": "@theme/DocPage",
"exact": false,
@ -857,6 +995,7 @@ Object {
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/team\\",
\\"permalink\\": \\"/community/team\\",
\\"tags\\": [],
\\"version\\": \\"1.0.0\\",
\\"frontMatter\\": {},
\\"sidebar\\": \\"version-1.0.0/community\\"
@ -871,12 +1010,15 @@ Object {
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/team\\",
\\"permalink\\": \\"/community/next/team\\",
\\"tags\\": [],
\\"version\\": \\"current\\",
\\"frontMatter\\": {
\\"title\\": \\"Team title translated\\"
},
\\"sidebar\\": \\"community\\"
}",
"tags-list-1-0-0-prop-483.json": "[]",
"tags-list-current-prop-15a.json": "[]",
"version-1-0-0-metadata-prop-608.json": "{
\\"pluginId\\": \\"community\\",
\\"version\\": \\"1.0.0\\",
@ -954,6 +1096,22 @@ Object {
exports[`versioned website (community) content: route config 1`] = `
Array [
Object {
"component": "@theme/DocTagsListPage",
"exact": true,
"modules": Object {
"tags": "~docs/tags-list-current-prop-15a.json",
},
"path": "/community/next/tags",
},
Object {
"component": "@theme/DocTagsListPage",
"exact": true,
"modules": Object {
"tags": "~docs/tags-list-1-0-0-prop-483.json",
},
"path": "/community/tags",
},
Object {
"component": "@theme/DocPage",
"exact": false,
@ -1106,9 +1264,31 @@ Object {
\\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/barSlug\\",
\\"permalink\\": \\"/docs/next/foo/barSlug\\",
\\"tags\\": [
{
\\"label\\": \\"barTag 1\\",
\\"permalink\\": \\"/docs/next/tags/bar-tag-1\\"
},
{
\\"label\\": \\"barTag-2\\",
\\"permalink\\": \\"/docs/next/tags/bar-tag-2\\"
},
{
\\"label\\": \\"barTag 3\\",
\\"permalink\\": \\"/docs/next/tags/barTag-3-permalink\\"
}
],
\\"version\\": \\"current\\",
\\"frontMatter\\": {
\\"slug\\": \\"barSlug\\"
\\"slug\\": \\"barSlug\\",
\\"tags\\": [
\\"barTag 1\\",
\\"barTag-2\\",
{
\\"label\\": \\"barTag 3\\",
\\"permalink\\": \\"barTag-3-permalink\\"
}
]
},
\\"sidebar\\": \\"docs\\",
\\"next\\": {
@ -1126,6 +1306,7 @@ Object {
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/\\",
\\"permalink\\": \\"/docs/next/\\",
\\"tags\\": [],
\\"version\\": \\"current\\",
\\"frontMatter\\": {},
\\"sidebar\\": \\"docs\\",
@ -1144,6 +1325,7 @@ Object {
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/absoluteSlug\\",
\\"permalink\\": \\"/docs/next/absoluteSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\",
\\"frontMatter\\": {
\\"slug\\": \\"/absoluteSlug\\"
@ -1159,6 +1341,7 @@ Object {
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/relativeSlug\\",
\\"permalink\\": \\"/docs/next/slugs/relativeSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\",
\\"frontMatter\\": {
\\"slug\\": \\"relativeSlug\\"
@ -1174,6 +1357,7 @@ Object {
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/hey/resolvedSlug\\",
\\"permalink\\": \\"/docs/next/slugs/hey/resolvedSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\",
\\"frontMatter\\": {
\\"slug\\": \\"./hey/ho/../resolvedSlug\\"
@ -1189,6 +1373,7 @@ Object {
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/tryToEscapeSlug\\",
\\"permalink\\": \\"/docs/next/tryToEscapeSlug\\",
\\"tags\\": [],
\\"version\\": \\"current\\",
\\"frontMatter\\": {
\\"slug\\": \\"../../../../../../../../tryToEscapeSlug\\"
@ -1204,6 +1389,7 @@ Object {
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/\\",
\\"permalink\\": \\"/docs/1.0.0/\\",
\\"tags\\": [],
\\"version\\": \\"1.0.0\\",
\\"frontMatter\\": {},
\\"sidebar\\": \\"version-1.0.0/docs\\",
@ -1222,6 +1408,7 @@ Object {
\\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/barSlug\\",
\\"permalink\\": \\"/docs/1.0.0/foo/barSlug\\",
\\"tags\\": [],
\\"version\\": \\"1.0.0\\",
\\"frontMatter\\": {
\\"slug\\": \\"barSlug\\"
@ -1242,6 +1429,7 @@ Object {
\\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/baz\\",
\\"permalink\\": \\"/docs/1.0.0/foo/baz\\",
\\"tags\\": [],
\\"version\\": \\"1.0.0\\",
\\"frontMatter\\": {},
\\"sidebar\\": \\"version-1.0.0/docs\\",
@ -1264,6 +1452,7 @@ Object {
\\"sourceDirName\\": \\"foo\\",
\\"slug\\": \\"/foo/bar\\",
\\"permalink\\": \\"/docs/foo/bar\\",
\\"tags\\": [],
\\"version\\": \\"1.0.1\\",
\\"frontMatter\\": {},
\\"sidebar\\": \\"version-1.0.1/docs\\",
@ -1282,6 +1471,7 @@ Object {
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/\\",
\\"permalink\\": \\"/docs/\\",
\\"tags\\": [],
\\"version\\": \\"1.0.1\\",
\\"frontMatter\\": {},
\\"sidebar\\": \\"version-1.0.1/docs\\",
@ -1300,6 +1490,7 @@ Object {
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootAbsoluteSlug\\",
\\"permalink\\": \\"/docs/withSlugs/rootAbsoluteSlug\\",
\\"tags\\": [],
\\"version\\": \\"withSlugs\\",
\\"frontMatter\\": {
\\"slug\\": \\"/rootAbsoluteSlug\\"
@ -1316,6 +1507,7 @@ Object {
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootRelativeSlug\\",
\\"permalink\\": \\"/docs/withSlugs/rootRelativeSlug\\",
\\"tags\\": [],
\\"version\\": \\"withSlugs\\",
\\"frontMatter\\": {
\\"slug\\": \\"rootRelativeSlug\\"
@ -1331,6 +1523,7 @@ Object {
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/hey/rootResolvedSlug\\",
\\"permalink\\": \\"/docs/withSlugs/hey/rootResolvedSlug\\",
\\"tags\\": [],
\\"version\\": \\"withSlugs\\",
\\"frontMatter\\": {
\\"slug\\": \\"./hey/ho/../rootResolvedSlug\\"
@ -1346,6 +1539,7 @@ Object {
\\"sourceDirName\\": \\".\\",
\\"slug\\": \\"/rootTryToEscapeSlug\\",
\\"permalink\\": \\"/docs/withSlugs/rootTryToEscapeSlug\\",
\\"tags\\": [],
\\"version\\": \\"withSlugs\\",
\\"frontMatter\\": {
\\"slug\\": \\"../../../../../../../../rootTryToEscapeSlug\\"
@ -1361,6 +1555,7 @@ Object {
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/absoluteSlug\\",
\\"permalink\\": \\"/docs/withSlugs/absoluteSlug\\",
\\"tags\\": [],
\\"version\\": \\"withSlugs\\",
\\"frontMatter\\": {
\\"slug\\": \\"/absoluteSlug\\"
@ -1376,6 +1571,7 @@ Object {
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/relativeSlug\\",
\\"permalink\\": \\"/docs/withSlugs/slugs/relativeSlug\\",
\\"tags\\": [],
\\"version\\": \\"withSlugs\\",
\\"frontMatter\\": {
\\"slug\\": \\"relativeSlug\\"
@ -1391,6 +1587,7 @@ Object {
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/slugs/hey/resolvedSlug\\",
\\"permalink\\": \\"/docs/withSlugs/slugs/hey/resolvedSlug\\",
\\"tags\\": [],
\\"version\\": \\"withSlugs\\",
\\"frontMatter\\": {
\\"slug\\": \\"./hey/ho/../resolvedSlug\\"
@ -1406,11 +1603,71 @@ Object {
\\"sourceDirName\\": \\"slugs\\",
\\"slug\\": \\"/tryToEscapeSlug\\",
\\"permalink\\": \\"/docs/withSlugs/tryToEscapeSlug\\",
\\"tags\\": [],
\\"version\\": \\"withSlugs\\",
\\"frontMatter\\": {
\\"slug\\": \\"../../../../../../../../tryToEscapeSlug\\"
}
}",
"tag-docs-next-tags-bar-tag-1-a8f.json": "{
\\"name\\": \\"barTag 1\\",
\\"permalink\\": \\"/docs/next/tags/bar-tag-1\\",
\\"docs\\": [
{
\\"id\\": \\"foo/bar\\",
\\"title\\": \\"bar\\",
\\"description\\": \\"This is next version of bar.\\",
\\"permalink\\": \\"/docs/next/foo/barSlug\\"
}
],
\\"allTagsPath\\": \\"/docs/next/tags\\"
}",
"tag-docs-next-tags-bar-tag-2-216.json": "{
\\"name\\": \\"barTag-2\\",
\\"permalink\\": \\"/docs/next/tags/bar-tag-2\\",
\\"docs\\": [
{
\\"id\\": \\"foo/bar\\",
\\"title\\": \\"bar\\",
\\"description\\": \\"This is next version of bar.\\",
\\"permalink\\": \\"/docs/next/foo/barSlug\\"
}
],
\\"allTagsPath\\": \\"/docs/next/tags\\"
}",
"tag-docs-next-tags-bar-tag-3-permalink-94a.json": "{
\\"name\\": \\"barTag 3\\",
\\"permalink\\": \\"/docs/next/tags/barTag-3-permalink\\",
\\"docs\\": [
{
\\"id\\": \\"foo/bar\\",
\\"title\\": \\"bar\\",
\\"description\\": \\"This is next version of bar.\\",
\\"permalink\\": \\"/docs/next/foo/barSlug\\"
}
],
\\"allTagsPath\\": \\"/docs/next/tags\\"
}",
"tags-list-1-0-0-prop-483.json": "[]",
"tags-list-1-0-1-prop-c39.json": "[]",
"tags-list-current-prop-15a.json": "[
{
\\"name\\": \\"barTag 1\\",
\\"permalink\\": \\"/docs/next/tags/bar-tag-1\\",
\\"count\\": 1
},
{
\\"name\\": \\"barTag-2\\",
\\"permalink\\": \\"/docs/next/tags/bar-tag-2\\",
\\"count\\": 1
},
{
\\"name\\": \\"barTag 3\\",
\\"permalink\\": \\"/docs/next/tags/barTag-3-permalink\\",
\\"count\\": 1
}
]",
"tags-list-with-slugs-prop-1ca.json": "[]",
"version-1-0-0-metadata-prop-608.json": "{
\\"pluginId\\": \\"default\\",
\\"version\\": \\"1.0.0\\",
@ -1699,6 +1956,62 @@ Object {
exports[`versioned website content: route config 1`] = `
Array [
Object {
"component": "@theme/DocTagsListPage",
"exact": true,
"modules": Object {
"tags": "~docs/tags-list-1-0-0-prop-483.json",
},
"path": "/docs/1.0.0/tags",
},
Object {
"component": "@theme/DocTagsListPage",
"exact": true,
"modules": Object {
"tags": "~docs/tags-list-current-prop-15a.json",
},
"path": "/docs/next/tags",
},
Object {
"component": "@theme/DocTagDocListPage",
"exact": true,
"modules": Object {
"tag": "~docs/tag-docs-next-tags-bar-tag-1-a8f.json",
},
"path": "/docs/next/tags/bar-tag-1",
},
Object {
"component": "@theme/DocTagDocListPage",
"exact": true,
"modules": Object {
"tag": "~docs/tag-docs-next-tags-bar-tag-2-216.json",
},
"path": "/docs/next/tags/bar-tag-2",
},
Object {
"component": "@theme/DocTagDocListPage",
"exact": true,
"modules": Object {
"tag": "~docs/tag-docs-next-tags-bar-tag-3-permalink-94a.json",
},
"path": "/docs/next/tags/barTag-3-permalink",
},
Object {
"component": "@theme/DocTagsListPage",
"exact": true,
"modules": Object {
"tags": "~docs/tags-list-1-0-1-prop-c39.json",
},
"path": "/docs/tags",
},
Object {
"component": "@theme/DocTagsListPage",
"exact": true,
"modules": Object {
"tags": "~docs/tags-list-with-slugs-prop-1ca.json",
},
"path": "/docs/withSlugs/tags",
},
Object {
"component": "@theme/DocPage",
"exact": false,

View file

@ -187,6 +187,7 @@ describe('simple site', () => {
id: 'bar',
title: 'Bar',
},
tags: [],
});
await defaultTestUtils.testMeta(path.join('hello.md'), {
version: 'current',
@ -202,7 +203,18 @@ describe('simple site', () => {
id: 'hello',
title: 'Hello, World !',
sidebar_label: 'Hello sidebar_label',
tags: ['tag-1', 'tag 3'],
},
tags: [
{
label: 'tag-1',
permalink: '/docs/tags/tag-1',
},
{
label: 'tag 3',
permalink: '/docs/tags/tag-3',
},
],
});
});
@ -232,7 +244,18 @@ describe('simple site', () => {
id: 'hello',
title: 'Hello, World !',
sidebar_label: 'Hello sidebar_label',
tags: ['tag-1', 'tag 3'],
},
tags: [
{
label: 'tag-1',
permalink: '/docs/tags/tag-1',
},
{
label: 'tag 3',
permalink: '/docs/tags/tag-3',
},
],
});
});
@ -263,6 +286,7 @@ describe('simple site', () => {
id: 'bar',
title: 'Bar',
},
tags: [],
});
});
@ -297,7 +321,22 @@ describe('simple site', () => {
slug: 'bazSlug.html',
title: 'baz',
pagination_label: 'baz pagination_label',
tags: [
'tag 1',
'tag-1',
{label: 'tag 2', permalink: 'tag2-custom-permalink'},
],
},
tags: [
{
label: 'tag 1',
permalink: '/docs/tags/tag-1',
},
{
label: 'tag 2',
permalink: '/docs/tags/tag2-custom-permalink',
},
],
});
});
@ -319,6 +358,7 @@ describe('simple site', () => {
custom_edit_url: 'https://github.com/customUrl/docs/lorem.md',
unrelated_frontmatter: "won't be part of metadata",
},
tags: [],
});
});
@ -356,7 +396,22 @@ describe('simple site', () => {
slug: 'bazSlug.html',
title: 'baz',
pagination_label: 'baz pagination_label',
tags: [
'tag 1',
'tag-1',
{label: 'tag 2', permalink: 'tag2-custom-permalink'},
],
},
tags: [
{
label: 'tag 1',
permalink: '/docs/tags/tag-1',
},
{
label: 'tag 2',
permalink: '/docs/tags/tag2-custom-permalink',
},
],
});
expect(editUrlFunction).toHaveBeenCalledTimes(1);
@ -402,6 +457,7 @@ describe('simple site', () => {
lastUpdatedAt: 1539502055,
formattedLastUpdatedAt: '10/14/2018',
lastUpdatedBy: 'Author',
tags: [],
});
});
@ -559,6 +615,7 @@ describe('versioned site', () => {
await currentVersionTestUtils.testMeta(path.join('foo', 'bar.md'), {
id: 'foo/bar',
version: 'current',
unversionedId: 'foo/bar',
sourceDirName: 'foo',
isDocsHomePage: false,
@ -566,11 +623,35 @@ describe('versioned site', () => {
slug: '/foo/barSlug',
title: 'bar',
description: 'This is next version of bar.',
frontMatter: {slug: 'barSlug'},
version: 'current',
frontMatter: {
slug: 'barSlug',
tags: [
'barTag 1',
'barTag-2',
{
label: 'barTag 3',
permalink: 'barTag-3-permalink',
},
],
},
tags: [
{
label: 'barTag 1',
permalink: '/docs/next/tags/bar-tag-1',
},
{
label: 'barTag-2',
permalink: '/docs/next/tags/bar-tag-2',
},
{
label: 'barTag 3',
permalink: '/docs/next/tags/barTag-3-permalink',
},
],
});
await currentVersionTestUtils.testMeta(path.join('hello.md'), {
id: 'hello',
version: 'current',
unversionedId: 'hello',
sourceDirName: '.',
isDocsHomePage: false,
@ -579,7 +660,7 @@ describe('versioned site', () => {
title: 'hello',
description: 'Hello next !',
frontMatter: {},
version: 'current',
tags: [],
});
});
@ -597,6 +678,7 @@ describe('versioned site', () => {
description: 'Bar 1.0.0 !',
frontMatter: {slug: 'barSlug'},
version: '1.0.0',
tags: [],
});
await version100TestUtils.testMeta(path.join('hello.md'), {
id: 'version-1.0.0/hello',
@ -611,6 +693,7 @@ describe('versioned site', () => {
version: '1.0.0',
source:
'@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md',
tags: [],
});
await version101TestUtils.testMeta(path.join('foo', 'bar.md'), {
id: 'version-1.0.1/foo/bar',
@ -623,6 +706,7 @@ describe('versioned site', () => {
description: 'Bar 1.0.1 !',
version: '1.0.1',
frontMatter: {},
tags: [],
});
await version101TestUtils.testMeta(path.join('hello.md'), {
id: 'version-1.0.1/hello',
@ -635,6 +719,7 @@ describe('versioned site', () => {
description: 'Hello 1.0.1 !',
version: '1.0.1',
frontMatter: {},
tags: [],
});
});
@ -729,6 +814,7 @@ describe('versioned site', () => {
source:
'@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md',
editUrl: hardcodedEditUrl,
tags: [],
});
expect(editUrlFunction).toHaveBeenCalledTimes(1);
@ -771,6 +857,7 @@ describe('versioned site', () => {
'@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md',
editUrl:
'https://github.com/facebook/docusaurus/edit/main/website/versioned_docs/version-1.0.0/hello.md',
tags: [],
});
});
@ -804,6 +891,7 @@ describe('versioned site', () => {
'@site/i18n/en/docusaurus-plugin-content-docs/version-1.0.0/hello.md',
editUrl:
'https://github.com/facebook/docusaurus/edit/main/website/docs/hello.md',
tags: [],
});
});
@ -838,6 +926,7 @@ describe('versioned site', () => {
'@site/i18n/fr/docusaurus-plugin-content-docs/version-1.0.0/hello.md',
editUrl:
'https://github.com/facebook/docusaurus/edit/main/website/i18n/fr/docusaurus-plugin-content-docs/version-1.0.0/hello.md',
tags: [],
});
});
@ -873,6 +962,7 @@ describe('versioned site', () => {
'@site/i18n/fr/docusaurus-plugin-content-docs/version-1.0.0/hello.md',
editUrl:
'https://github.com/facebook/docusaurus/edit/main/website/i18n/fr/docusaurus-plugin-content-docs/current/hello.md',
tags: [],
});
});
});

View file

@ -60,6 +60,7 @@ const defaultDocMetadata: Partial<DocMetadata> = {
lastUpdatedAt: undefined,
lastUpdatedBy: undefined,
formattedLastUpdatedAt: undefined,
tags: [],
};
const createFakeActions = (contentDir: string) => {
@ -364,7 +365,23 @@ describe('simple website', () => {
title: 'baz',
slug: 'bazSlug.html',
pagination_label: 'baz pagination_label',
tags: [
'tag 1',
'tag-1', // This one will be de-duplicated as it would lead to the same permalink as the first
{label: 'tag 2', permalink: 'tag2-custom-permalink'},
],
},
tags: [
{
label: 'tag 1',
permalink: '/docs/tags/tag-1',
},
{
label: 'tag 2',
permalink: '/docs/tags/tag2-custom-permalink',
},
],
});
expect(findDocById(currentVersion, 'hello')).toEqual({
@ -392,7 +409,18 @@ describe('simple website', () => {
id: 'hello',
title: 'Hello, World !',
sidebar_label: 'Hello sidebar_label',
tags: ['tag-1', 'tag 3'],
},
tags: [
{
label: 'tag-1',
permalink: '/docs/tags/tag-1',
},
{
label: 'tag 3',
permalink: '/docs/tags/tag-3',
},
],
});
expect(getDocById(currentVersion, 'foo/bar')).toEqual({
@ -579,6 +607,11 @@ describe('versioned website', () => {
description: 'This is next version of bar.',
frontMatter: {
slug: 'barSlug',
tags: [
'barTag 1',
'barTag-2',
{label: 'barTag 3', permalink: 'barTag-3-permalink'},
],
},
version: 'current',
sidebar: 'docs',
@ -586,6 +619,11 @@ describe('versioned website', () => {
title: 'hello',
permalink: '/docs/next/',
},
tags: [
{label: 'barTag 1', permalink: '/docs/next/tags/bar-tag-1'},
{label: 'barTag-2', permalink: '/docs/next/tags/bar-tag-2'},
{label: 'barTag 3', permalink: '/docs/next/tags/barTag-3-permalink'},
],
});
expect(getDocById(currentVersion, 'hello')).toEqual({
...defaultDocMetadata,

View file

@ -48,6 +48,8 @@ describe('normalizeDocsPluginOptions', () => {
numberPrefixParser: DefaultNumberPrefixParser,
docLayoutComponent: '@theme/DocPage',
docItemComponent: '@theme/DocItem',
docTagDocListComponent: '@theme/DocTagDocListPage',
docTagsListComponent: '@theme/DocTagsListPage',
remarkPlugins: [markdownPluginsObjectStub],
rehypePlugins: [markdownPluginsFunctionStub],
beforeDefaultRehypePlugins: [],

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 {toTagDocListProp} from '../props';
describe('toTagDocListProp', () => {
type Params = Parameters<typeof toTagDocListProp>[0];
type Tag = Params['tag'];
type Doc = Params['docs'][number];
const allTagsPath = '/all/tags';
test('should work', () => {
const tag: Tag = {
name: 'tag1',
permalink: '/tag1',
docIds: ['id1', 'id3'],
};
const doc1: Doc = {
id: 'id1',
title: 'ZZZ 1',
description: 'Description 1',
permalink: '/doc1',
};
const doc2: Doc = {
id: 'id2',
title: 'XXX 2',
description: 'Description 2',
permalink: '/doc2',
};
const doc3: Doc = {
id: 'id3',
title: 'AAA 3',
description: 'Description 3',
permalink: '/doc3',
};
const doc4: Doc = {
id: 'id4',
title: 'UUU 4',
description: 'Description 4',
permalink: '/doc4',
};
const result = toTagDocListProp({
allTagsPath,
tag,
docs: [doc1, doc2, doc3, doc4],
});
expect(result).toEqual({
allTagsPath,
name: tag.name,
permalink: tag.permalink,
docs: [doc3, doc1], // docs sorted by title, ignore "id5" absence
});
});
});

View file

@ -77,6 +77,7 @@ describe('simple site', () => {
isLast: true,
routePriority: -1,
sidebarFilePath: undefined,
tagsPath: '/docs/tags',
versionLabel: 'Next',
versionName: 'current',
versionPath: '/docs',
@ -111,6 +112,7 @@ describe('simple site', () => {
{
...vCurrent,
versionPath: '/myBaseUrl/docs',
tagsPath: '/myBaseUrl/docs/tags',
},
]);
});
@ -141,6 +143,7 @@ describe('simple site', () => {
versionLabel: 'current-label',
routePriority: undefined,
sidebarFilePath: undefined,
tagsPath: '/myBaseUrl/docs/current-path/tags',
versionEditUrl: undefined,
versionEditUrlLocalized: undefined,
},
@ -232,6 +235,7 @@ describe('versioned site, pluginId=default', () => {
isLast: false,
routePriority: undefined,
sidebarFilePath: path.join(versionedSiteDir, 'sidebars.json'),
tagsPath: '/docs/next/tags',
versionLabel: 'Next',
versionName: 'current',
versionPath: '/docs/next',
@ -250,6 +254,7 @@ describe('versioned site, pluginId=default', () => {
versionedSiteDir,
'versioned_sidebars/version-1.0.1-sidebars.json',
),
tagsPath: '/docs/tags',
versionLabel: '1.0.1',
versionName: '1.0.1',
versionPath: '/docs',
@ -268,6 +273,7 @@ describe('versioned site, pluginId=default', () => {
versionedSiteDir,
'versioned_sidebars/version-1.0.0-sidebars.json',
),
tagsPath: '/docs/1.0.0/tags',
versionLabel: '1.0.0',
versionName: '1.0.0',
versionPath: '/docs/1.0.0',
@ -289,6 +295,7 @@ describe('versioned site, pluginId=default', () => {
versionedSiteDir,
'versioned_sidebars/version-withSlugs-sidebars.json',
),
tagsPath: '/docs/withSlugs/tags',
versionLabel: 'withSlugs',
versionName: 'withSlugs',
versionPath: '/docs/withSlugs',
@ -377,6 +384,7 @@ describe('versioned site, pluginId=default', () => {
expect(versionsMetadata).toEqual([
{
...vCurrent,
tagsPath: '/docs/current-path/tags',
versionPath: '/docs/current-path',
versionBanner: 'unmaintained',
},
@ -384,6 +392,7 @@ describe('versioned site, pluginId=default', () => {
...v101,
isLast: false,
routePriority: undefined,
tagsPath: '/docs/1.0.1/tags',
versionPath: '/docs/1.0.1',
versionBanner: 'unreleased',
},
@ -391,6 +400,7 @@ describe('versioned site, pluginId=default', () => {
...v100,
isLast: true,
routePriority: -1,
tagsPath: '/docs/tags',
versionLabel: '1.0.0-label',
versionPath: '/docs',
versionBanner: 'unreleased',
@ -528,6 +538,7 @@ describe('versioned site, pluginId=default', () => {
...vCurrent,
isLast: true,
routePriority: -1,
tagsPath: '/docs/tags',
versionPath: '/docs',
versionBanner: 'none',
},
@ -648,6 +659,7 @@ describe('versioned site, pluginId=community', () => {
isLast: false,
routePriority: undefined,
sidebarFilePath: path.join(versionedSiteDir, 'sidebars.json'),
tagsPath: '/communityBasePath/next/tags',
versionLabel: 'Next',
versionName: 'current',
versionPath: '/communityBasePath/next',
@ -669,6 +681,7 @@ describe('versioned site, pluginId=community', () => {
versionedSiteDir,
'community_versioned_sidebars/version-1.0.0-sidebars.json',
),
tagsPath: '/communityBasePath/tags',
versionLabel: '1.0.0',
versionName: '1.0.0',
versionPath: '/communityBasePath',
@ -716,6 +729,7 @@ describe('versioned site, pluginId=community', () => {
...vCurrent,
isLast: true,
routePriority: -1,
tagsPath: '/communityBasePath/tags',
versionPath: '/communityBasePath',
versionBanner: 'none',
},

View file

@ -15,6 +15,7 @@ import {
parseMarkdownString,
posixPath,
Globby,
normalizeFrontMatterTags,
} from '@docusaurus/utils';
import {LoadContext} from '@docusaurus/types';
@ -252,6 +253,7 @@ function doProcessDocMetadata({
slug: docSlug,
permalink,
editUrl: customEditURL !== undefined ? customEditURL : getDocEditUrl(),
tags: normalizeFrontMatterTags(versionMetadata.tagsPath, frontMatter.tags),
version: versionMetadata.versionName,
lastUpdatedBy: lastUpdate.lastUpdatedBy,
lastUpdatedAt: lastUpdate.lastUpdatedAt,

View file

@ -37,19 +37,22 @@ import {
LoadedVersion,
DocFile,
DocsMarkdownOption,
VersionTag,
} from './types';
import {RuleSetRule} from 'webpack';
import {cliDocsVersionCommand} from './cli';
import {VERSIONS_JSON_FILE} from './constants';
import {flatten, keyBy, compact, mapValues} from 'lodash';
import {toGlobalDataVersion} from './globalData';
import {toVersionMetadataProp} from './props';
import {toTagDocListProp, toVersionMetadataProp} from './props';
import {
translateLoadedContent,
getLoadedContentTranslationFiles,
} from './translations';
import {CategoryMetadataFilenamePattern} from './sidebarItemsGenerator';
import chalk from 'chalk';
import {getVersionTags} from './tags';
import {PropTagsListPage} from '@docusaurus/plugin-content-docs-types';
export default function pluginContentDocs(
context: LoadContext,
@ -314,9 +317,60 @@ export default function pluginContentDocs(
return routes.sort((a, b) => a.path.localeCompare(b.path));
};
async function createVersionTagsRoutes(loadedVersion: LoadedVersion) {
const versionTags = getVersionTags(loadedVersion.docs);
async function createTagsListPage() {
const tagsProp: PropTagsListPage['tags'] = Object.values(
versionTags,
).map((tagValue) => ({
name: tagValue.name,
permalink: tagValue.permalink,
count: tagValue.docIds.length,
}));
const tagsPropPath = await createData(
`${docuHash(`tags-list-${loadedVersion.versionName}-prop`)}.json`,
JSON.stringify(tagsProp, null, 2),
);
addRoute({
path: loadedVersion.tagsPath,
exact: true,
component: options.docTagsListComponent,
modules: {
tags: aliasedSource(tagsPropPath),
},
});
}
async function createTagDocListPage(tag: VersionTag) {
const tagProps = toTagDocListProp({
allTagsPath: loadedVersion.tagsPath,
tag,
docs: loadedVersion.docs,
});
const tagPropPath = await createData(
`${docuHash(`tag-${tag.permalink}`)}.json`,
JSON.stringify(tagProps, null, 2),
);
addRoute({
path: tag.permalink,
component: options.docTagDocListComponent,
exact: true,
modules: {
tag: aliasedSource(tagPropPath),
},
});
}
await createTagsListPage();
await Promise.all(Object.values(versionTags).map(createTagDocListPage));
}
async function doCreateVersionRoutes(
loadedVersion: LoadedVersion,
): Promise<void> {
await createVersionTagsRoutes(loadedVersion);
const versionMetadata = toVersionMetadataProp(pluginId, loadedVersion);
const versionMetadataPropPath = await createData(
`${docuHash(

View file

@ -33,6 +33,8 @@ export const DEFAULT_OPTIONS: Omit<PluginOptions, 'id' | 'sidebarPath'> = {
numberPrefixParser: DefaultNumberPrefixParser,
docLayoutComponent: '@theme/DocPage',
docItemComponent: '@theme/DocItem',
docTagDocListComponent: '@theme/DocTagDocListPage',
docTagsListComponent: '@theme/DocTagsListPage',
remarkPlugins: [],
rehypePlugins: [],
beforeDefaultRemarkPlugins: [],
@ -94,6 +96,12 @@ export const OptionsSchema = Joi.object({
.default(() => DEFAULT_OPTIONS.numberPrefixParser),
docLayoutComponent: Joi.string().default(DEFAULT_OPTIONS.docLayoutComponent),
docItemComponent: Joi.string().default(DEFAULT_OPTIONS.docItemComponent),
docTagsListComponent: Joi.string().default(
DEFAULT_OPTIONS.docTagsListComponent,
),
docTagDocListComponent: Joi.string().default(
DEFAULT_OPTIONS.docTagDocListComponent,
),
remarkPlugins: RemarkPluginsSchema.default(DEFAULT_OPTIONS.remarkPlugins),
rehypePlugins: RehypePluginsSchema.default(DEFAULT_OPTIONS.rehypePlugins),
beforeDefaultRemarkPlugins: RemarkPluginsSchema.default(

View file

@ -9,6 +9,9 @@ declare module '@docusaurus/plugin-content-docs-types' {
type VersionBanner = import('./types').VersionBanner;
type GlobalDataVersion = import('./types').GlobalVersion;
type GlobalDataDoc = import('./types').GlobalDoc;
type VersionTag = import('./types').VersionTag;
export type {GlobalDataVersion, GlobalDataDoc};
export type PropVersionMetadata = {
pluginId: string;
@ -43,7 +46,26 @@ declare module '@docusaurus/plugin-content-docs-types' {
[sidebarId: string]: PropSidebarItem[];
};
export type {GlobalDataVersion, GlobalDataDoc};
export type PropTagDocListDoc = {
id: string;
title: string;
description: string;
permalink: string;
};
export type PropTagDocList = {
allTagsPath: string;
name: string; // normalized name/label of the tag
permalink: string; // pathname of the tag
docs: PropTagDocListDoc[];
};
export type PropTagsListPage = {
tags: {
name: string;
permalink: string;
count: number;
}[];
};
}
declare module '@theme/DocItem' {
@ -79,6 +101,10 @@ declare module '@theme/DocItem' {
readonly version?: string;
readonly previous?: {readonly permalink: string; readonly title: string};
readonly next?: {readonly permalink: string; readonly title: string};
readonly tags: readonly {
readonly label: string;
readonly permalink: string;
}[];
};
export type Props = {
@ -97,6 +123,19 @@ declare module '@theme/DocItem' {
export default DocItem;
}
declare module '@theme/DocItemFooter' {
import type {Props} from '@theme/DocItem';
export default function DocItemFooter(props: Props): JSX.Element;
}
declare module '@theme/DocTagsListPage' {
import type {PropTagsListPage} from '@docusaurus/plugin-content-docs-types';
export type Props = PropTagsListPage;
export default function DocItemFooter(props: Props): JSX.Element;
}
declare module '@theme/DocVersionBanner' {
import type {PropVersionMetadata} from '@docusaurus/plugin-content-docs-types';

View file

@ -10,13 +10,17 @@ import {
SidebarItemDoc,
SidebarItemLink,
SidebarItem,
VersionTag,
DocMetadata,
} from './types';
import {
import type {
PropSidebars,
PropVersionMetadata,
PropSidebarItem,
PropTagDocList,
PropTagDocListDoc,
} from '@docusaurus/plugin-content-docs-types';
import {keyBy, mapValues} from 'lodash';
import {compact, keyBy, mapValues} from 'lodash';
export function toSidebarsProp(loadedVersion: LoadedVersion): PropSidebars {
const docsById = keyBy(loadedVersion.docs, (doc) => doc.id);
@ -79,3 +83,34 @@ export function toVersionMetadataProp(
docsSidebars: toSidebarsProp(loadedVersion),
};
}
export function toTagDocListProp({
allTagsPath,
tag,
docs,
}: {
allTagsPath: string;
tag: VersionTag;
docs: Pick<DocMetadata, 'id' | 'title' | 'description' | 'permalink'>[];
}): 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 {
name: tag.name,
permalink: tag.permalink,
docs: toDocListProp(),
allTagsPath,
};
}

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 {groupTaggedItems} from '@docusaurus/utils';
import {VersionTags, DocMetadata} from './types';
import {mapValues} from 'lodash';
export function getVersionTags(docs: DocMetadata[]): VersionTags {
const groups = groupTaggedItems(docs, (doc) => doc.tags);
return mapValues(groups, (group) => {
return {
name: group.tag.label,
docIds: group.items.map((item) => item.id),
permalink: group.tag.permalink,
};
});
}

View file

@ -9,7 +9,8 @@
/// <reference types="@docusaurus/module-type-aliases" />
import type {RemarkAndRehypePluginOptions} from '@docusaurus/mdx-loader';
import {
import type {Tag, FrontMatterTag} from '@docusaurus/utils';
import type {
BrokenMarkdownLink as IBrokenMarkdownLink,
ContentPaths,
} from '@docusaurus/utils/lib/markdownLinks';
@ -28,6 +29,7 @@ export type VersionMetadata = ContentPaths & {
versionName: VersionName; // 1.0.0
versionLabel: string; // Version 1.0.0
versionPath: string; // /baseUrl/docs/1.0.0
tagsPath: string;
versionEditUrl?: string | undefined;
versionEditUrlLocalized?: string | undefined;
versionBanner: VersionBanner;
@ -90,6 +92,8 @@ export type PluginOptions = MetadataOptions &
exclude: string[];
docLayoutComponent: string;
docItemComponent: string;
docTagDocListComponent: string;
docTagsListComponent: string;
admonitions: Record<string, unknown>;
disableVersioning: boolean;
includeCurrentVersion: boolean;
@ -200,6 +204,7 @@ export type DocFrontMatter = {
/* eslint-disable camelcase */
id?: string;
title?: string;
tags?: FrontMatterTag[];
hide_title?: boolean;
hide_table_of_contents?: boolean;
keywords?: string[];
@ -227,6 +232,7 @@ export type DocMetadataBase = LastUpdateData & {
permalink: string;
sidebarPosition?: number;
editUrl?: string | null;
tags: Tag[];
frontMatter: DocFrontMatter & Record<string, unknown>;
};
@ -244,6 +250,16 @@ export type DocMetadata = DocMetadataBase & {
export type SourceToPermalink = {
[source: string]: string;
};
export type VersionTag = {
name: string; // normalized name/label of the tag
docIds: string[]; // all doc ids having this tag
permalink: string; // pathname of the tag
};
export type VersionTags = {
[key: string]: VersionTag;
};
export type LoadedVersion = VersionMetadata & {
versionPath: string;
mainDocId: string;

View file

@ -370,10 +370,15 @@ function createVersionMetadata({
// Because /docs/:route` should always be after `/docs/versionName/:route`.
const routePriority = versionPathPart === '' ? -1 : undefined;
// the path that will be used to refer the docs tags
// example below will be using /docs/tags
const tagsPath = normalizeUrl([versionPath, 'tags']);
return {
versionName,
versionLabel,
versionPath,
tagsPath,
versionEditUrl: versionEditUrls?.versionEditUrl,
versionEditUrlLocalized: versionEditUrls?.versionEditUrlLocalized,
versionBanner: getVersionBanner({

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 React from 'react';
import Layout from '@theme/Layout';
import Link from '@docusaurus/Link';
import type {Props} from '@theme/BlogTagsListPage';
function DocTagsListPage(props: Props): JSX.Element {
const {tags} = props;
const renderAllTags = () => (
<>
{Object.keys(tags).map((tag) => (
<Link
href={tags[tag].permalink}
key={tag}
className="btn btn-primary list-inline-item my-2">
{tags[tag].name}{' '}
<span className="badge badge-light">{tags[tag].count}</span>
</Link>
))}
</>
);
return (
<Layout title="Tags" description="Blog Tags">
<div className="container my-3 justify-content-center">
<h1 className="text-primary">Tags</h1>
<ul className="my-xl-4 list-inline">{renderAllTags()}</ul>
</div>
</Layout>
);
}
export default DocTagsListPage;

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "طي الشريط الجانبي",
"theme.docs.sidebar.expandButtonAriaLabel": "توسيع الشريط الجانبي",
"theme.docs.sidebar.expandButtonTitle": "توسيع الشريط الجانبي",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "احدث اصدار",
"theme.docs.versions.latestVersionSuggestionLabel": "للحصول على أحدث الوثائق، راجع {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "هذه هي وثائق {siteTitle} {versionLabel}، التي لم تعد تتم صيانتها بشكل نشط.",

View file

@ -85,6 +85,10 @@
"theme.docs.sidebar.expandButtonAriaLabel___DESCRIPTION": "The ARIA label and title attribute for expand button of doc sidebar",
"theme.docs.sidebar.expandButtonTitle": "Expand sidebar",
"theme.docs.sidebar.expandButtonTitle___DESCRIPTION": "The ARIA label and title attribute for expand button of doc sidebar",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle___DESCRIPTION": "The title of the page for a docs tag",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.tagDocListPageTitle.nDocsTagged___DESCRIPTION": "Pluralized label for \"{count} docs tagged\". Use as much plural forms (separated by \"|\") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)",
"theme.docs.versions.latestVersionLinkLabel": "latest version",
"theme.docs.versions.latestVersionLinkLabel___DESCRIPTION": "The label used for the latest version suggestion link label",
"theme.docs.versions.latestVersionSuggestionLabel": "For up-to-date documentation, see the {latestVersionLink} ({versionLabel}).",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "সাইডবারটি সঙ্কুচিত করুন",
"theme.docs.sidebar.expandButtonAriaLabel": "সাইডবারটি প্রসারিত করুন",
"theme.docs.sidebar.expandButtonTitle": "সাইডবারটি প্রসারিত করুন",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "লেটেস্ট ভার্সন",
"theme.docs.versions.latestVersionSuggestionLabel": "আপ-টু-ডেট ডকুমেন্টেশনের জন্য, {latestVersionLink} ({versionLabel}) দেখুন।",
"theme.docs.versions.unmaintainedVersionLabel": "এটি {siteTitle} {versionLabel} এর জন্যে ডকুমেন্টেশন, যা আর সক্রিয়ভাবে রক্ষণাবেক্ষণ করা হয় না।",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Zavřít postranní lištu",
"theme.docs.sidebar.expandButtonAriaLabel": "Otevřít postranní lištu",
"theme.docs.sidebar.expandButtonTitle": "Otevřít postranní lištu",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "Nejnovější verze",
"theme.docs.versions.latestVersionSuggestionLabel": "Aktuální dokumentace viz {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Tato dokumentace je pro {siteTitle} {versionLabel}, která už není aktivně udržována.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Sammenlæg sidenavigation",
"theme.docs.sidebar.expandButtonAriaLabel": "Udvid sidenavigation",
"theme.docs.sidebar.expandButtonTitle": "Udvid sidenavigation",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "seneste version",
"theme.docs.versions.latestVersionSuggestionLabel": "For seneste dokumentation, se {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Dette er dokumentationen for {siteTitle} {versionLabel}, som ikke længere bliver aktivt vedligeholdt.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Seitenleiste einklappen",
"theme.docs.sidebar.expandButtonAriaLabel": "Seitenleiste ausklappen",
"theme.docs.sidebar.expandButtonTitle": "Seitenleiste ausklappen",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "letzte Version",
"theme.docs.versions.latestVersionSuggestionLabel": "Für die aktuellste Dokumentation bitte auf {latestVersionLink} ({versionLabel}) gehen.",
"theme.docs.versions.unmaintainedVersionLabel": "Das ist die Dokumentation für {siteTitle} {versionLabel} und wird nicht weiter gewartet.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Colapsar barra lateral",
"theme.docs.sidebar.expandButtonAriaLabel": "Expandir barra lateral",
"theme.docs.sidebar.expandButtonTitle": "Expandir barra lateral",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "última versión",
"theme.docs.versions.latestVersionSuggestionLabel": "Para documentación actualizada, ver {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Esta es documentación para {siteTitle} {versionLabel}, que ya no se mantiene activamente.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "بستن نوار کناری",
"theme.docs.sidebar.expandButtonAriaLabel": "باز کردن نوار کناری",
"theme.docs.sidebar.expandButtonTitle": "باز کردن نوار کناری",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "آخرین نسخه",
"theme.docs.versions.latestVersionSuggestionLabel": "برای دیدن آخرین نسخه این متن، نسخه {latestVersionLink} ({versionLabel}) را ببینید.",
"theme.docs.versions.unmaintainedVersionLabel": "نسخه {siteTitle} {versionLabel} دیگر به روزرسانی نمی شود.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Itupî ang sidebar",
"theme.docs.sidebar.expandButtonAriaLabel": "Palakihin ang sidebar",
"theme.docs.sidebar.expandButtonTitle": "Palakihin ang sidebar",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "pinakahuling bersiyón",
"theme.docs.versions.latestVersionSuggestionLabel": "Para sa up-to-date na dokumentasyón, tingnan ang {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Ito ay dokumentasyón para sa {siteTitle} {versionLabel} na hindi na aktibong mine-maintain.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Réduire le menu latéral",
"theme.docs.sidebar.expandButtonAriaLabel": "Déplier le menu latéral",
"theme.docs.sidebar.expandButtonTitle": "Déplier le menu latéral",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} avec \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "Un document tagué|{count} documents tagués",
"theme.docs.versions.latestVersionLinkLabel": "dernière version",
"theme.docs.versions.latestVersionSuggestionLabel": "Pour une documentation à jour, consultez la {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Ceci est la documentation de {siteTitle} {versionLabel}, qui n'est plus activement maintenue.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "סגור",
"theme.docs.sidebar.expandButtonAriaLabel": "פתח",
"theme.docs.sidebar.expandButtonTitle": "פתח",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "גרסא אחרונה",
"theme.docs.versions.latestVersionSuggestionLabel": "לדוקומנטאציה עדכנית, ראה {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "דוקומנטאציה זו {siteTitle} {versionLabel}, כבר לא נתמכת.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "साइडबार बंद करें",
"theme.docs.sidebar.expandButtonAriaLabel": "साइडबार खोलें",
"theme.docs.sidebar.expandButtonTitle": "साइडबार खोलें",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "सबसे नया वर्जन",
"theme.docs.versions.latestVersionSuggestionLabel": "अप-टू-डेट डॉक्यूमेंटेशन के लिए {latestVersionLink} ({versionLabel}) देखें।",
"theme.docs.versions.unmaintainedVersionLabel": "यह {siteTitle} {versionLabel} के लिए डॉक्यूमेंटेशन है, जिसे अब सक्रिय रूप से नहीं बनाए रखा गया है।",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "サイドバーを隠す",
"theme.docs.sidebar.expandButtonAriaLabel": "サイドバーを開く",
"theme.docs.sidebar.expandButtonTitle": "サイドバーを開く",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "最新バージョン",
"theme.docs.versions.latestVersionSuggestionLabel": "最新のドキュメントは{latestVersionLink} ({versionLabel}) を見てください。",
"theme.docs.versions.unmaintainedVersionLabel": "これは{siteTitle} {versionLabel}のドキュメントで現在はアクティブにメンテナンスされていません。",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "사이드바 숨기기",
"theme.docs.sidebar.expandButtonAriaLabel": "사이드바 열기",
"theme.docs.sidebar.expandButtonTitle": "사이드바 열기",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "최신 버전",
"theme.docs.versions.latestVersionSuggestionLabel": "최신 문서는 {latestVersionLink} ({versionLabel})을 확인하세요.",
"theme.docs.versions.unmaintainedVersionLabel": "{siteTitle} {versionLabel} 문서는 업데이트되지 않습니다.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Zwiń boczny panel",
"theme.docs.sidebar.expandButtonAriaLabel": "Rozszerz boczny panel",
"theme.docs.sidebar.expandButtonTitle": "Rozszerz boczny panel",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "bieżącej wersji",
"theme.docs.versions.latestVersionSuggestionLabel": "Aby zobaczyć bieżącą dokumentację, przejdź do wersji {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Ta dokumentacja dotyczy {siteTitle} w wersji {versionLabel} i nie jest już aktywnie aktualizowana.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Fechar painel lateral",
"theme.docs.sidebar.expandButtonAriaLabel": "Expandir painel lateral",
"theme.docs.sidebar.expandButtonTitle": "Expandir painel lateral",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "última versão",
"theme.docs.versions.latestVersionSuggestionLabel": "Para a documentação atualizada, veja: {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Esta é a documentação para {siteTitle} {versionLabel}, que não é mais mantida ativamente.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Colapsar barra lateral",
"theme.docs.sidebar.expandButtonAriaLabel": "Expandir barra lateral",
"theme.docs.sidebar.expandButtonTitle": "Expandir barra lateral",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "última versão",
"theme.docs.versions.latestVersionSuggestionLabel": "Para a documentação atualizada, veja: {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Esta é a documentação para {siteTitle} {versionLabel}, que já não é mantida ativamente.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Свернуть сайдбар",
"theme.docs.sidebar.expandButtonAriaLabel": "Развернуть сайдбар",
"theme.docs.sidebar.expandButtonTitle": "Развернуть сайдбар",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "последней версии",
"theme.docs.versions.latestVersionSuggestionLabel": "Актуальная документация находится на странице {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Это документация {siteTitle} для версии {versionLabel}, которая уже не поддерживается.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Kenar çubuğunu daralt",
"theme.docs.sidebar.expandButtonAriaLabel": "Kenar çubuğunu genişlet",
"theme.docs.sidebar.expandButtonTitle": "Kenar çubuğunu genişlet",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "en son sürüm",
"theme.docs.versions.latestVersionSuggestionLabel": "Güncel belgeler için bkz. {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Bu, {siteTitle} {versionLabel} dokümantasyonudur ve bakımı sonlanmıştır.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "Thu gọn thanh bên",
"theme.docs.sidebar.expandButtonAriaLabel": "Mở rộng thanh bên",
"theme.docs.sidebar.expandButtonTitle": "Mở rộng thanh bên",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "phiên bản mới nhất",
"theme.docs.versions.latestVersionSuggestionLabel": "Để xem các cập nhật mới nhất, vui lòng xem phiên bản {latestVersionLink} ({versionLabel}).",
"theme.docs.versions.unmaintainedVersionLabel": "Đây là tài liệu của {siteTitle} {versionLabel}, hiện không còn được bảo trì.",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "收起侧边栏",
"theme.docs.sidebar.expandButtonAriaLabel": "展开侧边栏",
"theme.docs.sidebar.expandButtonTitle": "展开侧边栏",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "最新版本",
"theme.docs.versions.latestVersionSuggestionLabel": "最新的文档请参阅 {latestVersionLink} ({versionLabel})。",
"theme.docs.versions.unmaintainedVersionLabel": "此为 {siteTitle} {versionLabel} 版的文档,现已不再积极维护。",

View file

@ -42,6 +42,8 @@
"theme.docs.sidebar.collapseButtonTitle": "收起側邊欄",
"theme.docs.sidebar.expandButtonAriaLabel": "展開側邊欄",
"theme.docs.sidebar.expandButtonTitle": "展開側邊欄",
"theme.docs.tagDocListPageTitle": "{nDocsTagged} with \"{tagName}\"",
"theme.docs.tagDocListPageTitle.nDocsTagged": "One doc tagged|{count} docs tagged",
"theme.docs.versions.latestVersionLinkLabel": "最新版本",
"theme.docs.versions.latestVersionSuggestionLabel": "最新的文件請參閱 {latestVersionLink} ({versionLabel})。",
"theme.docs.versions.unmaintainedVersionLabel": "此為 {siteTitle} {versionLabel} 版的文件,現已不再積極維護。",

View file

@ -18,6 +18,7 @@ import EditThisPage from '@theme/EditThisPage';
import type {Props} from '@theme/BlogPostItem';
import styles from './styles.module.css';
import TagsListInline from '@theme/TagsListInline';
// Very simple pluralization: probably good enough for now
function useReadingTimePlural() {
@ -156,22 +157,7 @@ function BlogPostItem(props: Props): JSX.Element {
})}>
{tags.length > 0 && (
<div className="col">
<b>
<Translate
id="theme.tags.tagsListLabel"
description="The label alongside a tag list">
Tags:
</Translate>
</b>
{tags.map(({label, permalink: tagPermalink}) => (
<Link
key={tagPermalink}
className="margin-horiz--sm"
to={tagPermalink}>
{label}
</Link>
))}
<TagsListInline tags={tags} />
</div>
)}

View file

@ -7,51 +7,17 @@
import React from 'react';
import Link from '@docusaurus/Link';
import BlogLayout from '@theme/BlogLayout';
import TagsListByLetter from '@theme/TagsListByLetter';
import type {Props} from '@theme/BlogTagsListPage';
import {translate} from '@docusaurus/Translate';
import {ThemeClassNames} from '@docusaurus/theme-common';
function getCategoryOfTag(tag: string) {
// tag's category should be customizable
return tag[0].toUpperCase();
}
import {
ThemeClassNames,
translateTagsPageTitle,
} from '@docusaurus/theme-common';
function BlogTagsListPage(props: Props): JSX.Element {
const {tags, sidebar} = props;
const title = translate({
id: 'theme.tags.tagsPageTitle',
message: 'Tags',
description: 'The title of the tag list page',
});
const tagCategories: {[category: string]: string[]} = {};
Object.keys(tags).forEach((tag) => {
const category = getCategoryOfTag(tag);
tagCategories[category] = tagCategories[category] || [];
tagCategories[category].push(tag);
});
const tagsList = Object.entries(tagCategories).sort(([a], [b]) =>
a.localeCompare(b),
);
const tagsSection = tagsList
.map(([category, tagsForCategory]) => (
<article key={category}>
<h2>{category}</h2>
{tagsForCategory.map((tag) => (
<Link
className="padding-right--md"
href={tags[tag].permalink}
key={tag}>
{tags[tag].name} ({tags[tag].count})
</Link>
))}
<hr />
</article>
))
.filter((item) => item != null);
const title = translateTagsPageTitle();
return (
<BlogLayout
title={title}
@ -63,7 +29,7 @@ function BlogTagsListPage(props: Props): JSX.Element {
}}
sidebar={sidebar}>
<h1>{title}</h1>
<section className="margin-vert--lg">{tagsSection}</section>
<TagsListByLetter tags={Object.values(tags)} />
</BlogLayout>
);
}

View file

@ -49,7 +49,7 @@ function BlogTagsPostPage(props: Props): JSX.Element {
<BlogLayout
title={title}
wrapperClassName={ThemeClassNames.wrapper.blogPages}
pageClassName={ThemeClassNames.page.blogTagsPostPage}
pageClassName={ThemeClassNames.page.blogTagPostListPage}
searchMetadatas={{
// assign unique search tag to exclude this page from search results!
tag: 'blog_tags_posts',

View file

@ -13,16 +13,15 @@ import useWindowSize from '@theme/hooks/useWindowSize';
import DocPaginator from '@theme/DocPaginator';
import DocVersionBanner from '@theme/DocVersionBanner';
import Seo from '@theme/Seo';
import LastUpdated from '@theme/LastUpdated';
import type {Props} from '@theme/DocItem';
import DocItemFooter from '@theme/DocItemFooter';
import TOC from '@theme/TOC';
import TOCCollapsible from '@theme/TOCCollapsible';
import EditThisPage from '@theme/EditThisPage';
import {MainHeading} from '@theme/Heading';
import styles from './styles.module.css';
function DocItem(props: Props): JSX.Element {
export default function DocItem(props: Props): JSX.Element {
const {content: DocContent, versionMetadata} = props;
const {metadata, frontMatter} = DocContent;
const {
@ -31,14 +30,7 @@ function DocItem(props: Props): JSX.Element {
hide_title: hideTitle,
hide_table_of_contents: hideTableOfContents,
} = frontMatter;
const {
description,
title,
editUrl,
lastUpdatedAt,
formattedLastUpdatedAt,
lastUpdatedBy,
} = metadata;
const {description, title} = metadata;
const {pluginId} = useActivePlugin({failfast: true})!;
const versions = useVersions(pluginId);
@ -98,23 +90,7 @@ function DocItem(props: Props): JSX.Element {
<DocContent />
</div>
{(editUrl || lastUpdatedAt || lastUpdatedBy) && (
<footer className="row docusaurus-mt-lg">
<div className="col">
{editUrl && <EditThisPage editUrl={editUrl} />}
</div>
<div className={clsx('col', styles.lastUpdated)}>
{(lastUpdatedAt || lastUpdatedBy) && (
<LastUpdated
lastUpdatedAt={lastUpdatedAt}
formattedLastUpdatedAt={formattedLastUpdatedAt}
lastUpdatedBy={lastUpdatedBy}
/>
)}
</div>
</footer>
)}
<DocItemFooter {...props} />
</article>
<DocPaginator metadata={metadata} />
@ -129,5 +105,3 @@ function DocItem(props: Props): JSX.Element {
</>
);
}
export default DocItem;

View file

@ -10,21 +10,11 @@
margin-top: 0;
}
.lastUpdated {
margin-top: 0.2rem;
font-style: italic;
font-size: smaller;
}
@media only screen and (min-width: 997px) {
.docItemCol {
max-width: 75% !important;
}
.lastUpdated {
text-align: right;
}
/* Prevent hydration FOUC, as the mobile TOC needs to be server-rendered */
.tocMobile {
display: none;

View file

@ -0,0 +1,90 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import clsx from 'clsx';
import LastUpdated from '@theme/LastUpdated';
import type {Props} from '@theme/DocItem';
import EditThisPage from '@theme/EditThisPage';
import TagsListInline, {
Props as TagsListInlineProps,
} from '@theme/TagsListInline';
import styles from './styles.module.css';
function TagsRow(props: TagsListInlineProps) {
return (
<div className="row margin-bottom--sm">
<div className="col">
<TagsListInline {...props} />
</div>
</div>
);
}
type EditMetaRowProps = Pick<
Props['content']['metadata'],
'editUrl' | 'lastUpdatedAt' | 'lastUpdatedBy' | 'formattedLastUpdatedAt'
>;
function EditMetaRow({
editUrl,
lastUpdatedAt,
lastUpdatedBy,
formattedLastUpdatedAt,
}: EditMetaRowProps) {
return (
<div className="row">
<div className="col">{editUrl && <EditThisPage editUrl={editUrl} />}</div>
<div className={clsx('col', styles.lastUpdated)}>
{(lastUpdatedAt || lastUpdatedBy) && (
<LastUpdated
lastUpdatedAt={lastUpdatedAt}
formattedLastUpdatedAt={formattedLastUpdatedAt}
lastUpdatedBy={lastUpdatedBy}
/>
)}
</div>
</div>
);
}
export default function DocItemFooter(props: Props): JSX.Element {
const {content: DocContent} = props;
const {metadata} = DocContent;
const {
editUrl,
lastUpdatedAt,
formattedLastUpdatedAt,
lastUpdatedBy,
tags,
} = metadata;
const canDisplayTagsRow = tags.length > 0;
const canDisplayEditMetaRow = !!(editUrl || lastUpdatedAt || lastUpdatedBy);
const canDisplayFooter = canDisplayTagsRow || canDisplayEditMetaRow;
if (!canDisplayFooter) {
return <></>;
}
return (
<footer className="docusaurus-mt-lg">
{canDisplayTagsRow && <TagsRow tags={tags} />}
{canDisplayEditMetaRow && (
<EditMetaRow
editUrl={editUrl}
lastUpdatedAt={lastUpdatedAt}
lastUpdatedBy={lastUpdatedBy}
formattedLastUpdatedAt={formattedLastUpdatedAt}
/>
)}
</footer>
);
}

View file

@ -0,0 +1,18 @@
/**
* 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.
*/
.lastUpdated {
margin-top: 0.2rem;
font-style: italic;
font-size: smaller;
}
@media only screen and (min-width: 997px) {
.lastUpdated {
text-align: right;
}
}

View file

@ -56,7 +56,7 @@ function DocPageContent({
return (
<Layout
wrapperClassName={ThemeClassNames.wrapper.docPages}
pageClassName={ThemeClassNames.page.docPage}
pageClassName={ThemeClassNames.page.docsDocPage}
searchMetadatas={{
version,
tag: docVersionSearchTag(pluginId, version),

View file

@ -0,0 +1,89 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import Layout from '@theme/Layout';
import Link from '@docusaurus/Link';
import {ThemeClassNames, usePluralForm} from '@docusaurus/theme-common';
import type {
PropTagDocList,
PropTagDocListDoc,
} from '@docusaurus/plugin-content-docs-types';
import {translate} from '@docusaurus/Translate';
type Props = {
tag: PropTagDocList;
};
// Very simple pluralization: probably good enough for now
function useNDocsTaggedPlural() {
const {selectMessage} = usePluralForm();
return (count: number) =>
selectMessage(
count,
translate(
{
id: 'theme.docs.tagDocListPageTitle.nDocsTagged',
description:
'Pluralized label for "{count} docs tagged". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)',
message: 'One doc tagged|{count} docs tagged',
},
{count},
),
);
}
function DocItem({doc}: {doc: PropTagDocListDoc}): JSX.Element {
return (
<div className="margin-vert--lg">
<Link to={doc.permalink}>
<h2>{doc.title}</h2>
</Link>
{doc.description && <p>{doc.description}</p>}
</div>
);
}
export default function DocTagDocListPage({tag}: Props): JSX.Element {
const nDocsTaggedPlural = useNDocsTaggedPlural();
const title = translate(
{
id: 'theme.docs.tagDocListPageTitle',
description: 'The title of the page for a docs tag',
message: '{nDocsTagged} with "{tagName}"',
},
{nDocsTagged: nDocsTaggedPlural(tag.docs.length), tagName: tag.name},
);
return (
<Layout
title={title}
wrapperClassName={ThemeClassNames.wrapper.docPages}
pageClassName={ThemeClassNames.page.docsTagDocListPage}
searchMetadatas={{
// assign unique search tag to exclude this page from search results!
tag: 'doc_tag_doc_list',
}}>
<div className="container margin-vert--lg">
<div className="row">
<main className="col col--8 col--offset-2">
<header className="margin-bottom--xl">
<h1>{title}</h1>
<Link href={tag.allTagsPath}>View All Tags</Link>
</header>
<div className="margin-vert--lg">
{tag.docs.map((doc) => (
<DocItem key={doc.id} doc={doc} />
))}
</div>
</main>
</div>
</div>
</Layout>
);
}

View file

@ -0,0 +1,41 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import Layout from '@theme/Layout';
import {
ThemeClassNames,
translateTagsPageTitle,
} from '@docusaurus/theme-common';
import TagsListByLetter from '@theme/TagsListByLetter';
import type {Props} from '@theme/DocTagsListPage';
function DocTagsListPage({tags}: Props): JSX.Element {
const title = translateTagsPageTitle();
return (
<Layout
title={title}
wrapperClassName={ThemeClassNames.wrapper.docPages}
pageClassName={ThemeClassNames.page.docsTagsListPage}
searchMetadatas={{
// assign unique search tag to exclude this page from search results!
tag: 'doc_tags_list',
}}>
<div className="container margin-vert--lg">
<div className="row">
<main className="col col--8 col--offset-2">
<h1>{title}</h1>
<TagsListByLetter tags={tags} />
</main>
</div>
</div>
</Layout>
);
}
export default DocTagsListPage;

View file

@ -0,0 +1,44 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import Link from '@docusaurus/Link';
import type {Props} from '@theme/TagsListByLetter';
import {listTagsByLetters, TagLetterEntry} from '@docusaurus/theme-common';
function TagLetterEntryItem({letterEntry}: {letterEntry: TagLetterEntry}) {
return (
<div>
<h2>{letterEntry.letter}</h2>
{letterEntry.tags.map((tag) => (
<Link
className="padding-right--md"
href={tag.permalink}
key={tag.permalink}>
{tag.name} ({tag.count})
</Link>
))}
<hr />
</div>
);
}
function TagsListByLetter({tags}: Props): JSX.Element {
const letterList = listTagsByLetters(tags);
return (
<section className="margin-vert--lg">
{letterList.map((letterEntry) => (
<TagLetterEntryItem
key={letterEntry.letter}
letterEntry={letterEntry}
/>
))}
</section>
);
}
export default TagsListByLetter;

View file

@ -0,0 +1,30 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import Link from '@docusaurus/Link';
import Translate from '@docusaurus/Translate';
import type {Props} from '@theme/TagsListInline';
export default function TagsListInline({tags}: Props) {
return (
<>
<b>
<Translate
id="theme.tags.tagsListLabel"
description="The label alongside a tag list">
Tags:
</Translate>
</b>
{tags.map(({label, permalink: tagPermalink}) => (
<Link key={tagPermalink} className="margin-horiz--sm" to={tagPermalink}>
{label}
</Link>
))}
</>
);
}

View file

@ -703,3 +703,23 @@ declare module '@theme/IconExternalLink' {
const IconExternalLink: (props: Props) => JSX.Element;
export default IconExternalLink;
}
declare module '@theme/TagsListByLetter' {
export type TagsListItem = Readonly<{
name: string;
permalink: string;
count: number;
}>;
export type Props = Readonly<{
tags: readonly TagsListItem[];
}>;
export default function TagsListByLetter(props: Props): JSX.Element;
}
declare module '@theme/TagsListInline' {
export type Tag = Readonly<{label: string; permalink}>;
export type Props = Readonly<{
tags: readonly Tag[];
}>;
export default function TagsListInline(props: Props): JSX.Element;
}

View file

@ -14,6 +14,7 @@ const {mapValues, pickBy, difference, orderBy} = require('lodash');
const CodeDirPaths = [
path.join(__dirname, 'lib-next'),
// TODO other themes should rather define their own translations in the future?
path.join(__dirname, '..', 'docusaurus-theme-common', 'lib'),
path.join(__dirname, '..', 'docusaurus-theme-search-algolia', 'src', 'theme'),
path.join(__dirname, '..', 'docusaurus-theme-live-codeblock', 'src', 'theme'),
path.join(__dirname, '..', 'docusaurus-plugin-pwa', 'src', 'theme'),

View file

@ -14,7 +14,7 @@ const {mapValues, pickBy} = require('lodash');
jest.setTimeout(15000);
describe('update-code-translations', () => {
test(`to have base.json contain all the translations extracted from the theme. Please run "yarn workspace @docusaurus/theme-classic update-code-translations" to keep base.json up-to-date.`, async () => {
test(`to have base.json contain EXACTLY all the translations extracted from the theme. Please run "yarn workspace @docusaurus/theme-classic update-code-translations" to keep base.json up-to-date.`, async () => {
const baseMessages = pickBy(
JSON.parse(
await fs.readFile(

View file

@ -28,7 +28,8 @@
"tslib": "^2.1.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.0.0-beta.4"
"@docusaurus/module-type-aliases": "2.0.0-beta.4",
"lodash": "^4.17.20"
},
"peerDependencies": {
"prism-react-renderer": "^1.2.1",

View file

@ -68,3 +68,6 @@ export {
} from './utils/announcementBarUtils';
export {useLocalPathname} from './utils/useLocalPathname';
export {translateTagsPageTitle, listTagsByLetters} from './utils/tagsUtils';
export type {TagLetterEntry} from './utils/tagsUtils';

View file

@ -11,8 +11,12 @@ export const ThemeClassNames = {
blogListPage: 'blog-list-page',
blogPostPage: 'blog-post-page',
blogTagsListPage: 'blog-tags-list-page',
blogTagsPostPage: 'blog-tags-post-page',
docPage: 'doc-page',
blogTagPostListPage: 'blog-tags-post-list-page',
docsDocPage: 'docs-doc-page',
docsTagsListPage: 'docs-tags-list-page', // List of tags
docsTagDocListPage: 'docs-tags-doc-list-page', // Docs for a tag
mdxPage: 'mdx-page',
},
wrapper: {

View file

@ -0,0 +1,66 @@
/**
* 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 {shuffle} from 'lodash';
import {listTagsByLetters} from '../tagsUtils';
describe('listTagsByLetters', () => {
type Param = Parameters<typeof listTagsByLetters>[0];
type Tag = Param[number];
type Result = ReturnType<typeof listTagsByLetters>;
test('Should create letters list', () => {
const tag1: Tag = {
name: 'tag1',
permalink: '/tag1',
count: 1,
};
const tag2: Tag = {
name: 'Tag2',
permalink: '/tag2',
count: 11,
};
const tagzxy: Tag = {
name: 'zxy',
permalink: '/zxy',
count: 987,
};
const tagAbc: Tag = {
name: 'Abc',
permalink: '/abc',
count: 123,
};
const tagdef: Tag = {
name: 'def',
permalink: '/def',
count: 1,
};
const tagaaa: Tag = {
name: 'aaa',
permalink: '/aaa',
count: 10,
};
const expectedResult: Result = [
{letter: 'A', tags: [tagaaa, tagAbc]},
{letter: 'D', tags: [tagdef]},
{letter: 'T', tags: [tag1, tag2]},
{letter: 'Z', tags: [tagzxy]},
];
// Input order shouldn't matter, output is always consistently sorted
expect(
listTagsByLetters([tag1, tag2, tagzxy, tagAbc, tagdef, tagaaa]),
).toEqual(expectedResult);
expect(
listTagsByLetters([tagzxy, tagdef, tagaaa, tag2, tagAbc, tag1]),
).toEqual(expectedResult);
expect(
listTagsByLetters(shuffle([tagzxy, tagdef, tagaaa, tag2, tagAbc, tag1])),
).toEqual(expectedResult);
});
});

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 {translate} from '@docusaurus/Translate';
export const translateTagsPageTitle = () =>
translate({
id: 'theme.tags.tagsPageTitle',
message: 'Tags',
description: 'The title of the tag list page',
});
type TagsListItem = Readonly<{name: string; permalink: string; count: number}>; // TODO remove duplicated type :s
export type TagLetterEntry = Readonly<{letter: string; tags: TagsListItem[]}>;
function getTagLetter(tag: string): string {
return tag[0].toUpperCase();
}
export function listTagsByLetters(
tags: readonly TagsListItem[],
): TagLetterEntry[] {
// Group by letters
const groups: Record<string, TagsListItem[]> = {};
Object.values(tags).forEach((tag) => {
const letter = getTagLetter(tag.name);
groups[letter] = groups[letter] ?? [];
groups[letter].push(tag);
});
return (
Object.entries(groups)
// Sort letters
.sort(([letter1], [letter2]) => letter1.localeCompare(letter2))
.map(([letter, letterTags]) => {
// Sort tags inside a letter
const sortedTags = letterTags.sort((tag1, tag2) =>
tag1.name.localeCompare(tag2.name),
);
return {letter, tags: sortedTags};
})
);
}

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.
*/
import Joi from './Joi';
// Enhance the default Joi.string() type so that it can convert number to strings
// If user use frontmatter "tag: 2021", we shouldn't need to ask the user to write "tag: '2021'"
// Also yaml tries to convert patterns like "2019-01-01" to dates automatically
// see https://github.com/facebook/docusaurus/issues/4642
// see https://github.com/sideway/joi/issues/1442#issuecomment-823997884
const JoiFrontMatterString: Joi.Extension = {
type: 'string',
base: Joi.string(),
// Fix Yaml that tries to auto-convert many things to string out of the box
prepare: (value) => {
if (typeof value === 'number' || value instanceof Date) {
return {value: value.toString()};
}
return {value};
},
};
export const JoiFrontMatter: typeof Joi = Joi.extend(JoiFrontMatterString);

View file

@ -6,7 +6,8 @@
*/
import Joi from '../Joi';
import {JoiFrontMatter, validateFrontMatter} from '../validationUtils';
import {JoiFrontMatter} from '../JoiFrontMatter';
import {validateFrontMatter} from '../validationUtils';
describe('validateFrontMatter', () => {
test('should accept good values', () => {

View file

@ -7,6 +7,7 @@
// /!\ don't remove this export, as we recommend plugin authors to use it
export {default as Joi} from './Joi';
export {JoiFrontMatter} from './JoiFrontMatter';
export * from './validationUtils';
export * from './validationSchemas';

View file

@ -6,6 +6,8 @@
*/
import Joi from './Joi';
import {isValidPathname} from '@docusaurus/utils';
import type {Tag} from '@docusaurus/utils';
import {JoiFrontMatter} from './JoiFrontMatter';
export const PluginIdSchema = Joi.string()
.regex(/^[a-zA-Z_-]+$/)
@ -55,3 +57,13 @@ export const PathnameSchema = Joi.string()
.message(
'{{#label}} is not a valid pathname. Pathname should start with slash and not contain any domain or query string.',
);
export const FrontMatterTagsSchema = JoiFrontMatter.array().items(
JoiFrontMatter.alternatives().try(
JoiFrontMatter.string().required(),
JoiFrontMatter.object<Tag>({
label: JoiFrontMatter.string().required(),
permalink: JoiFrontMatter.string().required(),
}).required(),
),
);

View file

@ -99,24 +99,6 @@ export function normalizeThemeConfig<T>(
return value;
}
// Enhance the default Joi.string() type so that it can convert number to strings
// If user use frontmatter "tag: 2021", we shouldn't need to ask the user to write "tag: '2021'"
// Also yaml tries to convert patterns like "2019-01-01" to dates automatically
// see https://github.com/facebook/docusaurus/issues/4642
// see https://github.com/sideway/joi/issues/1442#issuecomment-823997884
const JoiFrontMatterString: Joi.Extension = {
type: 'string',
base: Joi.string(),
// Fix Yaml that tries to auto-convert many things to string out of the box
prepare: (value) => {
if (typeof value === 'number' || value instanceof Date) {
return {value: value.toString()};
}
return {value};
},
};
export const JoiFrontMatter: typeof Joi = Joi.extend(JoiFrontMatterString);
export function validateFrontMatter<T>(
frontMatter: Record<string, unknown>,
schema: Joi.ObjectSchema<T>,

View file

@ -12,7 +12,6 @@ import {
genChunkName,
idx,
getSubFolder,
normalizeUrl,
posixPath,
objectWithKeySorted,
aliasedSitePath,
@ -218,113 +217,6 @@ describe('load utils', () => {
expect(getSubFolder(testE, 'docs')).toBeNull();
});
test('normalizeUrl', () => {
const asserts = [
{
input: ['/', ''],
output: '/',
},
{
input: ['', '/'],
output: '/',
},
{
input: ['/'],
output: '/',
},
{
input: [''],
output: '',
},
{
input: ['/', '/'],
output: '/',
},
{
input: ['/', 'docs'],
output: '/docs',
},
{
input: ['/', 'docs', 'en', 'next', 'blog'],
output: '/docs/en/next/blog',
},
{
input: ['/test/', '/docs', 'ro', 'doc1'],
output: '/test/docs/ro/doc1',
},
{
input: ['/test/', '/', 'ro', 'doc1'],
output: '/test/ro/doc1',
},
{
input: ['/', '/', '2020/02/29/leap-day'],
output: '/2020/02/29/leap-day',
},
{
input: ['', '/', 'ko', 'hello'],
output: '/ko/hello',
},
{
input: ['hello', 'world'],
output: 'hello/world',
},
{
input: ['http://www.google.com/', 'foo/bar', '?test=123'],
output: 'http://www.google.com/foo/bar?test=123',
},
{
input: ['http:', 'www.google.com///', 'foo/bar', '?test=123'],
output: 'http://www.google.com/foo/bar?test=123',
},
{
input: ['http://foobar.com', '', 'test'],
output: 'http://foobar.com/test',
},
{
input: ['http://foobar.com', '', 'test', '/'],
output: 'http://foobar.com/test/',
},
{
input: ['/', '', 'hello', '', '/', '/', '', '/', '/world'],
output: '/hello/world',
},
{
input: ['', '', '/tt', 'ko', 'hello'],
output: '/tt/ko/hello',
},
{
input: ['', '///hello///', '', '///world'],
output: '/hello/world',
},
{
input: ['', '/hello/', ''],
output: '/hello/',
},
{
input: ['', '/', ''],
output: '/',
},
{
input: ['///', '///'],
output: '/',
},
{
input: ['/', '/hello/world/', '///'],
output: '/hello/world/',
},
];
asserts.forEach((testCase) => {
expect(normalizeUrl(testCase.input)).toBe(testCase.output);
});
expect(() =>
// @ts-expect-error undefined for test
normalizeUrl(['http:example.com', undefined]),
).toThrowErrorMatchingInlineSnapshot(
`"Url must be a string. Received undefined"`,
);
});
test('isValidPathname', () => {
expect(isValidPathname('/')).toBe(true);
expect(isValidPathname('/hey')).toBe(true);

View file

@ -0,0 +1,117 @@
/**
* 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 {normalizeUrl} from '../normalizeUrl';
describe('normalizeUrl', () => {
test('should normalize urls correctly', () => {
const asserts = [
{
input: ['/', ''],
output: '/',
},
{
input: ['', '/'],
output: '/',
},
{
input: ['/'],
output: '/',
},
{
input: [''],
output: '',
},
{
input: ['/', '/'],
output: '/',
},
{
input: ['/', 'docs'],
output: '/docs',
},
{
input: ['/', 'docs', 'en', 'next', 'blog'],
output: '/docs/en/next/blog',
},
{
input: ['/test/', '/docs', 'ro', 'doc1'],
output: '/test/docs/ro/doc1',
},
{
input: ['/test/', '/', 'ro', 'doc1'],
output: '/test/ro/doc1',
},
{
input: ['/', '/', '2020/02/29/leap-day'],
output: '/2020/02/29/leap-day',
},
{
input: ['', '/', 'ko', 'hello'],
output: '/ko/hello',
},
{
input: ['hello', 'world'],
output: 'hello/world',
},
{
input: ['http://www.google.com/', 'foo/bar', '?test=123'],
output: 'http://www.google.com/foo/bar?test=123',
},
{
input: ['http:', 'www.google.com///', 'foo/bar', '?test=123'],
output: 'http://www.google.com/foo/bar?test=123',
},
{
input: ['http://foobar.com', '', 'test'],
output: 'http://foobar.com/test',
},
{
input: ['http://foobar.com', '', 'test', '/'],
output: 'http://foobar.com/test/',
},
{
input: ['/', '', 'hello', '', '/', '/', '', '/', '/world'],
output: '/hello/world',
},
{
input: ['', '', '/tt', 'ko', 'hello'],
output: '/tt/ko/hello',
},
{
input: ['', '///hello///', '', '///world'],
output: '/hello/world',
},
{
input: ['', '/hello/', ''],
output: '/hello/',
},
{
input: ['', '/', ''],
output: '/',
},
{
input: ['///', '///'],
output: '/',
},
{
input: ['/', '/hello/world/', '///'],
output: '/hello/world/',
},
];
asserts.forEach((testCase) => {
expect(normalizeUrl(testCase.input)).toBe(testCase.output);
});
expect(() =>
// @ts-expect-error undefined for test
normalizeUrl(['http:example.com', undefined]),
).toThrowErrorMatchingInlineSnapshot(
`"Url must be a string. Received undefined"`,
);
});
});

View file

@ -0,0 +1,183 @@
/**
* 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 {
normalizeFrontMatterTag,
normalizeFrontMatterTags,
groupTaggedItems,
Tag,
} from '../tags';
describe('normalizeFrontMatterTag', () => {
type Input = Parameters<typeof normalizeFrontMatterTag>[1];
type Output = ReturnType<typeof normalizeFrontMatterTag>;
test('should normalize simple string tag', () => {
const tagsPath = '/all/tags';
const input: Input = 'tag';
const expectedOutput: Output = {
label: 'tag',
permalink: `${tagsPath}/tag`,
};
expect(normalizeFrontMatterTag(tagsPath, input)).toEqual(expectedOutput);
});
test('should normalize complex string tag', () => {
const tagsPath = '/all/tags';
const input: Input = 'some more Complex_tag';
const expectedOutput: Output = {
label: 'some more Complex_tag',
permalink: `${tagsPath}/some-more-complex-tag`,
};
expect(normalizeFrontMatterTag(tagsPath, input)).toEqual(expectedOutput);
});
test('should normalize simple object tag', () => {
const tagsPath = '/all/tags';
const input: Input = {label: 'tag', permalink: 'tagPermalink'};
const expectedOutput: Output = {
label: 'tag',
permalink: `${tagsPath}/tagPermalink`,
};
expect(normalizeFrontMatterTag(tagsPath, input)).toEqual(expectedOutput);
});
test('should normalize complex string tag', () => {
const tagsPath = '/all/tags';
const input: Input = {
label: 'tag complex Label',
permalink: '/MoreComplex/Permalink',
};
const expectedOutput: Output = {
label: 'tag complex Label',
permalink: `${tagsPath}/MoreComplex/Permalink`,
};
expect(normalizeFrontMatterTag(tagsPath, input)).toEqual(expectedOutput);
});
});
describe('normalizeFrontMatterTags', () => {
type Input = Parameters<typeof normalizeFrontMatterTags>[1];
type Output = ReturnType<typeof normalizeFrontMatterTags>;
test('should normalize string list', () => {
const tagsPath = '/all/tags';
const input: Input = ['tag 1', 'tag-1', 'tag 3', 'tag1', 'tag-2'];
// Keep user input order but remove tags that lead to same permalink
const expectedOutput: Output = [
{
label: 'tag 1',
permalink: `${tagsPath}/tag-1`,
},
{
label: 'tag 3',
permalink: `${tagsPath}/tag-3`,
},
{
label: 'tag-2',
permalink: `${tagsPath}/tag-2`,
},
];
expect(normalizeFrontMatterTags(tagsPath, input)).toEqual(expectedOutput);
});
test('should normalize complex mixed list', () => {
const tagsPath = '/all/tags';
const input: Input = [
'tag 1',
{label: 'tag-1', permalink: '/tag-1'},
'tag 3',
'tag1',
{label: 'tag 4', permalink: '/tag4Permalink'},
];
// Keep user input order but remove tags that lead to same permalink
const expectedOutput: Output = [
{
label: 'tag 1',
permalink: `${tagsPath}/tag-1`,
},
{
label: 'tag 3',
permalink: `${tagsPath}/tag-3`,
},
{
label: 'tag 4',
permalink: `${tagsPath}/tag4Permalink`,
},
];
expect(normalizeFrontMatterTags(tagsPath, input)).toEqual(expectedOutput);
});
});
describe('groupTaggedItems', () => {
type SomeTaggedItem = {
id: string;
nested: {
tags: Tag[];
};
};
function groupItems(items: SomeTaggedItem[]) {
return groupTaggedItems(items, (item) => item.nested.tags);
}
type Input = Parameters<typeof groupItems>[0];
type Output = ReturnType<typeof groupItems>;
test('should group items by tag permalink', () => {
const tagGuide = {label: 'Guide', permalink: '/guide'};
const tagTutorial = {label: 'Tutorial', permalink: '/tutorial'};
const tagAPI = {label: 'API', permalink: '/api'};
// This one will be grouped under same permalink and label is ignored
const tagTutorialOtherLabel = {
label: 'TutorialOtherLabel',
permalink: '/tutorial',
};
const item1: SomeTaggedItem = {
id: '1',
nested: {
tags: [
tagGuide,
tagTutorial,
tagAPI,
// Add some duplicates on purpose: they should be filtered
tagGuide,
tagTutorialOtherLabel,
],
},
};
const item2: SomeTaggedItem = {
id: '2',
nested: {
tags: [tagAPI],
},
};
const item3: SomeTaggedItem = {
id: '3',
nested: {
tags: [tagTutorial],
},
};
const item4: SomeTaggedItem = {
id: '4',
nested: {
tags: [tagTutorialOtherLabel],
},
};
const input: Input = [item1, item2, item3, item4];
const expectedOutput: Output = {
'/guide': {tag: tagGuide, items: [item1]},
'/tutorial': {tag: tagTutorial, items: [item1, item3, item4]},
'/api': {tag: tagAPI, items: [item1, item2]},
};
expect(groupItems(input)).toEqual(expectedOutput);
});
});

View file

@ -23,6 +23,10 @@ import resolvePathnameUnsafe from 'resolve-pathname';
import {posixPath as posixPathImport} from './posixPath';
import {simpleHash, docuHash} from './hashUtils';
import {normalizeUrl} from './normalizeUrl';
export * from './normalizeUrl';
export * from './tags';
export const posixPath = posixPathImport;
@ -190,80 +194,6 @@ export function getSubFolder(file: string, refDir: string): string | null {
return match && match[1];
}
export function normalizeUrl(rawUrls: string[]): string {
const urls = [...rawUrls];
const resultArray = [];
let hasStartingSlash = false;
let hasEndingSlash = false;
// If the first part is a plain protocol, we combine it with the next part.
if (urls[0].match(/^[^/:]+:\/*$/) && urls.length > 1) {
const first = urls.shift();
urls[0] = first + urls[0];
}
// There must be two or three slashes in the file protocol,
// two slashes in anything else.
const replacement = urls[0].match(/^file:\/\/\//) ? '$1:///' : '$1://';
urls[0] = urls[0].replace(/^([^/:]+):\/*/, replacement);
// eslint-disable-next-line
for (let i = 0; i < urls.length; i++) {
let component = urls[i];
if (typeof component !== 'string') {
throw new TypeError(`Url must be a string. Received ${typeof component}`);
}
if (component === '') {
if (i === urls.length - 1 && hasEndingSlash) {
resultArray.push('/');
}
// eslint-disable-next-line
continue;
}
if (component !== '/') {
if (i > 0) {
// Removing the starting slashes for each component but the first.
component = component.replace(
/^[/]+/,
// Special case where the first element of rawUrls is empty ["", "/hello"] => /hello
component[0] === '/' && !hasStartingSlash ? '/' : '',
);
}
hasEndingSlash = component[component.length - 1] === '/';
// Removing the ending slashes for each component but the last.
// For the last component we will combine multiple slashes to a single one.
component = component.replace(/[/]+$/, i < urls.length - 1 ? '' : '/');
}
hasStartingSlash = true;
resultArray.push(component);
}
let str = resultArray.join('/');
// Each input component is now separated by a single slash
// except the possible first plain protocol part.
// Remove trailing slash before parameters or hash.
str = str.replace(/\/(\?|&|#[^!])/g, '$1');
// Replace ? in parameters with &.
const parts = str.split('?');
str = parts.shift() + (parts.length > 0 ? '?' : '') + parts.join('&');
// Dedupe forward slashes in the entire path, avoiding protocol slashes.
str = str.replace(/([^:]\/)\/+/g, '$1');
// Dedupe forward slashes at the beginning of the path.
str = str.replace(/^\/+/g, '/');
return str;
}
/**
* Alias filepath relative to site directory, very useful so that we
* don't expose user's site structure.

View file

@ -0,0 +1,80 @@
/**
* 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 function normalizeUrl(rawUrls: string[]): string {
const urls = [...rawUrls];
const resultArray = [];
let hasStartingSlash = false;
let hasEndingSlash = false;
// If the first part is a plain protocol, we combine it with the next part.
if (urls[0].match(/^[^/:]+:\/*$/) && urls.length > 1) {
const first = urls.shift();
urls[0] = first + urls[0];
}
// There must be two or three slashes in the file protocol,
// two slashes in anything else.
const replacement = urls[0].match(/^file:\/\/\//) ? '$1:///' : '$1://';
urls[0] = urls[0].replace(/^([^/:]+):\/*/, replacement);
// eslint-disable-next-line
for (let i = 0; i < urls.length; i++) {
let component = urls[i];
if (typeof component !== 'string') {
throw new TypeError(`Url must be a string. Received ${typeof component}`);
}
if (component === '') {
if (i === urls.length - 1 && hasEndingSlash) {
resultArray.push('/');
}
// eslint-disable-next-line
continue;
}
if (component !== '/') {
if (i > 0) {
// Removing the starting slashes for each component but the first.
component = component.replace(
/^[/]+/,
// Special case where the first element of rawUrls is empty ["", "/hello"] => /hello
component[0] === '/' && !hasStartingSlash ? '/' : '',
);
}
hasEndingSlash = component[component.length - 1] === '/';
// Removing the ending slashes for each component but the last.
// For the last component we will combine multiple slashes to a single one.
component = component.replace(/[/]+$/, i < urls.length - 1 ? '' : '/');
}
hasStartingSlash = true;
resultArray.push(component);
}
let str = resultArray.join('/');
// Each input component is now separated by a single slash
// except the possible first plain protocol part.
// Remove trailing slash before parameters or hash.
str = str.replace(/\/(\?|&|#[^!])/g, '$1');
// Replace ? in parameters with &.
const parts = str.split('?');
str = parts.shift() + (parts.length > 0 ? '?' : '') + parts.join('&');
// Dedupe forward slashes in the entire path, avoiding protocol slashes.
str = str.replace(/([^:]\/)\/+/g, '$1');
// Dedupe forward slashes at the beginning of the path.
str = str.replace(/^\/+/g, '/');
return str;
}

View file

@ -0,0 +1,100 @@
/**
* 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 {kebabCase, uniq, uniqBy} from 'lodash';
import {normalizeUrl} from './normalizeUrl';
export type Tag = {
label: string;
permalink: string;
};
export type FrontMatterTag = string | Tag;
export function normalizeFrontMatterTag(
tagsPath: string,
frontMatterTag: FrontMatterTag,
): Tag {
function toTagObject(tagString: string): Tag {
return {
label: tagString,
permalink: kebabCase(tagString),
};
}
// TODO maybe make ensure the permalink is valid url path?
function normalizeTagPermalink(permalink: string): string {
// note: we always apply tagsPath on purpose
// for versioned docs, v1/doc.md and v2/doc.md tags with custom permalinks don't lead to the same created page
// tagsPath is different for each doc version
return normalizeUrl([tagsPath, permalink]);
}
const tag: Tag =
typeof frontMatterTag === 'string'
? toTagObject(frontMatterTag)
: frontMatterTag;
return {
label: tag.label,
permalink: normalizeTagPermalink(tag.permalink),
};
}
export function normalizeFrontMatterTags(
tagsPath: string,
frontMatterTags: FrontMatterTag[] | undefined,
): Tag[] {
const tags =
frontMatterTags?.map((tag) => normalizeFrontMatterTag(tagsPath, tag)) ?? [];
return uniqBy(tags, (tag) => tag.permalink);
}
export type TaggedItemGroup<Item> = {
tag: Tag;
items: Item[];
};
// Permits to group docs/blogPosts by tag (provided by FrontMatter)
// Note: groups are indexed by permalink, because routes must be unique in the end
// Labels may vary on 2 md files but they are normalized.
// Docs with label='some label' and label='some-label' should end-up in the same group/page in the end
// We can't create 2 routes /some-label because one would override the other
export function groupTaggedItems<Item>(
items: Item[],
getItemTags: (item: Item) => Tag[],
): Record<string, TaggedItemGroup<Item>> {
const result: Record<string, TaggedItemGroup<Item>> = {};
function handleItemTag(item: Item, tag: Tag) {
// Init missing tag groups
// TODO: it's not really clear what should be the behavior if 2 items have the same tag but the permalink is different for each
// For now, the first tag found wins
result[tag.permalink] = result[tag.permalink] ?? {
tag,
items: [],
};
// Add item to group
result[tag.permalink].items.push(item);
}
items.forEach((item) => {
getItemTags(item).forEach((tag) => {
handleItemTag(item, tag);
});
});
// If user add twice the same tag to a md doc (weird but possible),
// we don't want the item to appear twice in the list...
Object.values(result).forEach((group) => {
group.items = uniq(group.items);
});
return result;
}

View file

@ -1,5 +1,6 @@
---
slug: /
tags: [a, b, c, some tag]
---
# Docs tests

View file

@ -1,3 +1,7 @@
---
tags: [a, e, some-tag, some_tag]
---
# Another test page
[Test link](./folder%20with%20space/doc%201.md)

View file

@ -1,3 +1,10 @@
---
tags:
- b
- label: d
permalink: d-custom-permalink
---
# Standalone doc
This doc is not in any sidebar, on purpose, to measure the build size impact of the huge sidebar

View file

@ -42,6 +42,8 @@ Accepted fields:
| `numberPrefixParser` | <code>boolean &#124; PrefixParser</code> | _Omitted_ | 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. See also [Using number prefixes](/docs/sidebar#using-number-prefixes) |
| `docLayoutComponent` | `string` | `'@theme/DocPage'` | Root Layout component of each doc page. |
| `docItemComponent` | `string` | `'@theme/DocItem'` | Main doc container, with TOC, pagination, etc. |
| `docTagsListComponent` | `string` | `'@theme/DocTagsListPage'` | Root component of the tags list page |
| `docTagDocListComponent` | `string` | `'@theme/DocTagDocListPage'` | Root component of the "docs containing tag" page. |
| `remarkPlugins` | `any[]` | `[]` | Remark plugins passed to MDX. |
| `rehypePlugins` | `any[]` | `[]` | Rehype plugins passed to MDX. |
| `beforeDefaultRemarkPlugins` | `any[]` | `[]` | Custom Remark plugins passed to MDX before the default Docusaurus Remark plugins. |
@ -249,9 +251,14 @@ Accepted fields:
| `description` | `string` | The first line of Markdown content | The description of your document, which will become the `<meta name="description" content="..."/>` and `<meta property="og:description" content="..."/>` in `<head>`, used by search engines. |
| `image` | `string` | `undefined` | Cover or thumbnail image that will be used when displaying the link to your post. |
| `slug` | `string` | File path | Allows to customize the document url (`/<routeBasePath>/<slug>`). Support multiple patterns: `slug: my-doc`, `slug: /my/path/myDoc`, `slug: /`. |
| `tags` | `Tag[]` | `undefined` | A list of strings or objects of two string fields `label` and `permalink` to tag to your docs. |
</small>
```typescript
type Tag = string | {label: string; permalink: string};
```
Example:
```yml