@docusaurus/plugin-content-blog 2.0.0-beta.6f366f4b4 → 2.0.0-beta.7

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.
Files changed (53) hide show
  1. package/lib/.tsbuildinfo +1 -1
  2. package/lib/authors.d.ts +23 -0
  3. package/lib/authors.js +150 -0
  4. package/lib/blogFrontMatter.d.ts +19 -6
  5. package/lib/blogFrontMatter.js +31 -19
  6. package/lib/blogUtils.d.ts +10 -2
  7. package/lib/blogUtils.js +146 -104
  8. package/lib/index.js +76 -70
  9. package/lib/markdownLoader.js +3 -3
  10. package/lib/pluginOptionSchema.d.ts +3 -27
  11. package/lib/pluginOptionSchema.js +19 -7
  12. package/lib/translations.d.ts +10 -0
  13. package/lib/translations.js +53 -0
  14. package/lib/types.d.ts +37 -14
  15. package/package.json +13 -11
  16. package/src/__tests__/__fixtures__/authorsMapFiles/authors.json +29 -0
  17. package/src/__tests__/__fixtures__/authorsMapFiles/authors.yml +27 -0
  18. package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad1.json +5 -0
  19. package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad1.yml +3 -0
  20. package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad2.json +3 -0
  21. package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad2.yml +2 -0
  22. package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad3.json +8 -0
  23. package/src/__tests__/__fixtures__/authorsMapFiles/authorsBad3.yml +3 -0
  24. package/src/__tests__/__fixtures__/component/Typography.tsx +6 -0
  25. package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathEmpty/empty +0 -0
  26. package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathJson1/authors.json +0 -0
  27. package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathJson2/authors.json +0 -0
  28. package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathNestedYml/sub/folder/authors.yml +0 -0
  29. package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathYml1/authors.yml +0 -0
  30. package/src/__tests__/__fixtures__/getAuthorsMapFilePath/contentPathYml2/authors.yml +0 -0
  31. package/src/__tests__/__fixtures__/website/blog/2018-12-14-Happy-First-Birthday-Slash.md +3 -0
  32. package/src/__tests__/__fixtures__/website/blog/authors.yml +4 -0
  33. package/src/__tests__/__fixtures__/website/blog/mdx-blog-post.mdx +36 -0
  34. package/src/__tests__/__fixtures__/website/blog/simple-slug.md +4 -0
  35. package/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/2018-12-14-Happy-First-Birthday-Slash.md +3 -0
  36. package/src/__tests__/__fixtures__/website/i18n/en/docusaurus-plugin-content-blog/authors.yml +5 -0
  37. package/src/__tests__/__snapshots__/generateBlogFeed.test.ts.snap +41 -3
  38. package/src/__tests__/__snapshots__/translations.test.ts.snap +64 -0
  39. package/src/__tests__/authors.test.ts +608 -0
  40. package/src/__tests__/blogFrontMatter.test.ts +93 -16
  41. package/src/__tests__/blogUtils.test.ts +94 -0
  42. package/src/__tests__/generateBlogFeed.test.ts +4 -0
  43. package/src/__tests__/index.test.ts +63 -12
  44. package/src/__tests__/pluginOptionSchema.test.ts +3 -3
  45. package/src/__tests__/translations.test.ts +92 -0
  46. package/src/authors.ts +202 -0
  47. package/src/blogFrontMatter.ts +73 -33
  48. package/src/blogUtils.ts +205 -132
  49. package/src/index.ts +92 -61
  50. package/{index.d.ts → src/plugin-content-blog.d.ts} +35 -31
  51. package/src/pluginOptionSchema.ts +22 -9
  52. package/src/translations.ts +63 -0
  53. package/src/types.ts +47 -16
