diff --git a/jest.config.js b/jest.config.js index 9d0ecc0d2b..cf9c4a1707 100644 --- a/jest.config.js +++ b/jest.config.js @@ -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': '/jest/emptyModule.js', }, }; diff --git a/jest/emptyModule.js b/jest/emptyModule.js new file mode 100644 index 0000000000..ec757d00e9 --- /dev/null +++ b/jest/emptyModule.js @@ -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 = {}; diff --git a/packages/docusaurus-plugin-content-blog/src/blogFrontMatter.ts b/packages/docusaurus-plugin-content-blog/src/blogFrontMatter.ts index 2d8490a5be..4d0787d055 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogFrontMatter.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogFrontMatter.ts @@ -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({ - label: Joi.string().required(), - permalink: Joi.string().required(), - }), -); - const BlogFrontMatterSchema = Joi.object({ 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(), diff --git a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts index 5cc08c5957..f63527cf24 100644 --- a/packages/docusaurus-plugin-content-blog/src/blogUtils.ts +++ b/packages/docusaurus-plugin-content-blog/src/blogUtils.ts @@ -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 = /^(?\d{4}[-/]\d{1,2}[-/]\d{1,2})[-/]?(?.*?)(\/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, }, diff --git a/packages/docusaurus-plugin-content-blog/src/index.ts b/packages/docusaurus-plugin-content-blog/src/index.ts index 6789078fda..6caba758cb 100644 --- a/packages/docusaurus-plugin-content-blog/src/index.ts +++ b/packages/docusaurus-plugin-content-blog/src/index.ts @@ -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: { diff --git a/packages/docusaurus-plugin-content-blog/src/types.ts b/packages/docusaurus-plugin-content-blog/src/types.ts index c7d3f3d3bb..b64238a7cf 100644 --- a/packages/docusaurus-plugin-content-blog/src/types.ts +++ b/packages/docusaurus-plugin-content-blog/src/types.ts @@ -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; } diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/simple-site/docs/foo/baz.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/simple-site/docs/foo/baz.md index 1816c615ac..95d0486704 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/simple-site/docs/foo/baz.md +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/simple-site/docs/foo/baz.md @@ -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 diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/simple-site/docs/hello.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/simple-site/docs/hello.md index 24d8b672e1..6671f038f7 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/simple-site/docs/hello.md +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/simple-site/docs/hello.md @@ -2,6 +2,7 @@ id: hello title: Hello, World ! sidebar_label: Hello sidebar_label +tags: [tag-1, tag 3] --- Hi, Endilie here :) diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/docs/foo/bar.md b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/docs/foo/bar.md index 2ad77f287c..0945068f6b 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/docs/foo/bar.md +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__fixtures__/versioned-site/docs/foo/bar.md @@ -1,4 +1,10 @@ --- slug: barSlug +tags: + - barTag 1 + - barTag-2 + - label: barTag 3 + permalink: barTag-3-permalink --- + This is `next` version of bar. diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap index 477eecdb76..0400a366e4 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/__snapshots__/index.test.ts.snap @@ -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, diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts index 8380a931b1..b3bfb75d76 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/docs.test.ts @@ -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: [], }); }); }); diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts index a3382b2621..d75192ed7c 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/index.test.ts @@ -60,6 +60,7 @@ const defaultDocMetadata: Partial = { 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, diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/options.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/options.test.ts index cb50e80d62..163fca0a00 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/options.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/options.test.ts @@ -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: [], diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/props.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/props.test.ts new file mode 100644 index 0000000000..8a32f9d246 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/props.test.ts @@ -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[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 + }); + }); +}); diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/versions.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/versions.test.ts index 43e27bdc21..fba76fd6da 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/versions.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/versions.test.ts @@ -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', }, diff --git a/packages/docusaurus-plugin-content-docs/src/docs.ts b/packages/docusaurus-plugin-content-docs/src/docs.ts index 29f0a1e1e0..8f92614e57 100644 --- a/packages/docusaurus-plugin-content-docs/src/docs.ts +++ b/packages/docusaurus-plugin-content-docs/src/docs.ts @@ -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, diff --git a/packages/docusaurus-plugin-content-docs/src/index.ts b/packages/docusaurus-plugin-content-docs/src/index.ts index 30125c5dbe..c2ba9a2312 100644 --- a/packages/docusaurus-plugin-content-docs/src/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/index.ts @@ -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 { + await createVersionTagsRoutes(loadedVersion); + const versionMetadata = toVersionMetadataProp(pluginId, loadedVersion); const versionMetadataPropPath = await createData( `${docuHash( diff --git a/packages/docusaurus-plugin-content-docs/src/options.ts b/packages/docusaurus-plugin-content-docs/src/options.ts index 49891129d6..87b5dbe9b4 100644 --- a/packages/docusaurus-plugin-content-docs/src/options.ts +++ b/packages/docusaurus-plugin-content-docs/src/options.ts @@ -33,6 +33,8 @@ export const DEFAULT_OPTIONS: Omit = { 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( diff --git a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts index cc4b1314f6..1cf3ca72b5 100644 --- a/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts +++ b/packages/docusaurus-plugin-content-docs/src/plugin-content-docs.d.ts @@ -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'; diff --git a/packages/docusaurus-plugin-content-docs/src/props.ts b/packages/docusaurus-plugin-content-docs/src/props.ts index a9c3446725..a8ec05977f 100644 --- a/packages/docusaurus-plugin-content-docs/src/props.ts +++ b/packages/docusaurus-plugin-content-docs/src/props.ts @@ -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[]; +}): 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, + }; +} diff --git a/packages/docusaurus-plugin-content-docs/src/tags.ts b/packages/docusaurus-plugin-content-docs/src/tags.ts new file mode 100644 index 0000000000..ec49a7e731 --- /dev/null +++ b/packages/docusaurus-plugin-content-docs/src/tags.ts @@ -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, + }; + }); +} diff --git a/packages/docusaurus-plugin-content-docs/src/types.ts b/packages/docusaurus-plugin-content-docs/src/types.ts index 414d83e25c..400da88f95 100644 --- a/packages/docusaurus-plugin-content-docs/src/types.ts +++ b/packages/docusaurus-plugin-content-docs/src/types.ts @@ -9,7 +9,8 @@ /// 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; 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; }; @@ -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; diff --git a/packages/docusaurus-plugin-content-docs/src/versions.ts b/packages/docusaurus-plugin-content-docs/src/versions.ts index 5cfbb4560a..77284ea68b 100644 --- a/packages/docusaurus-plugin-content-docs/src/versions.ts +++ b/packages/docusaurus-plugin-content-docs/src/versions.ts @@ -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({ diff --git a/packages/docusaurus-theme-bootstrap/src/theme/DocTagsListPage/index.tsx b/packages/docusaurus-theme-bootstrap/src/theme/DocTagsListPage/index.tsx new file mode 100644 index 0000000000..56dda79d11 --- /dev/null +++ b/packages/docusaurus-theme-bootstrap/src/theme/DocTagsListPage/index.tsx @@ -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) => ( + + {tags[tag].name}{' '} + {tags[tag].count} + + ))} + + ); + + return ( + +
+

Tags

+
    {renderAllTags()}
+
+
+ ); +} + +export default DocTagsListPage; diff --git a/packages/docusaurus-theme-classic/codeTranslations/ar.json b/packages/docusaurus-theme-classic/codeTranslations/ar.json index b14faa0c95..e9cfb4b817 100644 --- a/packages/docusaurus-theme-classic/codeTranslations/ar.json +++ b/packages/docusaurus-theme-classic/codeTranslations/ar.json @@ -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}، التي لم تعد تتم صيانتها بشكل نشط.", diff --git a/packages/docusaurus-theme-classic/codeTranslations/base.json b/packages/docusaurus-theme-classic/codeTranslations/base.json index 0a333c0e4b..b7b488bb02 100644 --- a/packages/docusaurus-theme-classic/codeTranslations/base.json +++ b/packages/docusaurus-theme-classic/codeTranslations/base.json @@ -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}).", diff --git a/packages/docusaurus-theme-classic/codeTranslations/bn.json b/packages/docusaurus-theme-classic/codeTranslations/bn.json index 1edfd88015..666d9dca50 100644 --- a/packages/docusaurus-theme-classic/codeTranslations/bn.json +++ b/packages/docusaurus-theme-classic/codeTranslations/bn.json @@ -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} এর জন্যে ডকুমেন্টেশন, যা আর সক্রিয়ভাবে রক্ষণাবেক্ষণ করা হয় না।", diff --git a/packages/docusaurus-theme-classic/codeTranslations/cs.json b/packages/docusaurus-theme-classic/codeTranslations/cs.json index f1f37408fb..99135ee43b 100644 --- a/packages/docusaurus-theme-classic/codeTranslations/cs.json +++ b/packages/docusaurus-theme-classic/codeTranslations/cs.json @@ -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.", diff --git a/packages/docusaurus-theme-classic/codeTranslations/da.json b/packages/docusaurus-theme-classic/codeTranslations/da.json index 307c067f1f..93db383942 100644 --- a/packages/docusaurus-theme-classic/codeTranslations/da.json +++ b/packages/docusaurus-theme-classic/codeTranslations/da.json @@ -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.", diff --git a/packages/docusaurus-theme-classic/codeTranslations/de.json b/packages/docusaurus-theme-classic/codeTranslations/de.json index d39d30858d..c3a20e28c0 100644 --- a/packages/docusaurus-theme-classic/codeTranslations/de.json +++ b/packages/docusaurus-theme-classic/codeTranslations/de.json @@ -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.", diff --git a/packages/docusaurus-theme-classic/codeTranslations/es.json b/packages/docusaurus-theme-classic/codeTranslations/es.json index 6abc7b4d8d..d17ec91c3e 100644 --- a/packages/docusaurus-theme-classic/codeTranslations/es.json +++ b/packages/docusaurus-theme-classic/codeTranslations/es.json @@ -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.", diff --git a/packages/docusaurus-theme-classic/codeTranslations/fa.json b/packages/docusaurus-theme-classic/codeTranslations/fa.json index 804e6b8a27..577430a13a 100644 --- a/packages/docusaurus-theme-classic/codeTranslations/fa.json +++ b/packages/docusaurus-theme-classic/codeTranslations/fa.json @@ -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} دیگر به روزرسانی نمی شود.", diff --git a/packages/docusaurus-theme-classic/codeTranslations/fil.json b/packages/docusaurus-theme-classic/codeTranslations/fil.json index 7d7f994231..a7beca79cf 100644 --- a/packages/docusaurus-theme-classic/codeTranslations/fil.json +++ b/packages/docusaurus-theme-classic/codeTranslations/fil.json @@ -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.", diff --git a/packages/docusaurus-theme-classic/codeTranslations/fr.json b/packages/docusaurus-theme-classic/codeTranslations/fr.json index 412c29c3eb..a5df93c149 100644 --- a/packages/docusaurus-theme-classic/codeTranslations/fr.json +++ b/packages/docusaurus-theme-classic/codeTranslations/fr.json @@ -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.", diff --git a/packages/docusaurus-theme-classic/codeTranslations/he.json b/packages/docusaurus-theme-classic/codeTranslations/he.json index 605e896450..8f12842c95 100644 --- a/packages/docusaurus-theme-classic/codeTranslations/he.json +++ b/packages/docusaurus-theme-classic/codeTranslations/he.json @@ -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}, כבר לא נתמכת.", diff --git a/packages/docusaurus-theme-classic/codeTranslations/hi.json b/packages/docusaurus-theme-classic/codeTranslations/hi.json index 57c9c06d2e..1e773eca51 100644 --- a/packages/docusaurus-theme-classic/codeTranslations/hi.json +++ b/packages/docusaurus-theme-classic/codeTranslations/hi.json @@ -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} के लिए डॉक्यूमेंटेशन है, जिसे अब सक्रिय रूप से नहीं बनाए रखा गया है।", diff --git a/packages/docusaurus-theme-classic/codeTranslations/ja.json b/packages/docusaurus-theme-classic/codeTranslations/ja.json index 1eb8db9893..d7562aa74c 100644 --- a/packages/docusaurus-theme-classic/codeTranslations/ja.json +++ b/packages/docusaurus-theme-classic/codeTranslations/ja.json @@ -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}のドキュメントで現在はアクティブにメンテナンスされていません。", diff --git a/packages/docusaurus-theme-classic/codeTranslations/ko.json b/packages/docusaurus-theme-classic/codeTranslations/ko.json index abfe307514..94e35ade43 100644 --- a/packages/docusaurus-theme-classic/codeTranslations/ko.json +++ b/packages/docusaurus-theme-classic/codeTranslations/ko.json @@ -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} 문서는 업데이트되지 않습니다.", diff --git a/packages/docusaurus-theme-classic/codeTranslations/pl.json b/packages/docusaurus-theme-classic/codeTranslations/pl.json index ab491a6966..1b72b182f1 100644 --- a/packages/docusaurus-theme-classic/codeTranslations/pl.json +++ b/packages/docusaurus-theme-classic/codeTranslations/pl.json @@ -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.", diff --git a/packages/docusaurus-theme-classic/codeTranslations/pt-BR.json b/packages/docusaurus-theme-classic/codeTranslations/pt-BR.json index 11b8be97fb..488efbfd41 100644 --- a/packages/docusaurus-theme-classic/codeTranslations/pt-BR.json +++ b/packages/docusaurus-theme-classic/codeTranslations/pt-BR.json @@ -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.", diff --git a/packages/docusaurus-theme-classic/codeTranslations/pt-PT.json b/packages/docusaurus-theme-classic/codeTranslations/pt-PT.json index 34f2eca432..6da55b1e17 100644 --- a/packages/docusaurus-theme-classic/codeTranslations/pt-PT.json +++ b/packages/docusaurus-theme-classic/codeTranslations/pt-PT.json @@ -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.", diff --git a/packages/docusaurus-theme-classic/codeTranslations/ru.json b/packages/docusaurus-theme-classic/codeTranslations/ru.json index 4e3bdb80d6..898c4d07a9 100644 --- a/packages/docusaurus-theme-classic/codeTranslations/ru.json +++ b/packages/docusaurus-theme-classic/codeTranslations/ru.json @@ -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}, которая уже не поддерживается.", diff --git a/packages/docusaurus-theme-classic/codeTranslations/tr.json b/packages/docusaurus-theme-classic/codeTranslations/tr.json index acdff3d05c..436d3240a5 100644 --- a/packages/docusaurus-theme-classic/codeTranslations/tr.json +++ b/packages/docusaurus-theme-classic/codeTranslations/tr.json @@ -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.", diff --git a/packages/docusaurus-theme-classic/codeTranslations/vi.json b/packages/docusaurus-theme-classic/codeTranslations/vi.json index 81db429536..1de29cf71d 100644 --- a/packages/docusaurus-theme-classic/codeTranslations/vi.json +++ b/packages/docusaurus-theme-classic/codeTranslations/vi.json @@ -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ì.", diff --git a/packages/docusaurus-theme-classic/codeTranslations/zh-Hans.json b/packages/docusaurus-theme-classic/codeTranslations/zh-Hans.json index d8f4374bd3..0edf08fc5a 100644 --- a/packages/docusaurus-theme-classic/codeTranslations/zh-Hans.json +++ b/packages/docusaurus-theme-classic/codeTranslations/zh-Hans.json @@ -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} 版的文档,现已不再积极维护。", diff --git a/packages/docusaurus-theme-classic/codeTranslations/zh-Hant.json b/packages/docusaurus-theme-classic/codeTranslations/zh-Hant.json index 906238e607..0852d1a1f2 100644 --- a/packages/docusaurus-theme-classic/codeTranslations/zh-Hant.json +++ b/packages/docusaurus-theme-classic/codeTranslations/zh-Hant.json @@ -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} 版的文件,現已不再積極維護。", diff --git a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/index.tsx index 325b559c46..fcba24a960 100644 --- a/packages/docusaurus-theme-classic/src/theme/BlogPostItem/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/BlogPostItem/index.tsx @@ -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 && (
- - - Tags: - - - - {tags.map(({label, permalink: tagPermalink}) => ( - - {label} - - ))} +
)} diff --git a/packages/docusaurus-theme-classic/src/theme/BlogTagsListPage/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogTagsListPage/index.tsx index 1df05c8feb..44ab055e06 100644 --- a/packages/docusaurus-theme-classic/src/theme/BlogTagsListPage/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/BlogTagsListPage/index.tsx @@ -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]) => ( -
-

{category}

- {tagsForCategory.map((tag) => ( - - {tags[tag].name} ({tags[tag].count}) - - ))} -
-
- )) - .filter((item) => item != null); - + const title = translateTagsPageTitle(); return (

{title}

-
{tagsSection}
+
); } diff --git a/packages/docusaurus-theme-classic/src/theme/BlogTagsPostsPage/index.tsx b/packages/docusaurus-theme-classic/src/theme/BlogTagsPostsPage/index.tsx index 899407741b..e64a5efa59 100644 --- a/packages/docusaurus-theme-classic/src/theme/BlogTagsPostsPage/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/BlogTagsPostsPage/index.tsx @@ -49,7 +49,7 @@ function BlogTagsPostPage(props: Props): JSX.Element { - {(editUrl || lastUpdatedAt || lastUpdatedBy) && ( -
-
- {editUrl && } -
- -
- {(lastUpdatedAt || lastUpdatedBy) && ( - - )} -
-
- )} + @@ -129,5 +105,3 @@ function DocItem(props: Props): JSX.Element { ); } - -export default DocItem; diff --git a/packages/docusaurus-theme-classic/src/theme/DocItem/styles.module.css b/packages/docusaurus-theme-classic/src/theme/DocItem/styles.module.css index 15c3e4fa23..be0569c552 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocItem/styles.module.css +++ b/packages/docusaurus-theme-classic/src/theme/DocItem/styles.module.css @@ -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; diff --git a/packages/docusaurus-theme-classic/src/theme/DocItemFooter/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocItemFooter/index.tsx new file mode 100644 index 0000000000..7f7fd01594 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/DocItemFooter/index.tsx @@ -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 ( +
+
+ +
+
+ ); +} + +type EditMetaRowProps = Pick< + Props['content']['metadata'], + 'editUrl' | 'lastUpdatedAt' | 'lastUpdatedBy' | 'formattedLastUpdatedAt' +>; +function EditMetaRow({ + editUrl, + lastUpdatedAt, + lastUpdatedBy, + formattedLastUpdatedAt, +}: EditMetaRowProps) { + return ( +
+
{editUrl && }
+ +
+ {(lastUpdatedAt || lastUpdatedBy) && ( + + )} +
+
+ ); +} + +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 ( +
+ {canDisplayTagsRow && } + {canDisplayEditMetaRow && ( + + )} +
+ ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/DocItemFooter/styles.module.css b/packages/docusaurus-theme-classic/src/theme/DocItemFooter/styles.module.css new file mode 100644 index 0000000000..decdf39007 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/DocItemFooter/styles.module.css @@ -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; + } +} diff --git a/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx index 77b8f94b28..d255baad53 100644 --- a/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx +++ b/packages/docusaurus-theme-classic/src/theme/DocPage/index.tsx @@ -56,7 +56,7 @@ function DocPageContent({ return ( + 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 ( +
+ +

{doc.title}

+ + {doc.description &&

{doc.description}

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

{title}

+ View All Tags +
+
+ {tag.docs.map((doc) => ( + + ))} +
+
+
+
+
+ ); +} diff --git a/packages/docusaurus-theme-classic/src/theme/DocTagsListPage/index.tsx b/packages/docusaurus-theme-classic/src/theme/DocTagsListPage/index.tsx new file mode 100644 index 0000000000..780faa8911 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/DocTagsListPage/index.tsx @@ -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 ( + +
+
+
+

{title}

+ +
+
+
+
+ ); +} + +export default DocTagsListPage; diff --git a/packages/docusaurus-theme-classic/src/theme/TagsListByLetter/index.tsx b/packages/docusaurus-theme-classic/src/theme/TagsListByLetter/index.tsx new file mode 100644 index 0000000000..aab5e3245c --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/TagsListByLetter/index.tsx @@ -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 ( +
+

{letterEntry.letter}

+ {letterEntry.tags.map((tag) => ( + + {tag.name} ({tag.count}) + + ))} +
+
+ ); +} + +function TagsListByLetter({tags}: Props): JSX.Element { + const letterList = listTagsByLetters(tags); + return ( +
+ {letterList.map((letterEntry) => ( + + ))} +
+ ); +} + +export default TagsListByLetter; diff --git a/packages/docusaurus-theme-classic/src/theme/TagsListInline/index.tsx b/packages/docusaurus-theme-classic/src/theme/TagsListInline/index.tsx new file mode 100644 index 0000000000..ebb2741764 --- /dev/null +++ b/packages/docusaurus-theme-classic/src/theme/TagsListInline/index.tsx @@ -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 ( + <> + + + Tags: + + + {tags.map(({label, permalink: tagPermalink}) => ( + + {label} + + ))} + + ); +} diff --git a/packages/docusaurus-theme-classic/src/types.d.ts b/packages/docusaurus-theme-classic/src/types.d.ts index 97a098dea7..7240641019 100644 --- a/packages/docusaurus-theme-classic/src/types.d.ts +++ b/packages/docusaurus-theme-classic/src/types.d.ts @@ -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; +} diff --git a/packages/docusaurus-theme-classic/update-code-translations.js b/packages/docusaurus-theme-classic/update-code-translations.js index ce3a95a1fd..60bdfad5c3 100644 --- a/packages/docusaurus-theme-classic/update-code-translations.js +++ b/packages/docusaurus-theme-classic/update-code-translations.js @@ -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'), diff --git a/packages/docusaurus-theme-classic/update-code-translations.test.js b/packages/docusaurus-theme-classic/update-code-translations.test.js index 825ea04b6b..692b521378 100644 --- a/packages/docusaurus-theme-classic/update-code-translations.test.js +++ b/packages/docusaurus-theme-classic/update-code-translations.test.js @@ -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( diff --git a/packages/docusaurus-theme-common/package.json b/packages/docusaurus-theme-common/package.json index 6eccd5c296..d45162f4ca 100644 --- a/packages/docusaurus-theme-common/package.json +++ b/packages/docusaurus-theme-common/package.json @@ -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", diff --git a/packages/docusaurus-theme-common/src/index.ts b/packages/docusaurus-theme-common/src/index.ts index cabe5f23f0..08b097871e 100644 --- a/packages/docusaurus-theme-common/src/index.ts +++ b/packages/docusaurus-theme-common/src/index.ts @@ -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'; diff --git a/packages/docusaurus-theme-common/src/utils/ThemeClassNames.ts b/packages/docusaurus-theme-common/src/utils/ThemeClassNames.ts index c4a3f9b090..700bd66398 100644 --- a/packages/docusaurus-theme-common/src/utils/ThemeClassNames.ts +++ b/packages/docusaurus-theme-common/src/utils/ThemeClassNames.ts @@ -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: { diff --git a/packages/docusaurus-theme-common/src/utils/__tests__/tagUtils.test.ts b/packages/docusaurus-theme-common/src/utils/__tests__/tagUtils.test.ts new file mode 100644 index 0000000000..028ad4f0be --- /dev/null +++ b/packages/docusaurus-theme-common/src/utils/__tests__/tagUtils.test.ts @@ -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[0]; + type Tag = Param[number]; + type Result = ReturnType; + + 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); + }); +}); diff --git a/packages/docusaurus-theme-common/src/utils/tagsUtils.ts b/packages/docusaurus-theme-common/src/utils/tagsUtils.ts new file mode 100644 index 0000000000..ca53b77030 --- /dev/null +++ b/packages/docusaurus-theme-common/src/utils/tagsUtils.ts @@ -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 = {}; + 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}; + }) + ); +} diff --git a/packages/docusaurus-utils-validation/src/JoiFrontMatter.ts b/packages/docusaurus-utils-validation/src/JoiFrontMatter.ts new file mode 100644 index 0000000000..c6c4cc06a0 --- /dev/null +++ b/packages/docusaurus-utils-validation/src/JoiFrontMatter.ts @@ -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); diff --git a/packages/docusaurus-utils-validation/src/__tests__/validationUtils.test.ts b/packages/docusaurus-utils-validation/src/__tests__/validationUtils.test.ts index f1e9db11f0..e706b4d616 100644 --- a/packages/docusaurus-utils-validation/src/__tests__/validationUtils.test.ts +++ b/packages/docusaurus-utils-validation/src/__tests__/validationUtils.test.ts @@ -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', () => { diff --git a/packages/docusaurus-utils-validation/src/index.ts b/packages/docusaurus-utils-validation/src/index.ts index 420baadf6b..daaac61bd4 100644 --- a/packages/docusaurus-utils-validation/src/index.ts +++ b/packages/docusaurus-utils-validation/src/index.ts @@ -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'; diff --git a/packages/docusaurus-utils-validation/src/validationSchemas.ts b/packages/docusaurus-utils-validation/src/validationSchemas.ts index 304a391ab1..0496147b73 100644 --- a/packages/docusaurus-utils-validation/src/validationSchemas.ts +++ b/packages/docusaurus-utils-validation/src/validationSchemas.ts @@ -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({ + label: JoiFrontMatter.string().required(), + permalink: JoiFrontMatter.string().required(), + }).required(), + ), +); diff --git a/packages/docusaurus-utils-validation/src/validationUtils.ts b/packages/docusaurus-utils-validation/src/validationUtils.ts index 3365a32955..d530b165a2 100644 --- a/packages/docusaurus-utils-validation/src/validationUtils.ts +++ b/packages/docusaurus-utils-validation/src/validationUtils.ts @@ -99,24 +99,6 @@ export function normalizeThemeConfig( 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( frontMatter: Record, schema: Joi.ObjectSchema, diff --git a/packages/docusaurus-utils/src/__tests__/index.test.ts b/packages/docusaurus-utils/src/__tests__/index.test.ts index f80cc2b796..ea626caa42 100644 --- a/packages/docusaurus-utils/src/__tests__/index.test.ts +++ b/packages/docusaurus-utils/src/__tests__/index.test.ts @@ -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); diff --git a/packages/docusaurus-utils/src/__tests__/normalizeUrl.test.ts b/packages/docusaurus-utils/src/__tests__/normalizeUrl.test.ts new file mode 100644 index 0000000000..5731ab7d6a --- /dev/null +++ b/packages/docusaurus-utils/src/__tests__/normalizeUrl.test.ts @@ -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"`, + ); + }); +}); diff --git a/packages/docusaurus-utils/src/__tests__/tags.test.ts b/packages/docusaurus-utils/src/__tests__/tags.test.ts new file mode 100644 index 0000000000..5f3b761e14 --- /dev/null +++ b/packages/docusaurus-utils/src/__tests__/tags.test.ts @@ -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[1]; + type Output = ReturnType; + + 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[1]; + type Output = ReturnType; + + 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[0]; + type Output = ReturnType; + + 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); + }); +}); diff --git a/packages/docusaurus-utils/src/index.ts b/packages/docusaurus-utils/src/index.ts index 1bd7c5898e..3550b54dfd 100644 --- a/packages/docusaurus-utils/src/index.ts +++ b/packages/docusaurus-utils/src/index.ts @@ -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. diff --git a/packages/docusaurus-utils/src/normalizeUrl.ts b/packages/docusaurus-utils/src/normalizeUrl.ts new file mode 100644 index 0000000000..41dcbacf3f --- /dev/null +++ b/packages/docusaurus-utils/src/normalizeUrl.ts @@ -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; +} diff --git a/packages/docusaurus-utils/src/tags.ts b/packages/docusaurus-utils/src/tags.ts new file mode 100644 index 0000000000..3a0aa3bb10 --- /dev/null +++ b/packages/docusaurus-utils/src/tags.ts @@ -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 = { + 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( + items: Item[], + getItemTags: (item: Item) => Tag[], +): Record> { + const result: Record> = {}; + + 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; +} diff --git a/website/_dogfooding/_docs tests/index.md b/website/_dogfooding/_docs tests/index.md index a44511630d..f8a07cac85 100644 --- a/website/_dogfooding/_docs tests/index.md +++ b/website/_dogfooding/_docs tests/index.md @@ -1,5 +1,6 @@ --- slug: / +tags: [a, b, c, some tag] --- # Docs tests diff --git a/website/_dogfooding/_docs tests/more-test.md b/website/_dogfooding/_docs tests/more-test.md index a8b7310c06..3960962b67 100644 --- a/website/_dogfooding/_docs tests/more-test.md +++ b/website/_dogfooding/_docs tests/more-test.md @@ -1,3 +1,7 @@ +--- +tags: [a, e, some-tag, some_tag] +--- + # Another test page [Test link](./folder%20with%20space/doc%201.md) diff --git a/website/_dogfooding/_docs tests/standalone.md b/website/_dogfooding/_docs tests/standalone.md index 5103ded42c..c81811feae 100644 --- a/website/_dogfooding/_docs tests/standalone.md +++ b/website/_dogfooding/_docs tests/standalone.md @@ -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 diff --git a/website/docs/api/plugins/plugin-content-docs.md b/website/docs/api/plugins/plugin-content-docs.md index 12260289a1..800526ce47 100644 --- a/website/docs/api/plugins/plugin-content-docs.md +++ b/website/docs/api/plugins/plugin-content-docs.md @@ -42,6 +42,8 @@ Accepted fields: | `numberPrefixParser` | boolean | PrefixParser | _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 `` and `` in ``, 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 (`//`). 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. | +```typescript +type Tag = string | {label: string; permalink: string}; +``` + Example: ```yml