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:
ozaki 2024-05-31 17:32:08 +02:00 committed by GitHub
parent 1049294ba6
commit 0eb7b64aac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
63 changed files with 2597 additions and 722 deletions

View file

@ -6,117 +6,284 @@
*/
import {
normalizeFrontMatterTags,
reportInlineTags,
groupTaggedItems,
type Tag,
getTagVisibility,
} from '../tags';
} from '@docusaurus/utils';
import {normalizeTag} from '../tags';
import type {Tag, TagMetadata, FrontMatterTag, TagsFile} from '../tags';
describe('normalizeFrontMatterTags', () => {
it('normalizes simple string tag', () => {
const tagsPath = '/all/tags';
const input = 'tag';
const expectedOutput = {
label: 'tag',
permalink: `${tagsPath}/tag`,
describe('normalizeTag', () => {
const tagsBaseRoutePath = '/all/tags';
describe('inline', () => {
it('normalizes simple string tag', () => {
const input: FrontMatterTag = 'tag';
const expectedOutput: TagMetadata = {
inline: true,
label: 'tag',
permalink: `${tagsBaseRoutePath}/tag`,
description: undefined,
};
expect(
normalizeTag({tagsBaseRoutePath, tagsFile: null, tag: input}),
).toEqual(expectedOutput);
});
it('normalizes complex string tag', () => {
const input: FrontMatterTag = 'some more Complex_tag';
const expectedOutput: TagMetadata = {
inline: true,
label: 'some more Complex_tag',
permalink: `${tagsBaseRoutePath}/some-more-complex-tag`,
description: undefined,
};
expect(
normalizeTag({tagsBaseRoutePath, tagsFile: null, tag: input}),
).toEqual(expectedOutput);
});
it('normalizes simple object tag', () => {
const input: FrontMatterTag = {
label: 'tag',
permalink: 'tagPermalink',
};
const expectedOutput: TagMetadata = {
inline: true,
label: 'tag',
permalink: `${tagsBaseRoutePath}/tagPermalink`,
description: undefined,
};
expect(
normalizeTag({tagsBaseRoutePath, tagsFile: null, tag: input}),
).toEqual(expectedOutput);
});
it('normalizes complex string tag with object tag', () => {
const input: FrontMatterTag = {
label: 'tag complex Label',
permalink: '/MoreComplex/Permalink',
};
const expectedOutput: TagMetadata = {
inline: true,
label: 'tag complex Label',
permalink: `${tagsBaseRoutePath}/MoreComplex/Permalink`,
description: undefined,
};
expect(
normalizeTag({tagsBaseRoutePath, tagsFile: null, tag: input}),
).toEqual(expectedOutput);
});
});
describe('with tags file', () => {
const tagsFile: TagsFile = {
tag1: {
label: 'Tag 1 label',
permalink: 'tag-1-permalink',
description: 'Tag 1 description',
},
tag2: {
label: 'Tag 2 label',
permalink: '/tag-2-permalink',
description: undefined,
},
};
expect(normalizeFrontMatterTags(tagsPath, [input])).toEqual([
expectedOutput,
]);
it('normalizes tag1 ref', () => {
const input: FrontMatterTag = 'tag1';
const expectedOutput: TagMetadata = {
inline: false,
label: tagsFile.tag1.label,
description: tagsFile.tag1.description,
permalink: `${tagsBaseRoutePath}/tag-1-permalink`,
};
expect(normalizeTag({tagsBaseRoutePath, tagsFile, tag: input})).toEqual(
expectedOutput,
);
});
it('normalizes tag2 ref', () => {
const input: FrontMatterTag = 'tag2';
const expectedOutput: TagMetadata = {
inline: false,
label: tagsFile.tag2.label,
description: tagsFile.tag2.description,
permalink: `${tagsBaseRoutePath}/tag-2-permalink`,
};
expect(normalizeTag({tagsBaseRoutePath, tagsFile, tag: input})).toEqual(
expectedOutput,
);
});
it('normalizes inline tag not declared in tags file', () => {
const input: FrontMatterTag = 'inlineTag';
const expectedOutput: TagMetadata = {
inline: true,
label: 'inlineTag',
description: undefined,
permalink: `${tagsBaseRoutePath}/inline-tag`,
};
expect(normalizeTag({tagsBaseRoutePath, tagsFile, tag: input})).toEqual(
expectedOutput,
);
});
});
});
describe('reportInlineTags', () => {
const tagsFile: TagsFile = {
hello: {
label: 'Hello',
permalink: '/hello',
description: undefined,
},
test: {
label: 'Test',
permalink: '/test',
description: undefined,
},
open: {
label: 'Open Source',
permalink: '/open',
description: undefined,
},
};
it('throw when inline tags found', () => {
const testFn = () =>
reportInlineTags({
tags: [
{
label: 'hello',
permalink: 'hello',
inline: true,
description: undefined,
},
{
label: 'world',
permalink: 'world',
inline: true,
description: undefined,
},
],
source: 'wrong.md',
options: {onInlineTags: 'throw', tags: 'tags.yml'},
});
expect(testFn).toThrowErrorMatchingInlineSnapshot(
`"Tags [hello, world] used in wrong.md are not defined in tags.yml"`,
);
});
it('normalizes complex string tag', () => {
const tagsPath = '/all/tags';
const input = 'some more Complex_tag';
const expectedOutput = {
label: 'some more Complex_tag',
permalink: `${tagsPath}/some-more-complex-tag`,
};
expect(normalizeFrontMatterTags(tagsPath, [input])).toEqual([
expectedOutput,
]);
it('warn when docs has invalid tags', () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
reportInlineTags({
tags: [
{
label: 'hello',
permalink: 'hello',
inline: false,
description: undefined,
},
{
label: 'world',
permalink: 'world',
inline: true,
description: undefined,
},
],
source: 'wrong.md',
options: {onInlineTags: 'warn', tags: 'tags.yml'},
});
expect(warnSpy).toHaveBeenCalledTimes(1);
expect(warnSpy.mock.calls).toMatchInlineSnapshot(`
[
[
"[WARNING] Tags [world] used in wrong.md are not defined in tags.yml",
],
]
`);
warnSpy.mockRestore();
});
it('normalizes simple object tag', () => {
const tagsPath = '/all/tags';
const input = {label: 'tag', permalink: 'tagPermalink'};
const expectedOutput = {
label: 'tag',
permalink: `${tagsPath}/tagPermalink`,
};
expect(normalizeFrontMatterTags(tagsPath, [input])).toEqual([
expectedOutput,
]);
it('ignore when docs has invalid tags', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
reportInlineTags({
tags: [
{
label: 'hello',
permalink: 'hello',
inline: false,
description: undefined,
},
{
label: 'world',
permalink: 'world',
inline: true,
description: undefined,
},
],
source: 'wrong.md',
options: {onInlineTags: 'ignore', tags: 'tags.yml'},
});
expect(errorSpy).not.toHaveBeenCalled();
expect(warnSpy).not.toHaveBeenCalled();
expect(logSpy).not.toHaveBeenCalled();
errorSpy.mockRestore();
warnSpy.mockRestore();
logSpy.mockRestore();
});
it('normalizes complex string tag with object tag', () => {
const tagsPath = '/all/tags';
const input = {
label: 'tag complex Label',
permalink: '/MoreComplex/Permalink',
};
const expectedOutput = {
label: 'tag complex Label',
permalink: `${tagsPath}/MoreComplex/Permalink`,
};
expect(normalizeFrontMatterTags(tagsPath, [input])).toEqual([
expectedOutput,
]);
it('throw for unknown string and object tag', () => {
const frontmatter = ['open', 'world'];
const tags = frontmatter.map((tag) =>
normalizeTag({
tagsBaseRoutePath: '/tags',
tagsFile,
tag,
}),
);
const testFn = () =>
reportInlineTags({
tags,
source: 'default.md',
options: {
onInlineTags: 'throw',
tags: 'tags.yml',
},
});
expect(testFn).toThrowErrorMatchingInlineSnapshot(
`"Tags [world] used in default.md are not defined in tags.yml"`,
);
});
type Input = Parameters<typeof normalizeFrontMatterTags>[1];
type Output = ReturnType<typeof normalizeFrontMatterTags>;
it('normalizes 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);
});
it('succeeds for empty list', () => {
expect(normalizeFrontMatterTags('/foo')).toEqual([]);
});
it('normalizes 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);
it('does not throw when docs has valid tags', () => {
const frontmatter = ['open'];
const tags = frontmatter.map((tag) =>
normalizeTag({
tagsBaseRoutePath: '/tags',
tagsFile,
tag,
}),
);
const testFn = () =>
reportInlineTags({
tags,
source: 'wrong.md',
options: {
onInlineTags: 'throw',
tags: 'tags.yml',
},
});
expect(testFn).not.toThrow();
});
});
@ -135,14 +302,23 @@ describe('groupTaggedItems', () => {
type Output = ReturnType<typeof groupItems>;
it('groups items by tag permalink', () => {
const tagGuide = {label: 'Guide', permalink: '/guide'};
const tagTutorial = {label: 'Tutorial', permalink: '/tutorial'};
const tagAPI = {label: 'API', permalink: '/api'};
const tagGuide = {
label: 'Guide',
permalink: '/guide',
description: undefined,
};
const tagTutorial = {
label: 'Tutorial',
permalink: '/tutorial',
description: undefined,
};
const tagAPI = {label: 'API', permalink: '/api', description: undefined};
// This one will be grouped under same permalink and label is ignored
const tagTutorialOtherLabel = {
label: 'TutorialOtherLabel',
permalink: '/tutorial',
description: undefined,
};
const item1: SomeTaggedItem = {