package/src/authors.ts ADDED
@@ -0,0 +1,202 @@
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
+ const parse =
47
+ filePath.endsWith('.yml') || filePath.endsWith('.yaml')
48
+ ? Yaml.load
49
+ : JSON.parse;
50
+ try {
51
+ const unsafeContent = parse(contentString);
52
+ return validateAuthorsMapFile(unsafeContent);
53
+ } catch (e) {
54
+ // TODO replace later by error cause: see https://v8.dev/features/error-cause
55
+ console.error(chalk.red('The author list file looks invalid!'));
56
+ throw e;
57
+ }
58
+ }
59
+ return undefined;
60
+ }
61
+
62
+ type AuthorsMapParams = {
63
+ authorsMapPath: string;
64
+ contentPaths: BlogContentPaths;
65
+ };
66
+
67
+ export async function getAuthorsMapFilePath({
68
+ authorsMapPath,
69
+ contentPaths,
70
+ }: AuthorsMapParams): Promise<string | undefined> {
71
+ // Useful to load an eventually localize authors map
72
+ const contentPath = await findFolderContainingFile(
73
+ getContentPathList(contentPaths),
74
+ authorsMapPath,
75
+ );
76
+
77
+ if (contentPath) {
78
+ return path.join(contentPath, authorsMapPath);
79
+ }
80
+
81
+ return undefined;
82
+ }
83
+
84
+ export async function getAuthorsMap(
85
+ params: AuthorsMapParams,
86
+ ): Promise<AuthorsMap | undefined> {
87
+ const filePath = await getAuthorsMapFilePath(params);
88
+ if (!filePath) {
89
+ return undefined;
90
+ }
91
+ try {
92
+ return await readAuthorsMapFile(filePath);
93
+ } catch (e) {
94
+ // TODO replace later by error cause, see https://v8.dev/features/error-cause
95
+ console.error(
96
+ chalk.red(`Couldn't read blog authors map at path ${filePath}`),
97
+ );
98
+ throw e;
99
+ }
100
+ }
101
+
102
+ type AuthorsParam = {
103
+ frontMatter: BlogPostFrontMatter;
104
+ authorsMap: AuthorsMap | undefined;
105
+ };
106
+
107
+ // Legacy v1/early-v2 frontmatter fields
108
+ // We may want to deprecate those in favor of using only frontMatter.authors
109
+ function getFrontMatterAuthorLegacy(
110
+ frontMatter: BlogPostFrontMatter,
111
+ ): BlogPostFrontMatterAuthor | undefined {
112
+ const name = frontMatter.author;
113
+ const title = frontMatter.author_title ?? frontMatter.authorTitle;
114
+ const url = frontMatter.author_url ?? frontMatter.authorURL;
115
+ const imageURL = frontMatter.author_image_url ?? frontMatter.authorImageURL;
116
+
117
+ // Shouldn't we require at least an author name?
118
+ if (name || title || url || imageURL) {
119
+ return {
120
+ name,
121
+ title,
122
+ url,
123
+ imageURL,
124
+ };
125
+ }
126
+
127
+ return undefined;
128
+ }
129
+
130
+ function normalizeFrontMatterAuthors(
131
+ frontMatterAuthors: BlogPostFrontMatterAuthors = [],
132
+ ): BlogPostFrontMatterAuthor[] {
133
+ function normalizeAuthor(
134
+ authorInput: string | BlogPostFrontMatterAuthor,
135
+ ): BlogPostFrontMatterAuthor {
136
+ if (typeof authorInput === 'string') {
137
+ // Technically, we could allow users to provide an author's name here
138
+ // IMHO it's better to only support keys here
139
+ // Reason: a typo in a key would fallback to becoming a name and may end-up un-noticed
140
+ return {key: authorInput};
141
+ }
142
+ return authorInput;
143
+ }
144
+
145
+ return Array.isArray(frontMatterAuthors)
146
+ ? frontMatterAuthors.map(normalizeAuthor)
147
+ : [normalizeAuthor(frontMatterAuthors)];
148
+ }
149
+
150
+ function getFrontMatterAuthors(params: AuthorsParam): Author[] {
151
+ const {authorsMap} = params;
152
+ const frontMatterAuthors = normalizeFrontMatterAuthors(
153
+ params.frontMatter.authors,
154
+ );
155
+
156
+ function getAuthorsMapAuthor(key: string | undefined): Author | undefined {
157
+ if (key) {
158
+ if (!authorsMap || Object.keys(authorsMap).length === 0) {
159
+ throw new Error(`Can't reference blog post authors by a key (such as '${key}') because no authors map file could be loaded.
160
+ Please double-check your blog plugin config (in particular 'authorsMapPath'), ensure the file exists at the configured path, is not empty, and is valid!`);
161
+ }
162
+ const author = authorsMap[key];
163
+ if (!author) {
164
+ throw Error(`Blog author with key "${key}" not found in the authors map file.
165
+ Valid author keys are:
166
+ ${Object.keys(authorsMap)
167
+ .map((validKey) => `- ${validKey}`)
168
+ .join('\n')}`);
169
+ }
170
+ return author;
171
+ }
172
+ return undefined;
173
+ }
174
+
175
+ function toAuthor(frontMatterAuthor: BlogPostFrontMatterAuthor): Author {
176
+ return {
177
+ // Author def from authorsMap can be locally overridden by frontmatter
178
+ ...getAuthorsMapAuthor(frontMatterAuthor.key),
179
+ ...frontMatterAuthor,
180
+ };
181
+ }
182
+
183
+ return frontMatterAuthors.map(toAuthor);
184
+ }
185
+
186
+ export function getBlogPostAuthors(params: AuthorsParam): Author[] {
187
+ const authorLegacy = getFrontMatterAuthorLegacy(params.frontMatter);
188
+ const authors = getFrontMatterAuthors(params);
189
+
190
+ if (authorLegacy) {
191
+ // Technically, we could allow mixing legacy/authors frontmatter, but do we really want to?
192
+ if (authors.length > 0) {
193
+ throw new Error(
194
+ `To declare blog post authors, use the 'authors' FrontMatter in priority.
195
+ Don't mix 'authors' with other existing 'author_*' FrontMatter. Choose one or the other, not both at the same time.`,
196
+ );
197
+ }
198
+ return [authorLegacy];
199
+ }
200
+
201
+ return authors;
202
+ }
@@ -5,81 +5,121 @@
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
12
10
  URISchema,
