mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-12 16:47:26 +02:00
feat(docs, blog): add support for tags.yml
, predefined list of tags (#10137)
Co-authored-by: Sébastien Lorber <slorber@users.noreply.github.com> Co-authored-by: OzakIOne <OzakIOne@users.noreply.github.com> Co-authored-by: sebastien <lorber.sebastien@gmail.com> Co-authored-by: slorber <slorber@users.noreply.github.com>
This commit is contained in:
parent
1049294ba6
commit
0eb7b64aac
63 changed files with 2597 additions and 722 deletions
|
@ -21,10 +21,15 @@
|
|||
"@docusaurus/logger": "3.3.2",
|
||||
"@docusaurus/utils": "3.3.2",
|
||||
"@docusaurus/utils-common": "3.3.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"joi": "^17.9.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"tslib": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tmp-promise": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,538 @@
|
|||
/**
|
||||
* 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 * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import * as tmp from 'tmp-promise';
|
||||
import * as YAML from 'js-yaml';
|
||||
import {
|
||||
ensureUniquePermalinks,
|
||||
getTagsFile,
|
||||
getTagsFilePathsToWatch,
|
||||
normalizeTagsFile,
|
||||
} from '../tagsFile';
|
||||
import type {TagsFile, TagsFileInput} from '@docusaurus/utils';
|
||||
|
||||
describe('ensureUniquePermalinks', () => {
|
||||
it('throw when one duplicate permalink found', () => {
|
||||
const definedTags: TagsFile = {
|
||||
open: {
|
||||
label: 'Open Source',
|
||||
permalink: '/custom-open-source',
|
||||
description: 'Learn about the open source',
|
||||
},
|
||||
closed: {
|
||||
label: 'Closed Source',
|
||||
permalink: '/custom-open-source',
|
||||
description: 'Learn about the closed source',
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => ensureUniquePermalinks(definedTags))
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Duplicate permalinks found in tags file:
|
||||
- /custom-open-source"
|
||||
`);
|
||||
});
|
||||
|
||||
it('throw when multiple duplicate permalink found', () => {
|
||||
const definedTags: TagsFile = {
|
||||
open: {
|
||||
label: 'Open Source',
|
||||
permalink: '/custom-open-source',
|
||||
description: 'Learn about the open source',
|
||||
},
|
||||
closed: {
|
||||
label: 'Closed Source',
|
||||
permalink: '/custom-open-source',
|
||||
description: 'Learn about the closed source',
|
||||
},
|
||||
hello: {
|
||||
label: 'Hello',
|
||||
permalink: '/hello',
|
||||
description: 'Learn about the hello',
|
||||
},
|
||||
world: {
|
||||
label: 'Hello',
|
||||
permalink: '/hello',
|
||||
description: 'Learn about the world',
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => ensureUniquePermalinks(definedTags))
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Duplicate permalinks found in tags file:
|
||||
- /custom-open-source
|
||||
- /hello"
|
||||
`);
|
||||
});
|
||||
|
||||
it('do not throw when no duplicate permalink found', () => {
|
||||
const definedTags: TagsFile = {
|
||||
open: {
|
||||
label: 'Open Source',
|
||||
permalink: '/open-source',
|
||||
description: 'Learn about the open source',
|
||||
},
|
||||
closed: {
|
||||
label: 'Closed Source',
|
||||
permalink: '/closed-source',
|
||||
description: 'Learn about the closed source',
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => ensureUniquePermalinks(definedTags)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeTagsFile', () => {
|
||||
it('normalize null tag', () => {
|
||||
const input: TagsFileInput = {
|
||||
'kebab case test': null,
|
||||
};
|
||||
|
||||
const expectedOutput: TagsFile = {
|
||||
'kebab case test': {
|
||||
description: undefined,
|
||||
label: 'Kebab case test',
|
||||
permalink: '/kebab-case-test',
|
||||
},
|
||||
};
|
||||
|
||||
expect(normalizeTagsFile(input)).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it('normalize partial tag with label', () => {
|
||||
const input: TagsFileInput = {
|
||||
world: {label: 'WORLD'},
|
||||
};
|
||||
|
||||
const expectedOutput: TagsFile = {
|
||||
world: {
|
||||
description: undefined,
|
||||
label: 'WORLD',
|
||||
permalink: '/world',
|
||||
},
|
||||
};
|
||||
|
||||
expect(normalizeTagsFile(input)).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it('normalize partial tag with description', () => {
|
||||
const input: TagsFileInput = {
|
||||
world: {description: 'World description test'},
|
||||
};
|
||||
|
||||
const expectedOutput: TagsFile = {
|
||||
world: {
|
||||
description: 'World description test',
|
||||
label: 'World',
|
||||
permalink: '/world',
|
||||
},
|
||||
};
|
||||
|
||||
expect(normalizeTagsFile(input)).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it('normalize partial tag with permalink', () => {
|
||||
const input: TagsFileInput = {
|
||||
world: {permalink: 'world'},
|
||||
};
|
||||
|
||||
const expectedOutput: TagsFile = {
|
||||
world: {
|
||||
description: undefined,
|
||||
label: 'World',
|
||||
permalink: 'world',
|
||||
},
|
||||
};
|
||||
|
||||
expect(normalizeTagsFile(input)).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it('does not modify fully defined tags', () => {
|
||||
const input: TagsFileInput = {
|
||||
tag1: {
|
||||
label: 'Custom Label',
|
||||
description: 'Custom Description',
|
||||
permalink: 'custom-permalink',
|
||||
},
|
||||
};
|
||||
|
||||
expect(normalizeTagsFile(input)).toEqual(input);
|
||||
});
|
||||
|
||||
it('handle special characters in keys', () => {
|
||||
const input: TagsFileInput = {
|
||||
'special@char$!key': null,
|
||||
};
|
||||
|
||||
const expectedOutput: TagsFile = {
|
||||
'special@char$!key': {
|
||||
description: undefined,
|
||||
label: 'Special@char$!key',
|
||||
permalink: '/special-char-key',
|
||||
},
|
||||
};
|
||||
|
||||
expect(normalizeTagsFile(input)).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it('handle special characters in keys with chinese characters', () => {
|
||||
const input: TagsFileInput = {
|
||||
特殊字符测试: null,
|
||||
};
|
||||
|
||||
const expectedOutput: TagsFile = {
|
||||
特殊字符测试: {
|
||||
description: undefined,
|
||||
label: '特殊字符测试',
|
||||
permalink: '/特殊字符测试',
|
||||
},
|
||||
};
|
||||
|
||||
expect(normalizeTagsFile(input)).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it('normalize test', () => {
|
||||
const input: TagsFileInput = {
|
||||
world: {permalink: 'aze'},
|
||||
hello: {permalink: 'h e l l o'},
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
world: {
|
||||
description: undefined,
|
||||
label: 'World',
|
||||
permalink: 'aze',
|
||||
},
|
||||
hello: {
|
||||
description: undefined,
|
||||
label: 'Hello',
|
||||
permalink: 'h e l l o',
|
||||
},
|
||||
};
|
||||
|
||||
expect(normalizeTagsFile(input)).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTagsFilePathsToWatch', () => {
|
||||
it('returns tags file paths - tags undefined', () => {
|
||||
expect(
|
||||
getTagsFilePathsToWatch({
|
||||
tags: undefined,
|
||||
contentPaths: {
|
||||
contentPath: '/user/blog',
|
||||
contentPathLocalized: '/i18n/blog',
|
||||
},
|
||||
}),
|
||||
).toEqual(['/i18n/blog/tags.yml', '/user/blog/tags.yml']);
|
||||
});
|
||||
|
||||
it('returns tags file paths - tags.yml', () => {
|
||||
expect(
|
||||
getTagsFilePathsToWatch({
|
||||
tags: 'tags.yml',
|
||||
contentPaths: {
|
||||
contentPath: '/user/blog',
|
||||
contentPathLocalized: '/i18n/blog',
|
||||
},
|
||||
}),
|
||||
).toEqual(['/i18n/blog/tags.yml', '/user/blog/tags.yml']);
|
||||
});
|
||||
|
||||
it('returns tags file paths - customTags.yml', () => {
|
||||
expect(
|
||||
getTagsFilePathsToWatch({
|
||||
tags: 'customTags.yml',
|
||||
contentPaths: {
|
||||
contentPath: '/user/blog',
|
||||
contentPathLocalized: '/i18n/blog',
|
||||
},
|
||||
}),
|
||||
).toEqual(['/i18n/blog/customTags.yml', '/user/blog/customTags.yml']);
|
||||
});
|
||||
|
||||
it('returns [] - tags: null', () => {
|
||||
expect(
|
||||
getTagsFilePathsToWatch({
|
||||
tags: null,
|
||||
contentPaths: {
|
||||
contentPath: '/user/blog',
|
||||
contentPathLocalized: '/i18n/blog',
|
||||
},
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns [] - tags: false', () => {
|
||||
expect(
|
||||
getTagsFilePathsToWatch({
|
||||
tags: false,
|
||||
contentPaths: {
|
||||
contentPath: '/user/blog',
|
||||
contentPathLocalized: '/i18n/blog',
|
||||
},
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTagsFile', () => {
|
||||
async function createTestTagsFile({
|
||||
filePath,
|
||||
tagsFileInput,
|
||||
}: {
|
||||
filePath: string;
|
||||
tagsFileInput: TagsFileInput;
|
||||
}): Promise<{dir: string}> {
|
||||
async function createTmpDir() {
|
||||
return (
|
||||
await tmp.dir({
|
||||
prefix: 'jest-createTmpSiteDir',
|
||||
})
|
||||
).path;
|
||||
}
|
||||
const contentPath = await createTmpDir();
|
||||
const finalFilePath = path.join(contentPath, filePath);
|
||||
const fileContent = YAML.dump(tagsFileInput);
|
||||
await fs.writeFile(finalFilePath, fileContent);
|
||||
return {dir: contentPath};
|
||||
}
|
||||
|
||||
type Params = Parameters<typeof getTagsFile>[0];
|
||||
|
||||
it('reads tags file - regular', async () => {
|
||||
const {dir} = await createTestTagsFile({
|
||||
filePath: 'tags.yml',
|
||||
tagsFileInput: {
|
||||
tag1: {label: 'Tag1 Label'},
|
||||
tag2: {description: 'Tag2 Description'},
|
||||
tag3: {
|
||||
label: 'Tag3 Label',
|
||||
permalink: '/tag-3',
|
||||
description: 'Tag3 Description',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const params: Params = {
|
||||
contentPaths: {contentPath: dir, contentPathLocalized: dir},
|
||||
tags: 'tags.yml',
|
||||
};
|
||||
|
||||
await expect(getTagsFile(params)).resolves.toMatchInlineSnapshot(`
|
||||
{
|
||||
"tag1": {
|
||||
"description": undefined,
|
||||
"label": "Tag1 Label",
|
||||
"permalink": "/tag-1",
|
||||
},
|
||||
"tag2": {
|
||||
"description": "Tag2 Description",
|
||||
"label": "Tag2",
|
||||
"permalink": "/tag-2",
|
||||
},
|
||||
"tag3": {
|
||||
"description": "Tag3 Description",
|
||||
"label": "Tag3 Label",
|
||||
"permalink": "/tag-3",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('reads tags file - only keys', async () => {
|
||||
const {dir} = await createTestTagsFile({
|
||||
filePath: 'tags.yml',
|
||||
tagsFileInput: {
|
||||
tagKey: null,
|
||||
},
|
||||
});
|
||||
|
||||
const params: Params = {
|
||||
contentPaths: {contentPath: dir, contentPathLocalized: dir},
|
||||
tags: 'tags.yml',
|
||||
};
|
||||
|
||||
await expect(getTagsFile(params)).resolves.toMatchInlineSnapshot(`
|
||||
{
|
||||
"tagKey": {
|
||||
"description": undefined,
|
||||
"label": "Tagkey",
|
||||
"permalink": "/tag-key",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('reads tags file - tags option undefined', async () => {
|
||||
const {dir} = await createTestTagsFile({
|
||||
filePath: 'tags.yml',
|
||||
tagsFileInput: {
|
||||
tag: {label: 'tag label'},
|
||||
},
|
||||
});
|
||||
|
||||
const params: Params = {
|
||||
contentPaths: {contentPath: dir, contentPathLocalized: dir},
|
||||
tags: undefined,
|
||||
};
|
||||
|
||||
await expect(getTagsFile(params)).resolves.toMatchInlineSnapshot(`
|
||||
{
|
||||
"tag": {
|
||||
"description": undefined,
|
||||
"label": "tag label",
|
||||
"permalink": "/tag",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('reads tags file - empty file', async () => {
|
||||
const {dir} = await createTestTagsFile({
|
||||
filePath: 'tags.yml',
|
||||
tagsFileInput: {},
|
||||
});
|
||||
|
||||
const params: Params = {
|
||||
contentPaths: {contentPath: dir, contentPathLocalized: dir},
|
||||
tags: undefined,
|
||||
};
|
||||
|
||||
await expect(getTagsFile(params)).resolves.toEqual({});
|
||||
});
|
||||
|
||||
it('reads tags file - prioritizes reading from localized content path', async () => {
|
||||
const {dir} = await createTestTagsFile({
|
||||
filePath: 'tags.yml',
|
||||
tagsFileInput: {
|
||||
tag: {label: 'tag label'},
|
||||
},
|
||||
});
|
||||
|
||||
const {dir: dirLocalized} = await createTestTagsFile({
|
||||
filePath: 'tags.yml',
|
||||
tagsFileInput: {
|
||||
tag: {label: 'tag label (localized)'},
|
||||
},
|
||||
});
|
||||
|
||||
const params: Params = {
|
||||
contentPaths: {contentPath: dir, contentPathLocalized: dirLocalized},
|
||||
tags: undefined,
|
||||
};
|
||||
|
||||
await expect(getTagsFile(params)).resolves.toMatchInlineSnapshot(`
|
||||
{
|
||||
"tag": {
|
||||
"description": undefined,
|
||||
"label": "tag label (localized)",
|
||||
"permalink": "/tag",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('reads tags file - custom tags file path', async () => {
|
||||
const {dir} = await createTestTagsFile({
|
||||
filePath: 'custom-tags-path.yml',
|
||||
tagsFileInput: {
|
||||
tag: {label: 'tag label'},
|
||||
},
|
||||
});
|
||||
|
||||
const params: Params = {
|
||||
contentPaths: {contentPath: dir, contentPathLocalized: dir},
|
||||
tags: 'custom-tags-path.yml',
|
||||
};
|
||||
|
||||
await expect(getTagsFile(params)).resolves.toMatchInlineSnapshot(`
|
||||
{
|
||||
"tag": {
|
||||
"description": undefined,
|
||||
"label": "tag label",
|
||||
"permalink": "/tag",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('throws if duplicate permalink', async () => {
|
||||
const {dir} = await createTestTagsFile({
|
||||
filePath: 'tags.yml',
|
||||
tagsFileInput: {
|
||||
tag1: {permalink: '/duplicate'},
|
||||
tag2: {permalink: '/duplicate'},
|
||||
},
|
||||
});
|
||||
|
||||
const params: Params = {
|
||||
contentPaths: {contentPath: dir, contentPathLocalized: dir},
|
||||
tags: undefined,
|
||||
};
|
||||
|
||||
await expect(getTagsFile(params)).rejects.toMatchInlineSnapshot(`
|
||||
[Error: Duplicate permalinks found in tags file:
|
||||
- /duplicate]
|
||||
`);
|
||||
});
|
||||
|
||||
it('throws if custom tags file path does not exist', async () => {
|
||||
const params: Params = {
|
||||
contentPaths: {contentPath: 'any', contentPathLocalized: 'localizedAny'},
|
||||
tags: 'custom-tags-path.yml',
|
||||
};
|
||||
|
||||
await expect(getTagsFile(params)).rejects
|
||||
.toThrowErrorMatchingInlineSnapshot(`
|
||||
"No tags file 'custom-tags-path.yml' could be found in any of those directories:
|
||||
- localizedAny
|
||||
- any"
|
||||
`);
|
||||
});
|
||||
|
||||
it('does not read tags file - tags option null/false', async () => {
|
||||
const {dir} = await createTestTagsFile({
|
||||
filePath: 'tags.yml',
|
||||
tagsFileInput: {
|
||||
tag: {label: 'tag label'},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
getTagsFile({
|
||||
contentPaths: {contentPath: dir, contentPathLocalized: dir},
|
||||
tags: null,
|
||||
}),
|
||||
).resolves.toBeNull();
|
||||
await expect(
|
||||
getTagsFile({
|
||||
contentPaths: {contentPath: dir, contentPathLocalized: dir},
|
||||
tags: false,
|
||||
}),
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('does not read tags file - tags files has non-default name', async () => {
|
||||
const {dir} = await createTestTagsFile({
|
||||
filePath: 'bad-tags-file-name.yml',
|
||||
tagsFileInput: {
|
||||
tag: {label: 'tag label'},
|
||||
},
|
||||
});
|
||||
|
||||
const params: Params = {
|
||||
contentPaths: {contentPath: dir, contentPathLocalized: dir},
|
||||
tags: undefined,
|
||||
};
|
||||
|
||||
await expect(getTagsFile(params)).resolves.toBeNull();
|
||||
});
|
||||
});
|
|
@ -29,3 +29,4 @@ export {
|
|||
FrontMatterLastUpdateErrorMessage,
|
||||
FrontMatterLastUpdateSchema,
|
||||
} from './validationSchemas';
|
||||
export {getTagsFilePathsToWatch, getTagsFile} from './tagsFile';
|
||||
|
|
131
packages/docusaurus-utils-validation/src/tagsFile.ts
Normal file
131
packages/docusaurus-utils-validation/src/tagsFile.ts
Normal file
|
@ -0,0 +1,131 @@
|
|||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import path from 'node:path';
|
||||
import _ from 'lodash';
|
||||
import Joi from 'joi';
|
||||
import YAML from 'js-yaml';
|
||||
import {getContentPathList, getDataFilePath} from '@docusaurus/utils';
|
||||
import type {
|
||||
ContentPaths,
|
||||
TagsFile,
|
||||
TagsFileInput,
|
||||
TagsPluginOptions,
|
||||
} from '@docusaurus/utils';
|
||||
|
||||
const TagsFileInputSchema = Joi.object<TagsFileInput>().pattern(
|
||||
Joi.string(),
|
||||
Joi.object({
|
||||
label: Joi.string(),
|
||||
description: Joi.string(),
|
||||
permalink: Joi.string(),
|
||||
}).allow(null),
|
||||
);
|
||||
|
||||
export function ensureUniquePermalinks(tags: TagsFile): void {
|
||||
const permalinks = new Set<string>();
|
||||
const duplicates = new Set<string>();
|
||||
|
||||
for (const [, tag] of Object.entries(tags)) {
|
||||
const {permalink} = tag;
|
||||
if (permalinks.has(permalink)) {
|
||||
duplicates.add(permalink);
|
||||
} else {
|
||||
permalinks.add(permalink);
|
||||
}
|
||||
}
|
||||
|
||||
if (duplicates.size > 0) {
|
||||
const duplicateList = Array.from(duplicates)
|
||||
.map((permalink) => ` - ${permalink}`)
|
||||
.join('\n');
|
||||
throw new Error(
|
||||
`Duplicate permalinks found in tags file:\n${duplicateList}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeTagsFile(data: TagsFileInput): TagsFile {
|
||||
return _.mapValues(data, (tag, key) => {
|
||||
return {
|
||||
label: tag?.label || _.capitalize(key),
|
||||
description: tag?.description,
|
||||
permalink: tag?.permalink || `/${_.kebabCase(key)}`,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
type GetTagsFileParams = {
|
||||
tags: TagsPluginOptions['tags'];
|
||||
contentPaths: ContentPaths;
|
||||
};
|
||||
|
||||
const DefaultTagsFileName = 'tags.yml';
|
||||
|
||||
export function getTagsFilePathsToWatch({
|
||||
tags,
|
||||
contentPaths,
|
||||
}: GetTagsFileParams): string[] {
|
||||
if (tags === false || tags === null) {
|
||||
return [];
|
||||
}
|
||||
const relativeFilePath = tags ?? DefaultTagsFileName;
|
||||
|
||||
return getContentPathList(contentPaths).map((contentPath) =>
|
||||
path.posix.join(contentPath, relativeFilePath),
|
||||
);
|
||||
}
|
||||
|
||||
export async function getTagsFile({
|
||||
tags,
|
||||
contentPaths,
|
||||
}: GetTagsFileParams): Promise<TagsFile | null> {
|
||||
if (tags === false || tags === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const relativeFilePath = tags ?? DefaultTagsFileName;
|
||||
|
||||
// if returned path is defined, the file exists (localized or not)
|
||||
const yamlFilePath = await getDataFilePath({
|
||||
contentPaths,
|
||||
filePath: relativeFilePath,
|
||||
});
|
||||
|
||||
// If the tags option is undefined, don't throw when the file does not exist
|
||||
// Retro-compatible behavior: existing sites do not yet have tags.yml
|
||||
if (tags === undefined && !yamlFilePath) {
|
||||
return null;
|
||||
}
|
||||
if (!yamlFilePath) {
|
||||
throw new Error(
|
||||
`No tags file '${relativeFilePath}' could be found in any of those directories:\n- ${getContentPathList(
|
||||
contentPaths,
|
||||
).join('\n- ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
const tagDefinitionContent = await fs.readFile(yamlFilePath, 'utf-8');
|
||||
if (!tagDefinitionContent.trim()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const yamlContent = YAML.load(tagDefinitionContent);
|
||||
const tagsFileInputResult = TagsFileInputSchema.validate(yamlContent);
|
||||
if (tagsFileInputResult.error) {
|
||||
throw new Error(
|
||||
`There was an error extracting tags from file: ${tagsFileInputResult.error.message}`,
|
||||
{cause: tagsFileInputResult},
|
||||
);
|
||||
}
|
||||
|
||||
const tagsFile = normalizeTagsFile(tagsFileInputResult.value);
|
||||
ensureUniquePermalinks(tagsFile);
|
||||
|
||||
return tagsFile;
|
||||
}
|
|
@ -5,7 +5,11 @@
|
|||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {isValidPathname, DEFAULT_PLUGIN_ID, type Tag} from '@docusaurus/utils';
|
||||
import {
|
||||
isValidPathname,
|
||||
DEFAULT_PLUGIN_ID,
|
||||
type FrontMatterTag,
|
||||
} from '@docusaurus/utils';
|
||||
import {addLeadingSlash} from '@docusaurus/utils-common';
|
||||
import Joi from './Joi';
|
||||
import {JoiFrontMatter} from './JoiFrontMatter';
|
||||
|
@ -113,7 +117,9 @@ export const RouteBasePathSchema = Joi
|
|||
const FrontMatterTagSchema = JoiFrontMatter.alternatives()
|
||||
.try(
|
||||
JoiFrontMatter.string().required(),
|
||||
JoiFrontMatter.object<Tag>({
|
||||
// TODO Docusaurus v4 remove this legacy front matter tag object form
|
||||
// users should use tags.yml instead
|
||||
JoiFrontMatter.object<FrontMatterTag>({
|
||||
label: JoiFrontMatter.string().required(),
|
||||
permalink: JoiFrontMatter.string().required(),
|
||||
}).required(),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue