@docusaurus/plugin-content-blog 2.0.0-beta.0e652730d → 2.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/.tsbuildinfo +1 -1
- package/lib/authors.d.ts +23 -0
- package/lib/authors.js +147 -0
- package/lib/blogFrontMatter.d.ts +19 -6
- package/lib/blogFrontMatter.js +35 -23
- package/lib/blogUtils.d.ts +10 -4
- package/lib/blogUtils.js +141 -136
- package/lib/feed.d.ts +20 -0
- package/lib/feed.js +90 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +109 -112
- package/lib/markdownLoader.d.ts +3 -6
- package/lib/markdownLoader.js +5 -5
- package/lib/pluginOptionSchema.d.ts +3 -26
- package/lib/pluginOptionSchema.js +28 -7
- package/lib/translations.d.ts +10 -0
- package/lib/translations.js +53 -0
- package/lib/types.d.ts +54 -14
- package/package.json +19 -13
- package/src/__tests__/__fixtures__/authorsMapFiles/authors.json +29 -0
- package/src/__tests__/__fixtures__/authorsMapFiles/authors.yml +27 -0
- package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad1.json +5 -0
- package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad1.yml +3 -0
- package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad2.json +3 -0
- package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad2.yml +2 -0
- package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad3.json +8 -0
- package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad3.yml +3 -0
- package/src/__tests__/__fixtures__/component/Typography.tsx +6 -0
- package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathEmpty/empty +0 -0
- package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathJson1/authors.json +0 -0
- package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathJson2/authors.json +0 -0
- package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathNestedYml/sub/folder/authors.yml +0 -0
- package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathYml1/authors.yml +0 -0
- package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathYml2/authors.yml +0 -0
- package/src/__tests__/__fixtures__/website/blog/2018-12-14-Happy-First-Birthday-Slash.md +3 -0
- package/src/__tests__/__fixtures__/website/blog/_partials/somePartial.md +3 -0
- package/src/__tests__/__fixtures__/website/blog/_partials/subfolder/somePartial.md +3 -0
- package/src/__tests__/__fixtures__/website/blog/_somePartial.md +3 -0
- package/src/__tests__/__fixtures__/website/blog/authors.yml +4 -0
- package/src/__tests__/__fixtures__/website/blog/mdx-blog-post.mdx +36 -0
- package/src/__tests__/__fixtures__/website/blog/mdx-require-blog-post.mdx +14 -0
- package/src/__tests__/__fixtures__/website/blog/simple-slug.md +4 -0
- package/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/2018-12-14-Happy-First-Birthday-Slash.md +3 -0
- package/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/authors.yml +5 -0
- package/src/__tests__/__fixtures__/website/static/img/docusaurus.png +0 -0
- package/src/__tests__/__snapshots__/feed.test.ts.snap +164 -0
- package/src/__tests__/__snapshots__/translations.test.ts.snap +64 -0
- package/src/__tests__/authors.test.ts +608 -0
- package/src/__tests__/blogFrontMatter.test.ts +165 -36
- package/src/__tests__/blogUtils.test.ts +94 -0
- package/src/__tests__/{generateBlogFeed.test.ts → feed.test.ts} +35 -9
- package/src/__tests__/index.test.ts +84 -12
- package/src/__tests__/pluginOptionSchema.test.ts +3 -3
- package/src/__tests__/translations.test.ts +92 -0
- package/src/authors.ts +198 -0
- package/src/blogFrontMatter.ts +76 -37
- package/src/blogUtils.ts +202 -179
- package/{types.d.ts → src/deps.d.ts} +0 -0
- package/src/feed.ts +129 -0
- package/src/index.ts +131 -112
- package/src/markdownLoader.ts +8 -12
- package/{index.d.ts → src/plugin-content-blog.d.ts} +35 -31
- package/src/pluginOptionSchema.ts +31 -9
- package/src/translations.ts +63 -0
- package/src/types.ts +69 -16
- package/src/__tests__/__snapshots__/generateBlogFeed.test.ts.snap +0 -76
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
9
|
-
|
|
10
8
|
import fs from 'fs-extra';
|
|
11
9
|
import path from 'path';
|
|
12
10
|
import pluginContentBlog from '../index';
|
|
@@ -14,6 +12,7 @@ import {DocusaurusConfig, LoadContext, I18n} from '@docusaurus/types';
|
|
|
14
12
|
import {PluginOptionSchema} from '../pluginOptionSchema';
|
|
15
13
|
import {PluginOptions, EditUrlFunction, BlogPost} from '../types';
|
|
16
14
|
import {Joi} from '@docusaurus/utils-validation';
|
|
15
|
+
import {posixPath} from '@docusaurus/utils';
|
|
17
16
|
|
|
18
17
|
function findByTitle(
|
|
19
18
|
blogPosts: BlogPost[],
|
|
@@ -60,7 +59,7 @@ describe('loadBlog', () => {
|
|
|
60
59
|
|
|
61
60
|
const BaseEditUrl = 'https://baseEditUrl.com/edit';
|
|
62
61
|
|
|
63
|
-
const
|
|
62
|
+
const getPlugin = async (
|
|
64
63
|
siteDir: string,
|
|
65
64
|
pluginOptions: Partial<PluginOptions> = {},
|
|
66
65
|
i18n: I18n = DefaultI18N,
|
|
@@ -71,7 +70,7 @@ describe('loadBlog', () => {
|
|
|
71
70
|
baseUrl: '/',
|
|
72
71
|
url: 'https://docusaurus.io',
|
|
73
72
|
} as DocusaurusConfig;
|
|
74
|
-
|
|
73
|
+
return pluginContentBlog(
|
|
75
74
|
{
|
|
76
75
|
siteDir,
|
|
77
76
|
siteConfig,
|
|
@@ -84,11 +83,32 @@ describe('loadBlog', () => {
|
|
|
84
83
|
...pluginOptions,
|
|
85
84
|
}),
|
|
86
85
|
);
|
|
87
|
-
|
|
86
|
+
};
|
|
88
87
|
|
|
88
|
+
const getBlogPosts = async (
|
|
89
|
+
siteDir: string,
|
|
90
|
+
pluginOptions: Partial<PluginOptions> = {},
|
|
91
|
+
i18n: I18n = DefaultI18N,
|
|
92
|
+
) => {
|
|
93
|
+
const plugin = await getPlugin(siteDir, pluginOptions, i18n);
|
|
94
|
+
const {blogPosts} = (await plugin.loadContent!())!;
|
|
89
95
|
return blogPosts;
|
|
90
96
|
};
|
|
91
97
|
|
|
98
|
+
test('getPathsToWatch', async () => {
|
|
99
|
+
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
|
100
|
+
const plugin = await getPlugin(siteDir);
|
|
101
|
+
const pathsToWatch = plugin.getPathsToWatch!();
|
|
102
|
+
const relativePathsToWatch = pathsToWatch.map((p) =>
|
|
103
|
+
posixPath(path.relative(siteDir, p)),
|
|
104
|
+
);
|
|
105
|
+
expect(relativePathsToWatch).toEqual([
|
|
106
|
+
'blog/authors.yml',
|
|
107
|
+
'i18n/en/docusaurus-plugin-content-blog/**/*.{md,mdx}',
|
|
108
|
+
'blog/**/*.{md,mdx}',
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
|
|
92
112
|
test('simple website', async () => {
|
|
93
113
|
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
|
94
114
|
const blogPosts = await getBlogPosts(siteDir);
|
|
@@ -103,6 +123,7 @@ describe('loadBlog', () => {
|
|
|
103
123
|
source: path.posix.join('@site', PluginPath, 'date-matter.md'),
|
|
104
124
|
title: 'date-matter',
|
|
105
125
|
description: `date inside front matter`,
|
|
126
|
+
authors: [],
|
|
106
127
|
date: new Date('2019-01-01'),
|
|
107
128
|
formattedDate: 'January 1, 2019',
|
|
108
129
|
prevItem: undefined,
|
|
@@ -128,6 +149,16 @@ describe('loadBlog', () => {
|
|
|
128
149
|
),
|
|
129
150
|
title: 'Happy 1st Birthday Slash! (translated)',
|
|
130
151
|
description: `Happy birthday! (translated)`,
|
|
152
|
+
authors: [
|
|
153
|
+
{
|
|
154
|
+
name: 'Yangshun Tay (translated)',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
key: 'slorber',
|
|
158
|
+
name: 'Sébastien Lorber (translated)',
|
|
159
|
+
title: 'Docusaurus maintainer (translated)',
|
|
160
|
+
},
|
|
161
|
+
],
|
|
131
162
|
date: new Date('2018-12-14'),
|
|
132
163
|
formattedDate: 'December 14, 2018',
|
|
133
164
|
tags: [],
|
|
@@ -148,6 +179,7 @@ describe('loadBlog', () => {
|
|
|
148
179
|
source: path.posix.join('@site', PluginPath, 'complex-slug.md'),
|
|
149
180
|
title: 'Complex Slug',
|
|
150
181
|
description: `complex url slug`,
|
|
182
|
+
authors: [],
|
|
151
183
|
prevItem: undefined,
|
|
152
184
|
nextItem: {
|
|
153
185
|
permalink: '/blog/simple/slug',
|
|
@@ -169,6 +201,14 @@ describe('loadBlog', () => {
|
|
|
169
201
|
source: path.posix.join('@site', PluginPath, 'simple-slug.md'),
|
|
170
202
|
title: 'Simple Slug',
|
|
171
203
|
description: `simple url slug`,
|
|
204
|
+
authors: [
|
|
205
|
+
{
|
|
206
|
+
name: 'Sébastien Lorber',
|
|
207
|
+
title: 'Docusaurus maintainer',
|
|
208
|
+
url: 'https://sebastienlorber.com',
|
|
209
|
+
imageURL: undefined,
|
|
210
|
+
},
|
|
211
|
+
],
|
|
172
212
|
prevItem: undefined,
|
|
173
213
|
nextItem: {
|
|
174
214
|
permalink: '/blog/draft',
|
|
@@ -190,6 +230,7 @@ describe('loadBlog', () => {
|
|
|
190
230
|
source: path.posix.join('@site', PluginPath, 'heading-as-title.md'),
|
|
191
231
|
title: 'some heading',
|
|
192
232
|
description: '',
|
|
233
|
+
authors: [],
|
|
193
234
|
date: new Date('2019-01-02'),
|
|
194
235
|
formattedDate: 'January 2, 2019',
|
|
195
236
|
prevItem: undefined,
|
|
@@ -205,23 +246,29 @@ describe('loadBlog', () => {
|
|
|
205
246
|
test('simple website blog dates localized', async () => {
|
|
206
247
|
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
|
207
248
|
const blogPostsFrench = await getBlogPosts(siteDir, {}, getI18n('fr'));
|
|
208
|
-
expect(blogPostsFrench).toHaveLength(
|
|
249
|
+
expect(blogPostsFrench).toHaveLength(8);
|
|
209
250
|
expect(blogPostsFrench[0].metadata.formattedDate).toMatchInlineSnapshot(
|
|
210
|
-
`"
|
|
251
|
+
`"6 mars 2021"`,
|
|
211
252
|
);
|
|
212
253
|
expect(blogPostsFrench[1].metadata.formattedDate).toMatchInlineSnapshot(
|
|
213
|
-
`"
|
|
254
|
+
`"5 mars 2021"`,
|
|
214
255
|
);
|
|
215
256
|
expect(blogPostsFrench[2].metadata.formattedDate).toMatchInlineSnapshot(
|
|
216
|
-
`"
|
|
257
|
+
`"16 août 2020"`,
|
|
217
258
|
);
|
|
218
259
|
expect(blogPostsFrench[3].metadata.formattedDate).toMatchInlineSnapshot(
|
|
219
|
-
`"
|
|
260
|
+
`"15 août 2020"`,
|
|
220
261
|
);
|
|
221
262
|
expect(blogPostsFrench[4].metadata.formattedDate).toMatchInlineSnapshot(
|
|
222
|
-
`"
|
|
263
|
+
`"27 février 2020"`,
|
|
223
264
|
);
|
|
224
265
|
expect(blogPostsFrench[5].metadata.formattedDate).toMatchInlineSnapshot(
|
|
266
|
+
`"2 janvier 2019"`,
|
|
267
|
+
);
|
|
268
|
+
expect(blogPostsFrench[6].metadata.formattedDate).toMatchInlineSnapshot(
|
|
269
|
+
`"1 janvier 2019"`,
|
|
270
|
+
);
|
|
271
|
+
expect(blogPostsFrench[7].metadata.formattedDate).toMatchInlineSnapshot(
|
|
225
272
|
`"14 décembre 2018"`,
|
|
226
273
|
);
|
|
227
274
|
});
|
|
@@ -251,7 +298,8 @@ describe('loadBlog', () => {
|
|
|
251
298
|
expect(blogPost.metadata.editUrl).toEqual(hardcodedEditUrl);
|
|
252
299
|
});
|
|
253
300
|
|
|
254
|
-
expect(editUrlFunction).toHaveBeenCalledTimes(
|
|
301
|
+
expect(editUrlFunction).toHaveBeenCalledTimes(8);
|
|
302
|
+
|
|
255
303
|
expect(editUrlFunction).toHaveBeenCalledWith({
|
|
256
304
|
blogDirPath: 'blog',
|
|
257
305
|
blogPath: 'date-matter.md',
|
|
@@ -264,6 +312,18 @@ describe('loadBlog', () => {
|
|
|
264
312
|
permalink: '/blog/draft',
|
|
265
313
|
locale: 'en',
|
|
266
314
|
});
|
|
315
|
+
expect(editUrlFunction).toHaveBeenCalledWith({
|
|
316
|
+
blogDirPath: 'blog',
|
|
317
|
+
blogPath: 'mdx-blog-post.mdx',
|
|
318
|
+
permalink: '/blog/mdx-blog-post',
|
|
319
|
+
locale: 'en',
|
|
320
|
+
});
|
|
321
|
+
expect(editUrlFunction).toHaveBeenCalledWith({
|
|
322
|
+
blogDirPath: 'blog',
|
|
323
|
+
blogPath: 'mdx-require-blog-post.mdx',
|
|
324
|
+
permalink: '/blog/mdx-require-blog-post',
|
|
325
|
+
locale: 'en',
|
|
326
|
+
});
|
|
267
327
|
expect(editUrlFunction).toHaveBeenCalledWith({
|
|
268
328
|
blogDirPath: 'blog',
|
|
269
329
|
blogPath: 'complex-slug.md',
|
|
@@ -325,6 +385,7 @@ describe('loadBlog', () => {
|
|
|
325
385
|
source: noDateSource,
|
|
326
386
|
title: 'no date',
|
|
327
387
|
description: `no date`,
|
|
388
|
+
authors: [],
|
|
328
389
|
date: noDateSourceBirthTime,
|
|
329
390
|
formattedDate,
|
|
330
391
|
tags: [],
|
|
@@ -333,4 +394,15 @@ describe('loadBlog', () => {
|
|
|
333
394
|
truncated: false,
|
|
334
395
|
});
|
|
335
396
|
});
|
|
397
|
+
|
|
398
|
+
test('test ascending sort direction of blog post', async () => {
|
|
399
|
+
const siteDir = path.join(__dirname, '__fixtures__', 'website');
|
|
400
|
+
const normalOrder = await getBlogPosts(siteDir);
|
|
401
|
+
const reversedOrder = await getBlogPosts(siteDir, {
|
|
402
|
+
sortPosts: 'ascending',
|
|
403
|
+
});
|
|
404
|
+
expect(normalOrder.reverse().map((x) => x.metadata.date)).toEqual(
|
|
405
|
+
reversedOrder.map((x) => x.metadata.date),
|
|
406
|
+
);
|
|
407
|
+
});
|
|
336
408
|
});
|
|
@@ -29,7 +29,7 @@ test('should accept correctly defined user options', () => {
|
|
|
29
29
|
const {value, error} = PluginOptionSchema.validate(userOptions);
|
|
30
30
|
expect(value).toEqual({
|
|
31
31
|
...userOptions,
|
|
32
|
-
feedOptions: {type: ['rss'], title: 'myTitle'},
|
|
32
|
+
feedOptions: {type: ['rss'], title: 'myTitle', copyright: ''},
|
|
33
33
|
});
|
|
34
34
|
expect(error).toBe(undefined);
|
|
35
35
|
});
|
|
@@ -78,7 +78,7 @@ test('should convert all feed type to array with other feed type', () => {
|
|
|
78
78
|
});
|
|
79
79
|
expect(value).toEqual({
|
|
80
80
|
...DEFAULT_OPTIONS,
|
|
81
|
-
feedOptions: {type: ['rss', 'atom']},
|
|
81
|
+
feedOptions: {type: ['rss', 'atom'], copyright: ''},
|
|
82
82
|
});
|
|
83
83
|
});
|
|
84
84
|
|
|
@@ -106,7 +106,7 @@ test('should have array with rss + atom, title for missing feed type', () => {
|
|
|
106
106
|
});
|
|
107
107
|
expect(value).toEqual({
|
|
108
108
|
...DEFAULT_OPTIONS,
|
|
109
|
-
feedOptions: {type: ['rss', 'atom'], title: 'title'},
|
|
109
|
+
feedOptions: {type: ['rss', 'atom'], title: 'title', copyright: ''},
|
|
110
110
|
});
|
|
111
111
|
});
|
|
112
112
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {BlogPost, BlogContent, PluginOptions} from '../types';
|
|
9
|
+
import {getTranslationFiles, translateContent} from '../translations';
|
|
10
|
+
import {DEFAULT_OPTIONS} from '../pluginOptionSchema';
|
|
11
|
+
import {updateTranslationFileMessages} from '@docusaurus/utils';
|
|
12
|
+
|
|
13
|
+
const sampleBlogOptions: PluginOptions = {
|
|
14
|
+
...DEFAULT_OPTIONS,
|
|
15
|
+
blogSidebarTitle: 'All my posts',
|
|
16
|
+
blogTitle: 'My blog',
|
|
17
|
+
blogDescription: "Someone's random blog",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const sampleBlogPosts: BlogPost[] = [
|
|
21
|
+
{
|
|
22
|
+
id: 'hello',
|
|
23
|
+
metadata: {
|
|
24
|
+
permalink: '/blog/2021/06/19/hello',
|
|
25
|
+
source: '/blog/2021/06/19/hello',
|
|
26
|
+
description: '/blog/2021/06/19/hello',
|
|
27
|
+
date: new Date(2021, 6, 19),
|
|
28
|
+
formattedDate: 'June 19, 2021',
|
|
29
|
+
tags: [],
|
|
30
|
+
title: 'Hello',
|
|
31
|
+
truncated: true,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const sampleBlogContent: BlogContent = {
|
|
37
|
+
blogSidebarTitle: sampleBlogOptions.blogSidebarTitle,
|
|
38
|
+
blogListPaginated: [
|
|
39
|
+
{
|
|
40
|
+
items: ['hello'],
|
|
41
|
+
metadata: {
|
|
42
|
+
permalink: '/',
|
|
43
|
+
page: 1,
|
|
44
|
+
postsPerPage: 10,
|
|
45
|
+
totalPages: 1,
|
|
46
|
+
totalCount: 1,
|
|
47
|
+
previousPage: null,
|
|
48
|
+
nextPage: null,
|
|
49
|
+
blogTitle: sampleBlogOptions.blogTitle,
|
|
50
|
+
blogDescription: sampleBlogOptions.blogDescription,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
blogPosts: sampleBlogPosts,
|
|
55
|
+
blogTags: {},
|
|
56
|
+
blogTagsListPath: '/tags',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function getSampleTranslationFiles() {
|
|
60
|
+
return getTranslationFiles(sampleBlogOptions);
|
|
61
|
+
}
|
|
62
|
+
function getSampleTranslationFilesTranslated() {
|
|
63
|
+
const translationFiles = getSampleTranslationFiles();
|
|
64
|
+
return translationFiles.map((translationFile) =>
|
|
65
|
+
updateTranslationFileMessages(
|
|
66
|
+
translationFile,
|
|
67
|
+
(message) => `${message} (translated)`,
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe('getContentTranslationFiles', () => {
|
|
73
|
+
test('should return translation files matching snapshot', async () => {
|
|
74
|
+
expect(getSampleTranslationFiles()).toMatchSnapshot();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('translateContent', () => {
|
|
79
|
+
test('should not translate anything if translation files are untranslated', () => {
|
|
80
|
+
const translationFiles = getSampleTranslationFiles();
|
|
81
|
+
expect(translateContent(sampleBlogContent, translationFiles)).toEqual(
|
|
82
|
+
sampleBlogContent,
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('should return translated loaded content matching snapshot', () => {
|
|
87
|
+
const translationFiles = getSampleTranslationFilesTranslated();
|
|
88
|
+
expect(
|
|
89
|
+
translateContent(sampleBlogContent, translationFiles),
|
|
90
|
+
).toMatchSnapshot();
|
|
91
|
+
});
|
|
92
|
+
});
|
package/src/authors.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs-extra';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import {Author, BlogContentPaths} from './types';
|
|
12
|
+
import {findFolderContainingFile} from '@docusaurus/utils';
|
|
13
|
+
import {Joi, URISchema} from '@docusaurus/utils-validation';
|
|
14
|
+
import {
|
|
15
|
+
BlogPostFrontMatter,
|
|
16
|
+
BlogPostFrontMatterAuthor,
|
|
17
|
+
BlogPostFrontMatterAuthors,
|
|
18
|
+
} from './blogFrontMatter';
|
|
19
|
+
import {getContentPathList} from './blogUtils';
|
|
20
|
+
import Yaml from 'js-yaml';
|
|
21
|
+
|
|
22
|
+
export type AuthorsMap = Record<string, Author>;
|
|
23
|
+
|
|
24
|
+
const AuthorsMapSchema = Joi.object<AuthorsMap>().pattern(
|
|
25
|
+
Joi.string(),
|
|
26
|
+
Joi.object({
|
|
27
|
+
name: Joi.string().required(),
|
|
28
|
+
url: URISchema,
|
|
29
|
+
imageURL: URISchema,
|
|
30
|
+
title: Joi.string(),
|
|
31
|
+
})
|
|
32
|
+
.rename('image_url', 'imageURL')
|
|
33
|
+
.unknown()
|
|
34
|
+
.required(),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export function validateAuthorsMapFile(content: unknown): AuthorsMap {
|
|
38
|
+
return Joi.attempt(content, AuthorsMapSchema);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function readAuthorsMapFile(
|
|
42
|
+
filePath: string,
|
|
43
|
+
): Promise<AuthorsMap | undefined> {
|
|
44
|
+
if (await fs.pathExists(filePath)) {
|
|
45
|
+
const contentString = await fs.readFile(filePath, {encoding: 'utf8'});
|
|
46
|
+
try {
|
|
47
|
+
const unsafeContent = Yaml.load(contentString);
|
|
48
|
+
return validateAuthorsMapFile(unsafeContent);
|
|
49
|
+
} catch (e) {
|
|
50
|
+
// TODO replace later by error cause: see https://v8.dev/features/error-cause
|
|
51
|
+
console.error(chalk.red('The author list file looks invalid!'));
|
|
52
|
+
throw e;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type AuthorsMapParams = {
|
|
59
|
+
authorsMapPath: string;
|
|
60
|
+
contentPaths: BlogContentPaths;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export async function getAuthorsMapFilePath({
|
|
64
|
+
authorsMapPath,
|
|
65
|
+
contentPaths,
|
|
66
|
+
}: AuthorsMapParams): Promise<string | undefined> {
|
|
67
|
+
// Useful to load an eventually localize authors map
|
|
68
|
+
const contentPath = await findFolderContainingFile(
|
|
69
|
+
getContentPathList(contentPaths),
|
|
70
|
+
authorsMapPath,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (contentPath) {
|
|
74
|
+
return path.join(contentPath, authorsMapPath);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function getAuthorsMap(
|
|
81
|
+
params: AuthorsMapParams,
|
|
82
|
+
): Promise<AuthorsMap | undefined> {
|
|
83
|
+
const filePath = await getAuthorsMapFilePath(params);
|
|
84
|
+
if (!filePath) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
return await readAuthorsMapFile(filePath);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
// TODO replace later by error cause, see https://v8.dev/features/error-cause
|
|
91
|
+
console.error(
|
|
92
|
+
chalk.red(`Couldn't read blog authors map at path ${filePath}`),
|
|
93
|
+
);
|
|
94
|
+
throw e;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
type AuthorsParam = {
|
|
99
|
+
frontMatter: BlogPostFrontMatter;
|
|
100
|
+
authorsMap: AuthorsMap | undefined;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Legacy v1/early-v2 frontmatter fields
|
|
104
|
+
// We may want to deprecate those in favor of using only frontMatter.authors
|
|
105
|
+
function getFrontMatterAuthorLegacy(
|
|
106
|
+
frontMatter: BlogPostFrontMatter,
|
|
107
|
+
): BlogPostFrontMatterAuthor | undefined {
|
|
108
|
+
const name = frontMatter.author;
|
|
109
|
+
const title = frontMatter.author_title ?? frontMatter.authorTitle;
|
|
110
|
+
const url = frontMatter.author_url ?? frontMatter.authorURL;
|
|
111
|
+
const imageURL = frontMatter.author_image_url ?? frontMatter.authorImageURL;
|
|
112
|
+
|
|
113
|
+
// Shouldn't we require at least an author name?
|
|
114
|
+
if (name || title || url || imageURL) {
|
|
115
|
+
return {
|
|
116
|
+
name,
|
|
117
|
+
title,
|
|
118
|
+
url,
|
|
119
|
+
imageURL,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeFrontMatterAuthors(
|
|
127
|
+
frontMatterAuthors: BlogPostFrontMatterAuthors = [],
|
|
128
|
+
): BlogPostFrontMatterAuthor[] {
|
|
129
|
+
function normalizeAuthor(
|
|
130
|
+
authorInput: string | BlogPostFrontMatterAuthor,
|
|
131
|
+
): BlogPostFrontMatterAuthor {
|
|
132
|
+
if (typeof authorInput === 'string') {
|
|
133
|
+
// Technically, we could allow users to provide an author's name here
|
|
134
|
+
// IMHO it's better to only support keys here
|
|
135
|
+
// Reason: a typo in a key would fallback to becoming a name and may end-up un-noticed
|
|
136
|
+
return {key: authorInput};
|
|
137
|
+
}
|
|
138
|
+
return authorInput;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return Array.isArray(frontMatterAuthors)
|
|
142
|
+
? frontMatterAuthors.map(normalizeAuthor)
|
|
143
|
+
: [normalizeAuthor(frontMatterAuthors)];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getFrontMatterAuthors(params: AuthorsParam): Author[] {
|
|
147
|
+
const {authorsMap} = params;
|
|
148
|
+
const frontMatterAuthors = normalizeFrontMatterAuthors(
|
|
149
|
+
params.frontMatter.authors,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
function getAuthorsMapAuthor(key: string | undefined): Author | undefined {
|
|
153
|
+
if (key) {
|
|
154
|
+
if (!authorsMap || Object.keys(authorsMap).length === 0) {
|
|
155
|
+
throw new Error(`Can't reference blog post authors by a key (such as '${key}') because no authors map file could be loaded.
|
|
156
|
+
Please double-check your blog plugin config (in particular 'authorsMapPath'), ensure the file exists at the configured path, is not empty, and is valid!`);
|
|
157
|
+
}
|
|
158
|
+
const author = authorsMap[key];
|
|
159
|
+
if (!author) {
|
|
160
|
+
throw Error(`Blog author with key "${key}" not found in the authors map file.
|
|
161
|
+
Valid author keys are:
|
|
162
|
+
${Object.keys(authorsMap)
|
|
163
|
+
.map((validKey) => `- ${validKey}`)
|
|
164
|
+
.join('\n')}`);
|
|
165
|
+
}
|
|
166
|
+
return author;
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function toAuthor(frontMatterAuthor: BlogPostFrontMatterAuthor): Author {
|
|
172
|
+
return {
|
|
173
|
+
// Author def from authorsMap can be locally overridden by frontmatter
|
|
174
|
+
...getAuthorsMapAuthor(frontMatterAuthor.key),
|
|
175
|
+
...frontMatterAuthor,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return frontMatterAuthors.map(toAuthor);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function getBlogPostAuthors(params: AuthorsParam): Author[] {
|
|
183
|
+
const authorLegacy = getFrontMatterAuthorLegacy(params.frontMatter);
|
|
184
|
+
const authors = getFrontMatterAuthors(params);
|
|
185
|
+
|
|
186
|
+
if (authorLegacy) {
|
|
187
|
+
// Technically, we could allow mixing legacy/authors frontmatter, but do we really want to?
|
|
188
|
+
if (authors.length > 0) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`To declare blog post authors, use the 'authors' FrontMatter in priority.
|
|
191
|
+
Don't mix 'authors' with other existing 'author_*' FrontMatter. Choose one or the other, not both at the same time.`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
return [authorLegacy];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return authors;
|
|
198
|
+
}
|
package/src/blogFrontMatter.ts
CHANGED
|
@@ -5,80 +5,119 @@
|
|
|
5
5
|
* LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
/* eslint-disable camelcase */
|
|
9
|
-
|
|
10
8
|
import {
|
|
11
9
|
JoiFrontMatter as Joi, // Custom instance for frontmatter
|
|
10
|
+
URISchema,
|
|
12
11
|
validateFrontMatter,
|
|
12
|
+
FrontMatterTagsSchema,
|
|
13
|
+
FrontMatterTOCHeadingLevels,
|
|
13
14
|
} from '@docusaurus/utils-validation';
|
|
14
|
-
import {
|
|
15
|
+
import type {FrontMatterTag} from '@docusaurus/utils';
|
|
16
|
+
|
|
17
|
+
export type BlogPostFrontMatterAuthor = Record<string, unknown> & {
|
|
18
|
+
key?: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
imageURL?: string;
|
|
21
|
+
url?: string;
|
|
22
|
+
title?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// All the possible variants that the user can use for convenience
|
|
26
|
+
export type BlogPostFrontMatterAuthors =
|
|
27
|
+
| string
|
|
28
|
+
| BlogPostFrontMatterAuthor
|
|
29
|
+
| (string | BlogPostFrontMatterAuthor)[];
|
|
30
|
+
|
|
31
|
+
const BlogPostFrontMatterAuthorSchema = Joi.object({
|
|
32
|
+
key: Joi.string(),
|
|
33
|
+
name: Joi.string(),
|
|
34
|
+
title: Joi.string(),
|
|
35
|
+
url: URISchema,
|
|
36
|
+
imageURL: Joi.string(),
|
|
37
|
+
})
|
|
38
|
+
.or('key', 'name')
|
|
39
|
+
.rename('image_url', 'imageURL', {alias: true});
|
|
15
40
|
|
|
16
41
|
export type BlogPostFrontMatter = {
|
|
17
42
|
id?: string;
|
|
18
43
|
title?: string;
|
|
19
44
|
description?: string;
|
|
20
|
-
tags?:
|
|
45
|
+
tags?: FrontMatterTag[];
|
|
21
46
|
slug?: string;
|
|
22
47
|
draft?: boolean;
|
|
23
|
-
date?: Date;
|
|
48
|
+
date?: Date | string; // Yaml automagically convert some string patterns as Date, but not all
|
|
24
49
|
|
|
50
|
+
authors?: BlogPostFrontMatterAuthors;
|
|
51
|
+
|
|
52
|
+
// We may want to deprecate those older author frontmatter fields later:
|
|
25
53
|
author?: string;
|
|
26
54
|
author_title?: string;
|
|
27
55
|
author_url?: string;
|
|
28
56
|
author_image_url?: string;
|
|
29
57
|
|
|
30
|
-
image?: string;
|
|
31
|
-
keywords?: string[];
|
|
32
|
-
hide_table_of_contents?: boolean;
|
|
33
|
-
|
|
34
58
|
/** @deprecated */
|
|
35
59
|
authorTitle?: string;
|
|
60
|
+
/** @deprecated */
|
|
36
61
|
authorURL?: string;
|
|
62
|
+
/** @deprecated */
|
|
37
63
|
authorImageURL?: string;
|
|
64
|
+
|
|
65
|
+
image?: string;
|
|
66
|
+
keywords?: string[];
|
|
67
|
+
hide_table_of_contents?: boolean;
|
|
68
|
+
toc_min_heading_level?: number;
|
|
69
|
+
toc_max_heading_level?: number;
|
|
38
70
|
};
|
|
39
71
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// While the user did not provide those values explicitly
|
|
43
|
-
// We use default values in code instead
|
|
44
|
-
const BlogTagSchema = Joi.alternatives().try(
|
|
45
|
-
Joi.string().required(),
|
|
46
|
-
Joi.object<Tag>({
|
|
47
|
-
label: Joi.string().required(),
|
|
48
|
-
permalink: Joi.string().required(),
|
|
49
|
-
}),
|
|
50
|
-
);
|
|
72
|
+
const FrontMatterAuthorErrorMessage =
|
|
73
|
+
'{{#label}} does not look like a valid blog post author. Please use an author key or an author object (with a key and/or name).';
|
|
51
74
|
|
|
52
75
|
const BlogFrontMatterSchema = Joi.object<BlogPostFrontMatter>({
|
|
53
76
|
id: Joi.string(),
|
|
54
77
|
title: Joi.string().allow(''),
|
|
55
78
|
description: Joi.string().allow(''),
|
|
56
|
-
tags:
|
|
79
|
+
tags: FrontMatterTagsSchema,
|
|
57
80
|
draft: Joi.boolean(),
|
|
58
81
|
date: Joi.date().raw(),
|
|
59
82
|
|
|
83
|
+
// New multi-authors frontmatter:
|
|
84
|
+
authors: Joi.alternatives()
|
|
85
|
+
.try(
|
|
86
|
+
Joi.string(),
|
|
87
|
+
BlogPostFrontMatterAuthorSchema,
|
|
88
|
+
Joi.array()
|
|
89
|
+
.items(Joi.string(), BlogPostFrontMatterAuthorSchema)
|
|
90
|
+
.messages({
|
|
91
|
+
'array.sparse': FrontMatterAuthorErrorMessage,
|
|
92
|
+
'array.includes': FrontMatterAuthorErrorMessage,
|
|
93
|
+
}),
|
|
94
|
+
)
|
|
95
|
+
.messages({
|
|
96
|
+
'alternatives.match': FrontMatterAuthorErrorMessage,
|
|
97
|
+
}),
|
|
98
|
+
// Legacy author frontmatter
|
|
60
99
|
author: Joi.string(),
|
|
61
100
|
author_title: Joi.string(),
|
|
62
|
-
author_url:
|
|
63
|
-
author_image_url:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
keywords: Joi.array().items(Joi.string().required()),
|
|
67
|
-
hide_table_of_contents: Joi.boolean(),
|
|
68
|
-
|
|
69
|
-
// TODO re-enable warnings later, our v1 blog posts use those older frontmatter fields
|
|
70
|
-
authorURL: Joi.string().uri(),
|
|
101
|
+
author_url: URISchema,
|
|
102
|
+
author_image_url: URISchema,
|
|
103
|
+
// TODO enable deprecation warnings later
|
|
104
|
+
authorURL: URISchema,
|
|
71
105
|
// .warning('deprecate.error', { alternative: '"author_url"'}),
|
|
72
106
|
authorTitle: Joi.string(),
|
|
73
107
|
// .warning('deprecate.error', { alternative: '"author_title"'}),
|
|
74
|
-
authorImageURL:
|
|
108
|
+
authorImageURL: URISchema,
|
|
75
109
|
// .warning('deprecate.error', { alternative: '"author_image_url"'}),
|
|
76
|
-
|
|
77
|
-
.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
110
|
+
|
|
111
|
+
slug: Joi.string(),
|
|
112
|
+
image: URISchema,
|
|
113
|
+
keywords: Joi.array().items(Joi.string().required()),
|
|
114
|
+
hide_table_of_contents: Joi.boolean(),
|
|
115
|
+
|
|
116
|
+
...FrontMatterTOCHeadingLevels,
|
|
117
|
+
}).messages({
|
|
118
|
+
'deprecate.error':
|
|
119
|
+
'{#label} blog frontMatter field is deprecated. Please use {#alternative} instead.',
|
|
120
|
+
});
|
|
82
121
|
|
|
83
122
|
export function validateBlogPostFrontMatter(
|
|
84
123
|
frontMatter: Record<string, unknown>,
|