13
11
  validateFrontMatter,
12
+ FrontMatterTagsSchema,
13
+ FrontMatterTOCHeadingLevels,
14
14
  } from '@docusaurus/utils-validation';
15
- import {Tag} from './types';
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});
16
40
 
17
41
  export type BlogPostFrontMatter = {
42
+ /* eslint-disable camelcase */
18
43
  id?: string;
19
44
  title?: string;
20
45
  description?: string;
21
- tags?: (string | Tag)[];
46
+ tags?: FrontMatterTag[];
22
47
  slug?: string;
23
48
  draft?: boolean;
24
- date?: Date;
49
+ date?: Date | string; // Yaml automagically convert some string patterns as Date, but not all
50
+
51
+ authors?: BlogPostFrontMatterAuthors;
25
52
 
53
+ // We may want to deprecate those older author frontmatter fields later:
26
54
  author?: string;
27
55
  author_title?: string;
28
56
  author_url?: string;
29
57
  author_image_url?: string;
30
58
 
31
- image?: string;
32
- keywords?: string[];
33
- hide_table_of_contents?: boolean;
34
-
35
59
  /** @deprecated */
36
60
  authorTitle?: string;
61
+ /** @deprecated */
37
62
  authorURL?: string;
63
+ /** @deprecated */
38
64
  authorImageURL?: string;
65
+
66
+ image?: string;
67
+ keywords?: string[];
68
+ hide_table_of_contents?: boolean;
69
+ toc_min_heading_level?: number;
70
+ toc_max_heading_level?: number;
71
+ /* eslint-enable camelcase */
39
72
  };
40
73
 
41
- // NOTE: we don't add any default value on purpose here
42
- // We don't want default values to magically appear in doc metadatas and props
43
- // While the user did not provide those values explicitly
44
- // We use default values in code instead
45
- const BlogTagSchema = Joi.alternatives().try(
46
- Joi.string().required(),
47
- Joi.object<Tag>({
48
- label: Joi.string().required(),
49
- permalink: Joi.string().required(),
50
- }),
51
- );
74
+ const FrontMatterAuthorErrorMessage =
75
+ '{{#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).';
52
76
 
53
77
  const BlogFrontMatterSchema = Joi.object<BlogPostFrontMatter>({
54
78
  id: Joi.string(),
55
79
  title: Joi.string().allow(''),
56
80
  description: Joi.string().allow(''),
57
- tags: Joi.array().items(BlogTagSchema),
81
+ tags: FrontMatterTagsSchema,
58
82
  draft: Joi.boolean(),
59
83
  date: Joi.date().raw(),
60
84
 
85
+ // New multi-authors frontmatter:
86
+ authors: Joi.alternatives()
87
+ .try(
88
+ Joi.string(),
89
+ BlogPostFrontMatterAuthorSchema,
90
+ Joi.array()
91
+ .items(Joi.string(), BlogPostFrontMatterAuthorSchema)
92
+ .messages({
93
+ 'array.sparse': FrontMatterAuthorErrorMessage,
94
+ 'array.includes': FrontMatterAuthorErrorMessage,
95
+ }),
96
+ )
97
+ .messages({
98
+ 'alternatives.match': FrontMatterAuthorErrorMessage,
99
+ }),
100
+ // Legacy author frontmatter
61
101
  author: Joi.string(),
62
102
  author_title: Joi.string(),
63
103
  author_url: URISchema,
64
104
  author_image_url: URISchema,
65
- slug: Joi.string(),
66
- image: URISchema,
67
- keywords: Joi.array().items(Joi.string().required()),
68
- hide_table_of_contents: Joi.boolean(),
69
-
70
- // TODO re-enable warnings later, our v1 blog posts use those older frontmatter fields
105
+ // TODO enable deprecation warnings later
71
106
  authorURL: URISchema,
72
107
  // .warning('deprecate.error', { alternative: '"author_url"'}),
73
108
  authorTitle: Joi.string(),
74
109
  // .warning('deprecate.error', { alternative: '"author_title"'}),
75
110
  authorImageURL: URISchema,
76
111
  // .warning('deprecate.error', { alternative: '"author_image_url"'}),
77
- })
78
- .unknown()
79
- .messages({
80
- 'deprecate.error':
81
- '{#label} blog frontMatter field is deprecated. Please use {#alternative} instead.',
82
- });
112
+
113
+ slug: Joi.string(),
114
+ image: URISchema,
115
+ keywords: Joi.array().items(Joi.string().required()),
116
+ hide_table_of_contents: Joi.boolean(),
117
+
118
+ ...FrontMatterTOCHeadingLevels,
119
+ }).messages({
120
+ 'deprecate.error':
121
+ '{#label} blog frontMatter field is deprecated. Please use {#alternative} instead.',
122
+ });
83
123
 
84
124
  export function validateBlogPostFrontMatter(
85
125
  frontMatter: Record<string, unknown>,