@azerion/bunosaurus-plugin-content-blog 0.0.0-stage → 4.0.0
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/README.md +6 -2
- package/assets/atom.css +81 -0
- package/assets/atom.xsl +98 -0
- package/assets/rss.css +81 -0
- package/assets/rss.xsl +92 -0
- package/lib/authors.d.ts +26 -0
- package/lib/authors.js +132 -0
- package/lib/authorsMap.d.ts +24 -0
- package/lib/authorsMap.js +118 -0
- package/lib/authorsProblems.d.ts +21 -0
- package/lib/authorsProblems.js +51 -0
- package/lib/authorsSocials.d.ts +10 -0
- package/lib/authorsSocials.js +68 -0
- package/lib/blogUtils.d.ts +50 -0
- package/lib/blogUtils.js +296 -0
- package/lib/client/contexts.d.ts +33 -0
- package/lib/client/contexts.js +54 -0
- package/lib/client/index.d.ts +9 -0
- package/lib/client/index.js +9 -0
- package/lib/client/sidebarUtils.d.ts +21 -0
- package/lib/client/sidebarUtils.js +49 -0
- package/lib/client/structuredDataUtils.d.ts +10 -0
- package/lib/client/structuredDataUtils.js +130 -0
- package/lib/contentHelpers.d.ts +12 -0
- package/lib/contentHelpers.js +30 -0
- package/lib/feed.d.ts +21 -0
- package/lib/feed.js +250 -0
- package/lib/frontMatter.d.ts +10 -0
- package/lib/frontMatter.js +68 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +275 -0
- package/lib/markdownLoader.d.ts +9 -0
- package/lib/markdownLoader.js +26 -0
- package/lib/options.d.ts +14 -0
- package/lib/options.js +202 -0
- package/lib/props.d.ts +23 -0
- package/lib/props.js +43 -0
- package/lib/readingTime.d.ts +17 -0
- package/lib/readingTime.js +45 -0
- package/lib/remark/footnoteIDFixer.d.ts +14 -0
- package/lib/remark/footnoteIDFixer.js +28 -0
- package/lib/routes.d.ts +18 -0
- package/lib/routes.js +263 -0
- package/lib/translations.d.ts +10 -0
- package/lib/translations.js +40 -0
- package/lib/types.d.ts +11 -0
- package/lib/types.js +8 -0
- package/package.json +70 -4
- package/src/authors.ts +191 -0
- package/src/authorsMap.ts +178 -0
- package/src/authorsProblems.ts +72 -0
- package/src/authorsSocials.ts +86 -0
- package/src/blogUtils.ts +478 -0
- package/src/client/contexts.tsx +95 -0
- package/src/client/index.tsx +24 -0
- package/src/client/sidebarUtils.test.ts +53 -0
- package/src/client/sidebarUtils.tsx +85 -0
- package/src/client/structuredDataUtils.ts +190 -0
- package/src/contentHelpers.ts +35 -0
- package/src/feed.ts +442 -0
- package/src/frontMatter.ts +90 -0
- package/src/index.ts +389 -0
- package/src/markdownLoader.ts +34 -0
- package/src/options.ts +258 -0
- package/src/plugin-content-blog.d.ts +856 -0
- package/src/props.ts +75 -0
- package/src/readingTime.ts +49 -0
- package/src/remark/footnoteIDFixer.ts +29 -0
- package/src/routes.ts +365 -0
- package/src/translations.ts +46 -0
- package/src/types.ts +14 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.reportAuthorsProblems = reportAuthorsProblems;
|
|
10
|
+
exports.reportInlineAuthors = reportInlineAuthors;
|
|
11
|
+
exports.reportDuplicateAuthors = reportDuplicateAuthors;
|
|
12
|
+
const tslib_1 = require("tslib");
|
|
13
|
+
const lodash_1 = tslib_1.__importDefault(require("lodash"));
|
|
14
|
+
const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
|
|
15
|
+
function reportAuthorsProblems(params) {
|
|
16
|
+
reportInlineAuthors(params);
|
|
17
|
+
reportDuplicateAuthors(params);
|
|
18
|
+
}
|
|
19
|
+
function reportInlineAuthors({ authors, blogSourceRelative, options: { onInlineAuthors, authorsMapPath }, }) {
|
|
20
|
+
if (onInlineAuthors === 'ignore') {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const inlineAuthors = authors.filter((author) => !author.key);
|
|
24
|
+
if (inlineAuthors.length > 0) {
|
|
25
|
+
logger_1.default.report(onInlineAuthors)(logger_1.default.interpolate `Some blog authors used in path=${blogSourceRelative} are not defined in path=${authorsMapPath}:
|
|
26
|
+
- ${inlineAuthors.map(authorToString).join('\n- ')}
|
|
27
|
+
|
|
28
|
+
Note that we recommend to declare authors once in a path=${authorsMapPath} file and reference them by key in blog posts front matter to avoid author info duplication.
|
|
29
|
+
But if you want to allow inline blog authors, you can disable this message by setting onInlineAuthors: 'ignore' in your blog plugin options.
|
|
30
|
+
More info at url=${'https://docusaurus.io/docs/blog'}
|
|
31
|
+
`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function reportDuplicateAuthors({ authors, blogSourceRelative, }) {
|
|
35
|
+
const duplicateAuthors = (0, lodash_1.default)(authors)
|
|
36
|
+
// for now we only check for predefined authors duplicates
|
|
37
|
+
.filter((author) => !!author.key)
|
|
38
|
+
.groupBy((author) => author.key)
|
|
39
|
+
.pickBy((authorsByKey) => authorsByKey.length > 1)
|
|
40
|
+
// We only keep the "test" of all the duplicate groups
|
|
41
|
+
// The first author of a group is not really a duplicate...
|
|
42
|
+
.flatMap(([, ...rest]) => rest)
|
|
43
|
+
.value();
|
|
44
|
+
if (duplicateAuthors.length > 0) {
|
|
45
|
+
throw new Error(logger_1.default.interpolate `Duplicate blog post authors were found in blog post path=${blogSourceRelative} front matter:
|
|
46
|
+
- ${duplicateAuthors.map(authorToString).join('\n- ')}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function authorToString(author) {
|
|
50
|
+
return JSON.stringify(author);
|
|
51
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
import { Joi } from '@docusaurus/utils-validation';
|
|
8
|
+
import type { AuthorSocials } from '@docusaurus/plugin-content-blog';
|
|
9
|
+
export declare const AuthorSocialsSchema: Joi.ObjectSchema<AuthorSocials>;
|
|
10
|
+
export declare const normalizeSocials: (socials: AuthorSocials) => AuthorSocials;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.normalizeSocials = exports.AuthorSocialsSchema = void 0;
|
|
10
|
+
const utils_validation_1 = require("@docusaurus/utils-validation");
|
|
11
|
+
exports.AuthorSocialsSchema = utils_validation_1.Joi.object({
|
|
12
|
+
twitter: utils_validation_1.Joi.string(),
|
|
13
|
+
github: utils_validation_1.Joi.string(),
|
|
14
|
+
linkedin: utils_validation_1.Joi.string(),
|
|
15
|
+
// StackOverflow userIds like '82609' are parsed as numbers by Yaml
|
|
16
|
+
stackoverflow: utils_validation_1.Joi.alternatives()
|
|
17
|
+
.try(utils_validation_1.Joi.number(), utils_validation_1.Joi.string())
|
|
18
|
+
.custom((val) => String(val)),
|
|
19
|
+
x: utils_validation_1.Joi.string(),
|
|
20
|
+
bluesky: utils_validation_1.Joi.string(),
|
|
21
|
+
instagram: utils_validation_1.Joi.string(),
|
|
22
|
+
threads: utils_validation_1.Joi.string(),
|
|
23
|
+
mastodon: utils_validation_1.Joi.string(),
|
|
24
|
+
twitch: utils_validation_1.Joi.string(),
|
|
25
|
+
youtube: utils_validation_1.Joi.string(),
|
|
26
|
+
email: utils_validation_1.Joi.string(),
|
|
27
|
+
}).unknown();
|
|
28
|
+
const PredefinedPlatformNormalizers = {
|
|
29
|
+
x: (handle) => `https://x.com/${handle}`,
|
|
30
|
+
twitter: (handle) => `https://twitter.com/${handle}`,
|
|
31
|
+
github: (handle) => `https://github.com/${handle}`,
|
|
32
|
+
linkedin: (handle) => `https://www.linkedin.com/in/${handle}/`,
|
|
33
|
+
stackoverflow: (userId) => `https://stackoverflow.com/users/${userId}`,
|
|
34
|
+
bluesky: (handle) => `https://bsky.app/profile/${handle}`,
|
|
35
|
+
instagram: (handle) => `https://www.instagram.com/${handle}`,
|
|
36
|
+
threads: (handle) => `https://www.threads.net/@${handle}`,
|
|
37
|
+
mastodon: (handle) => `https://mastodon.social/@${handle}`, // can be in format user@other.server and it will redirect if needed
|
|
38
|
+
twitch: (handle) => `https://twitch.tv/${handle}`,
|
|
39
|
+
youtube: (handle) => `https://youtube.com/@${handle}`, // https://support.google.com/youtube/answer/6180214?hl=en
|
|
40
|
+
email: (email) => `mailto:${email}`,
|
|
41
|
+
};
|
|
42
|
+
function normalizeSocialEntry([platform, value]) {
|
|
43
|
+
if (typeof value !== 'string') {
|
|
44
|
+
throw new Error(`Author socials should be usernames/userIds/handles, or fully qualified HTTP(s) absolute URLs.
|
|
45
|
+
Social platform '${platform}' has illegal value '${value}'`);
|
|
46
|
+
}
|
|
47
|
+
const isAbsoluteUrl = value.startsWith('http://') ||
|
|
48
|
+
value.startsWith('https://') ||
|
|
49
|
+
value.startsWith('mailto:');
|
|
50
|
+
if (isAbsoluteUrl) {
|
|
51
|
+
return [platform, value];
|
|
52
|
+
}
|
|
53
|
+
else if (value.includes('/')) {
|
|
54
|
+
throw new Error(`Author socials should be usernames/userIds/handles, or fully qualified HTTP(s) absolute URLs.
|
|
55
|
+
Social platform '${platform}' has illegal value '${value}'`);
|
|
56
|
+
}
|
|
57
|
+
const normalizer = PredefinedPlatformNormalizers[platform.toLowerCase()];
|
|
58
|
+
if (normalizer && !isAbsoluteUrl) {
|
|
59
|
+
const normalizedPlatform = platform.toLowerCase();
|
|
60
|
+
const normalizedValue = normalizer(value);
|
|
61
|
+
return [normalizedPlatform, normalizedValue];
|
|
62
|
+
}
|
|
63
|
+
return [platform, value];
|
|
64
|
+
}
|
|
65
|
+
const normalizeSocials = (socials) => {
|
|
66
|
+
return Object.fromEntries(Object.entries(socials).map(normalizeSocialEntry));
|
|
67
|
+
};
|
|
68
|
+
exports.normalizeSocials = normalizeSocials;
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
import type { TagsFile } from '@docusaurus/utils';
|
|
8
|
+
import type { LoadContext, VcsConfig } from '@docusaurus/types';
|
|
9
|
+
import type { AuthorsMap, PluginOptions, BlogPost, BlogTags, BlogPaginated } from '@docusaurus/plugin-content-blog';
|
|
10
|
+
import type { BlogContentPaths } from './types';
|
|
11
|
+
export declare function truncate(fileString: string, truncateMarker: RegExp): string;
|
|
12
|
+
export declare function reportUntruncatedBlogPosts({ blogPosts, onUntruncatedBlogPosts, }: {
|
|
13
|
+
blogPosts: BlogPost[];
|
|
14
|
+
onUntruncatedBlogPosts: PluginOptions['onUntruncatedBlogPosts'];
|
|
15
|
+
}): void;
|
|
16
|
+
export declare function paginateBlogPosts({ blogPosts, basePageUrl, blogTitle, blogDescription, postsPerPageOption, pageBasePath, }: {
|
|
17
|
+
blogPosts: BlogPost[];
|
|
18
|
+
basePageUrl: string;
|
|
19
|
+
blogTitle: string;
|
|
20
|
+
blogDescription: string;
|
|
21
|
+
postsPerPageOption: number | 'ALL';
|
|
22
|
+
pageBasePath: string;
|
|
23
|
+
}): BlogPaginated[];
|
|
24
|
+
export declare function shouldBeListed(blogPost: BlogPost): boolean;
|
|
25
|
+
export declare function getBlogTags({ blogPosts, ...params }: {
|
|
26
|
+
blogPosts: BlogPost[];
|
|
27
|
+
blogTitle: string;
|
|
28
|
+
blogDescription: string;
|
|
29
|
+
postsPerPageOption: number | 'ALL';
|
|
30
|
+
pageBasePath: string;
|
|
31
|
+
}): BlogTags;
|
|
32
|
+
type ParsedBlogFileName = {
|
|
33
|
+
date: Date | undefined;
|
|
34
|
+
text: string;
|
|
35
|
+
slug: string;
|
|
36
|
+
};
|
|
37
|
+
export declare function parseBlogFileName(blogSourceRelative: string): ParsedBlogFileName;
|
|
38
|
+
export declare function parseFrontMatterDate(date: string | Date): Date;
|
|
39
|
+
export declare function getBlogPostDate({ frontMatterDate, filenameDate, filePath, vcs, }: {
|
|
40
|
+
frontMatterDate?: string | Date;
|
|
41
|
+
filenameDate?: Date;
|
|
42
|
+
filePath: string;
|
|
43
|
+
vcs: VcsConfig;
|
|
44
|
+
}): Promise<Date>;
|
|
45
|
+
export declare function generateBlogPosts(contentPaths: BlogContentPaths, context: LoadContext, options: PluginOptions, tagsFile: TagsFile | null, authorsMap?: AuthorsMap): Promise<BlogPost[]>;
|
|
46
|
+
export declare function applyProcessBlogPosts({ blogPosts, processBlogPosts, }: {
|
|
47
|
+
blogPosts: BlogPost[];
|
|
48
|
+
processBlogPosts: PluginOptions['processBlogPosts'];
|
|
49
|
+
}): Promise<BlogPost[]>;
|
|
50
|
+
export {};
|
package/lib/blogUtils.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.truncate = truncate;
|
|
10
|
+
exports.reportUntruncatedBlogPosts = reportUntruncatedBlogPosts;
|
|
11
|
+
exports.paginateBlogPosts = paginateBlogPosts;
|
|
12
|
+
exports.shouldBeListed = shouldBeListed;
|
|
13
|
+
exports.getBlogTags = getBlogTags;
|
|
14
|
+
exports.parseBlogFileName = parseBlogFileName;
|
|
15
|
+
exports.parseFrontMatterDate = parseFrontMatterDate;
|
|
16
|
+
exports.getBlogPostDate = getBlogPostDate;
|
|
17
|
+
exports.generateBlogPosts = generateBlogPosts;
|
|
18
|
+
exports.applyProcessBlogPosts = applyProcessBlogPosts;
|
|
19
|
+
const tslib_1 = require("tslib");
|
|
20
|
+
const node_path_1 = tslib_1.__importDefault(require("node:path"));
|
|
21
|
+
const lodash_1 = tslib_1.__importDefault(require("lodash"));
|
|
22
|
+
const logger_1 = tslib_1.__importDefault(require("@docusaurus/logger"));
|
|
23
|
+
const utils_1 = require("@docusaurus/utils");
|
|
24
|
+
const glob_1 = require("@docusaurus/glob");
|
|
25
|
+
const fs_1 = require("@docusaurus/fs");
|
|
26
|
+
const frontMatter_1 = require("./frontMatter");
|
|
27
|
+
const authors_1 = require("./authors");
|
|
28
|
+
const authorsProblems_1 = require("./authorsProblems");
|
|
29
|
+
const readingTime_1 = require("./readingTime");
|
|
30
|
+
function truncate(fileString, truncateMarker) {
|
|
31
|
+
return fileString.split(truncateMarker, 1).shift();
|
|
32
|
+
}
|
|
33
|
+
function reportUntruncatedBlogPosts({ blogPosts, onUntruncatedBlogPosts, }) {
|
|
34
|
+
const untruncatedBlogPosts = blogPosts.filter((p) => !p.metadata.hasTruncateMarker);
|
|
35
|
+
if (onUntruncatedBlogPosts !== 'ignore' && untruncatedBlogPosts.length > 0) {
|
|
36
|
+
const message = logger_1.default.interpolate `Docusaurus found blog posts without truncation markers:
|
|
37
|
+
- ${untruncatedBlogPosts
|
|
38
|
+
.map((p) => logger_1.default.path((0, utils_1.aliasedSitePathToRelativePath)(p.metadata.source)))
|
|
39
|
+
.join('\n- ')}
|
|
40
|
+
|
|
41
|
+
We recommend using truncation markers (code=${`<!-- truncate -->`} or code=${`{/* truncate */}`}) in blog posts to create shorter previews on blog paginated lists.
|
|
42
|
+
Tip: turn this security off with the code=${`onUntruncatedBlogPosts: 'ignore'`} blog plugin option.`;
|
|
43
|
+
logger_1.default.report(onUntruncatedBlogPosts)(message);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function paginateBlogPosts({ blogPosts, basePageUrl, blogTitle, blogDescription, postsPerPageOption, pageBasePath, }) {
|
|
47
|
+
const totalCount = blogPosts.length;
|
|
48
|
+
const postsPerPage = postsPerPageOption === 'ALL' ? totalCount : postsPerPageOption;
|
|
49
|
+
const numberOfPages = Math.max(1, Math.ceil(totalCount / postsPerPage));
|
|
50
|
+
const pages = [];
|
|
51
|
+
function permalink(page) {
|
|
52
|
+
return page > 0
|
|
53
|
+
? (0, utils_1.normalizeUrl)([basePageUrl, pageBasePath, `${page + 1}`])
|
|
54
|
+
: basePageUrl;
|
|
55
|
+
}
|
|
56
|
+
for (let page = 0; page < numberOfPages; page += 1) {
|
|
57
|
+
pages.push({
|
|
58
|
+
items: blogPosts
|
|
59
|
+
.slice(page * postsPerPage, (page + 1) * postsPerPage)
|
|
60
|
+
.map((item) => item.id),
|
|
61
|
+
metadata: {
|
|
62
|
+
permalink: permalink(page),
|
|
63
|
+
page: page + 1,
|
|
64
|
+
postsPerPage,
|
|
65
|
+
totalPages: numberOfPages,
|
|
66
|
+
totalCount,
|
|
67
|
+
previousPage: page !== 0 ? permalink(page - 1) : undefined,
|
|
68
|
+
nextPage: page < numberOfPages - 1 ? permalink(page + 1) : undefined,
|
|
69
|
+
blogDescription,
|
|
70
|
+
blogTitle,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return pages;
|
|
75
|
+
}
|
|
76
|
+
function shouldBeListed(blogPost) {
|
|
77
|
+
return !blogPost.metadata.unlisted;
|
|
78
|
+
}
|
|
79
|
+
function getBlogTags({ blogPosts, ...params }) {
|
|
80
|
+
const groups = (0, utils_1.groupTaggedItems)(blogPosts, (blogPost) => blogPost.metadata.tags);
|
|
81
|
+
return lodash_1.default.mapValues(groups, ({ tag, items: tagBlogPosts }) => {
|
|
82
|
+
const tagVisibility = (0, utils_1.getTagVisibility)({
|
|
83
|
+
items: tagBlogPosts,
|
|
84
|
+
isUnlisted: (item) => item.metadata.unlisted,
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
inline: tag.inline,
|
|
88
|
+
label: tag.label,
|
|
89
|
+
permalink: tag.permalink,
|
|
90
|
+
description: tag.description,
|
|
91
|
+
items: tagVisibility.listedItems.map((item) => item.id),
|
|
92
|
+
pages: paginateBlogPosts({
|
|
93
|
+
blogPosts: tagVisibility.listedItems,
|
|
94
|
+
basePageUrl: tag.permalink,
|
|
95
|
+
...params,
|
|
96
|
+
}),
|
|
97
|
+
unlisted: tagVisibility.unlisted,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const DATE_FILENAME_REGEX = /^(?<folder>.*)(?<date>\d{4}[-/]\d{1,2}[-/]\d{1,2})[-/]?(?<text>.*?)(?:\/index)?.mdx?$/;
|
|
102
|
+
function parseBlogFileName(blogSourceRelative) {
|
|
103
|
+
const dateFilenameMatch = blogSourceRelative.match(DATE_FILENAME_REGEX);
|
|
104
|
+
if (dateFilenameMatch) {
|
|
105
|
+
const { folder, text, date: dateString } = dateFilenameMatch.groups;
|
|
106
|
+
// Always treat dates as UTC by adding the `Z`
|
|
107
|
+
const date = new Date(`${dateString}Z`);
|
|
108
|
+
const slugDate = dateString.replace(/-/g, '/');
|
|
109
|
+
const slug = `/${slugDate}/${folder}${text}`;
|
|
110
|
+
return { date, text: text, slug };
|
|
111
|
+
}
|
|
112
|
+
const text = blogSourceRelative.replace(/(?:\/index)?\.mdx?$/, '');
|
|
113
|
+
const slug = `/${text}`;
|
|
114
|
+
return { date: undefined, text, slug };
|
|
115
|
+
}
|
|
116
|
+
function parseFrontMatterDate(date) {
|
|
117
|
+
if (typeof date === 'string') {
|
|
118
|
+
const dateString = date.trim();
|
|
119
|
+
// Detect a timezone suffix in the time portion, not the date's day/year.
|
|
120
|
+
// YAML timestamps allow tabs and short offsets; also accept ISO +/-HHMM.
|
|
121
|
+
// https://yaml.org/type/timestamp.html
|
|
122
|
+
// This detects a timezone, not date validity (already checked by Joi).
|
|
123
|
+
const hasTimeZone = /[t \t]\d.*(?:z|[+-]\d{1,2}(?::?\d{2})?)$/i.test(dateString);
|
|
124
|
+
// Treat dates without an explicit timezone as UTC.
|
|
125
|
+
return new Date(hasTimeZone ? dateString : `${dateString}Z`);
|
|
126
|
+
}
|
|
127
|
+
// Custom front matter parsers can still return Date objects.
|
|
128
|
+
return date;
|
|
129
|
+
}
|
|
130
|
+
async function getBlogPostDate({ frontMatterDate, filenameDate, filePath, vcs, }) {
|
|
131
|
+
// Prefer front matter, then the filename, then the file creation date.
|
|
132
|
+
if (frontMatterDate) {
|
|
133
|
+
return parseFrontMatterDate(frontMatterDate);
|
|
134
|
+
}
|
|
135
|
+
else if (filenameDate) {
|
|
136
|
+
return filenameDate;
|
|
137
|
+
}
|
|
138
|
+
const result = await vcs.getFileCreationInfo(filePath);
|
|
139
|
+
if (result == null) {
|
|
140
|
+
return (await fs_1.fs.stat(filePath)).birthtime;
|
|
141
|
+
}
|
|
142
|
+
return new Date(result.timestamp);
|
|
143
|
+
}
|
|
144
|
+
async function parseBlogPostMarkdownFile({ filePath, parseFrontMatter, }) {
|
|
145
|
+
const fileContent = await fs_1.fs.readFile(filePath, 'utf-8');
|
|
146
|
+
try {
|
|
147
|
+
const result = await (0, utils_1.parseMarkdownFile)({
|
|
148
|
+
filePath,
|
|
149
|
+
fileContent,
|
|
150
|
+
parseFrontMatter,
|
|
151
|
+
removeContentTitle: true,
|
|
152
|
+
});
|
|
153
|
+
return {
|
|
154
|
+
...result,
|
|
155
|
+
frontMatter: (0, frontMatter_1.validateBlogPostFrontMatter)(result.frontMatter),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
logger_1.default.error `Error while parsing blog post file path=${filePath}.`;
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const defaultReadingTime = ({ content, locale, options }) => (0, readingTime_1.calculateReadingTime)(content, locale, options);
|
|
164
|
+
async function processBlogSourceFile(blogSourceRelative, contentPaths, context, options, tagsFile, authorsMap) {
|
|
165
|
+
const { siteConfig: { baseUrl, markdown: { parseFrontMatter }, vcs, }, siteDir, i18n, } = context;
|
|
166
|
+
const { routeBasePath, tagsBasePath: tagsRouteBasePath, truncateMarker, showReadingTime, editUrl, } = options;
|
|
167
|
+
// Lookup in localized folder in priority
|
|
168
|
+
const blogDirPath = await (0, utils_1.getFolderContainingFile)((0, utils_1.getContentPathList)(contentPaths), blogSourceRelative);
|
|
169
|
+
const blogSourceAbsolute = node_path_1.default.join(blogDirPath, blogSourceRelative);
|
|
170
|
+
const { frontMatter, content, contentTitle, excerpt } = await parseBlogPostMarkdownFile({
|
|
171
|
+
filePath: blogSourceAbsolute,
|
|
172
|
+
parseFrontMatter,
|
|
173
|
+
});
|
|
174
|
+
const aliasedSource = (0, utils_1.aliasedSitePath)(blogSourceAbsolute, siteDir);
|
|
175
|
+
const lastUpdate = await (0, utils_1.readLastUpdateData)(blogSourceAbsolute, options, frontMatter.last_update, vcs);
|
|
176
|
+
const draft = (0, utils_1.isDraft)({ frontMatter });
|
|
177
|
+
const unlisted = (0, utils_1.isUnlisted)({ frontMatter });
|
|
178
|
+
if (draft) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
if (frontMatter.id) {
|
|
182
|
+
logger_1.default.warn `name=${'id'} header option is deprecated in path=${blogSourceRelative} file. Please use name=${'slug'} option instead.`;
|
|
183
|
+
}
|
|
184
|
+
const parsedBlogFileName = parseBlogFileName(blogSourceRelative);
|
|
185
|
+
const date = await getBlogPostDate({
|
|
186
|
+
frontMatterDate: frontMatter.date,
|
|
187
|
+
filenameDate: parsedBlogFileName.date,
|
|
188
|
+
filePath: blogSourceAbsolute,
|
|
189
|
+
vcs,
|
|
190
|
+
});
|
|
191
|
+
const title = frontMatter.title ?? contentTitle ?? parsedBlogFileName.text;
|
|
192
|
+
const description = frontMatter.description ?? excerpt ?? '';
|
|
193
|
+
const slug = frontMatter.slug ?? parsedBlogFileName.slug;
|
|
194
|
+
const permalink = (0, utils_1.normalizeUrl)([baseUrl, routeBasePath, slug]);
|
|
195
|
+
function getBlogEditUrl() {
|
|
196
|
+
const blogPathRelative = node_path_1.default.relative(blogDirPath, node_path_1.default.resolve(blogSourceAbsolute));
|
|
197
|
+
if (typeof editUrl === 'function') {
|
|
198
|
+
return editUrl({
|
|
199
|
+
blogDirPath: (0, utils_1.posixPath)(node_path_1.default.relative(siteDir, blogDirPath)),
|
|
200
|
+
blogPath: (0, utils_1.posixPath)(blogPathRelative),
|
|
201
|
+
permalink,
|
|
202
|
+
locale: i18n.currentLocale,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
else if (typeof editUrl === 'string') {
|
|
206
|
+
const isLocalized = blogDirPath === contentPaths.contentPathLocalized;
|
|
207
|
+
const fileContentPath = isLocalized &&
|
|
208
|
+
options.editLocalizedFiles &&
|
|
209
|
+
contentPaths.contentPathLocalized
|
|
210
|
+
? contentPaths.contentPathLocalized
|
|
211
|
+
: contentPaths.contentPath;
|
|
212
|
+
const contentPathEditUrl = (0, utils_1.normalizeUrl)([
|
|
213
|
+
editUrl,
|
|
214
|
+
(0, utils_1.posixPath)(node_path_1.default.relative(siteDir, fileContentPath)),
|
|
215
|
+
]);
|
|
216
|
+
return (0, utils_1.getEditUrl)(blogPathRelative, contentPathEditUrl);
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
const tagsBaseRoutePath = (0, utils_1.normalizeUrl)([
|
|
221
|
+
baseUrl,
|
|
222
|
+
routeBasePath,
|
|
223
|
+
tagsRouteBasePath,
|
|
224
|
+
]);
|
|
225
|
+
const authors = (0, authors_1.getBlogPostAuthors)({ authorsMap, frontMatter, baseUrl });
|
|
226
|
+
(0, authorsProblems_1.reportAuthorsProblems)({
|
|
227
|
+
authors,
|
|
228
|
+
blogSourceRelative,
|
|
229
|
+
options,
|
|
230
|
+
});
|
|
231
|
+
const tags = (0, utils_1.normalizeTags)({
|
|
232
|
+
options,
|
|
233
|
+
source: blogSourceRelative,
|
|
234
|
+
frontMatterTags: frontMatter.tags,
|
|
235
|
+
tagsBaseRoutePath,
|
|
236
|
+
tagsFile,
|
|
237
|
+
});
|
|
238
|
+
return {
|
|
239
|
+
id: slug,
|
|
240
|
+
metadata: {
|
|
241
|
+
permalink,
|
|
242
|
+
editUrl: getBlogEditUrl(),
|
|
243
|
+
source: aliasedSource,
|
|
244
|
+
title,
|
|
245
|
+
description,
|
|
246
|
+
date,
|
|
247
|
+
tags,
|
|
248
|
+
readingTime: showReadingTime
|
|
249
|
+
? options.readingTime({
|
|
250
|
+
content,
|
|
251
|
+
frontMatter,
|
|
252
|
+
defaultReadingTime,
|
|
253
|
+
locale: i18n.currentLocale,
|
|
254
|
+
})
|
|
255
|
+
: undefined,
|
|
256
|
+
hasTruncateMarker: truncateMarker.test(content),
|
|
257
|
+
authors,
|
|
258
|
+
frontMatter,
|
|
259
|
+
unlisted,
|
|
260
|
+
lastUpdatedAt: lastUpdate.lastUpdatedAt,
|
|
261
|
+
lastUpdatedBy: lastUpdate.lastUpdatedBy,
|
|
262
|
+
},
|
|
263
|
+
content,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
async function generateBlogPosts(contentPaths, context, options, tagsFile, authorsMap) {
|
|
267
|
+
const { include, exclude } = options;
|
|
268
|
+
if (!(await (0, fs_1.pathExists)(contentPaths.contentPath))) {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
const blogSourceFiles = await (0, glob_1.glob)(include, {
|
|
272
|
+
cwd: contentPaths.contentPath,
|
|
273
|
+
ignore: exclude,
|
|
274
|
+
});
|
|
275
|
+
async function doProcessBlogSourceFile(blogSourceFile) {
|
|
276
|
+
try {
|
|
277
|
+
return await processBlogSourceFile(blogSourceFile, contentPaths, context, options, tagsFile, authorsMap);
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
throw new Error(`Processing of blog source file path=${blogSourceFile} failed.`, { cause: err });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const blogPosts = (await Promise.all(blogSourceFiles.map(doProcessBlogSourceFile))).filter(Boolean);
|
|
284
|
+
blogPosts.sort((a, b) => b.metadata.date.getTime() - a.metadata.date.getTime());
|
|
285
|
+
if (options.sortPosts === 'ascending') {
|
|
286
|
+
return blogPosts.reverse();
|
|
287
|
+
}
|
|
288
|
+
return blogPosts;
|
|
289
|
+
}
|
|
290
|
+
async function applyProcessBlogPosts({ blogPosts, processBlogPosts, }) {
|
|
291
|
+
const processedBlogPosts = await processBlogPosts({ blogPosts });
|
|
292
|
+
if (Array.isArray(processedBlogPosts)) {
|
|
293
|
+
return processedBlogPosts;
|
|
294
|
+
}
|
|
295
|
+
return blogPosts;
|
|
296
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
import { type ReactNode } from 'react';
|
|
8
|
+
import type { PropBlogPostContent, BlogMetadata } from '@docusaurus/plugin-content-blog';
|
|
9
|
+
export declare function useBlogMetadata(): BlogMetadata;
|
|
10
|
+
/**
|
|
11
|
+
* The React context value returned by the `useBlogPost()` hook.
|
|
12
|
+
* It contains useful data related to the currently browsed blog post.
|
|
13
|
+
*/
|
|
14
|
+
export type BlogPostContextValue = Pick<PropBlogPostContent, 'metadata' | 'frontMatter' | 'assets' | 'toc'> & {
|
|
15
|
+
readonly isBlogPostPage: boolean;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* This is a very thin layer around the `content` received from the MDX loader.
|
|
19
|
+
* It provides metadata about the blog post to the children tree.
|
|
20
|
+
*/
|
|
21
|
+
export declare function BlogPostProvider({ children, content, isBlogPostPage, }: {
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
content: PropBlogPostContent;
|
|
24
|
+
isBlogPostPage?: boolean;
|
|
25
|
+
}): ReactNode;
|
|
26
|
+
/**
|
|
27
|
+
* Returns the data of the currently browsed blog post. Gives access to
|
|
28
|
+
* front matter, metadata, TOC, etc.
|
|
29
|
+
* When swizzling a low-level component (e.g. the "Edit this page" link)
|
|
30
|
+
* and you need some extra metadata, you don't have to drill the props
|
|
31
|
+
* all the way through the component tree: simply use this hook instead.
|
|
32
|
+
*/
|
|
33
|
+
export declare function useBlogPost(): BlogPostContextValue;
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
import React, { useMemo, useContext } from 'react';
|
|
8
|
+
import { ReactContextError } from '@docusaurus/theme-common';
|
|
9
|
+
import useRouteContext from '@docusaurus/useRouteContext';
|
|
10
|
+
export function useBlogMetadata() {
|
|
11
|
+
const routeContext = useRouteContext();
|
|
12
|
+
const blogMetadata = routeContext?.data?.blogMetadata;
|
|
13
|
+
if (!blogMetadata) {
|
|
14
|
+
throw new Error("useBlogMetadata() can't be called on the current route because the blog metadata could not be found in route context");
|
|
15
|
+
}
|
|
16
|
+
return blogMetadata;
|
|
17
|
+
}
|
|
18
|
+
const Context = React.createContext(null);
|
|
19
|
+
/**
|
|
20
|
+
* Note: we don't use `PropBlogPostContent` as context value on purpose.
|
|
21
|
+
* Metadata is currently stored inside the MDX component, but we may want to
|
|
22
|
+
* change that in the future.
|
|
23
|
+
*/
|
|
24
|
+
function useContextValue({ content, isBlogPostPage, }) {
|
|
25
|
+
return useMemo(() => ({
|
|
26
|
+
metadata: content.metadata,
|
|
27
|
+
frontMatter: content.frontMatter,
|
|
28
|
+
assets: content.assets,
|
|
29
|
+
toc: content.toc,
|
|
30
|
+
isBlogPostPage,
|
|
31
|
+
}), [content, isBlogPostPage]);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* This is a very thin layer around the `content` received from the MDX loader.
|
|
35
|
+
* It provides metadata about the blog post to the children tree.
|
|
36
|
+
*/
|
|
37
|
+
export function BlogPostProvider({ children, content, isBlogPostPage = false, }) {
|
|
38
|
+
const contextValue = useContextValue({ content, isBlogPostPage });
|
|
39
|
+
return <Context value={contextValue}>{children}</Context>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Returns the data of the currently browsed blog post. Gives access to
|
|
43
|
+
* front matter, metadata, TOC, etc.
|
|
44
|
+
* When swizzling a low-level component (e.g. the "Edit this page" link)
|
|
45
|
+
* and you need some extra metadata, you don't have to drill the props
|
|
46
|
+
* all the way through the component tree: simply use this hook instead.
|
|
47
|
+
*/
|
|
48
|
+
export function useBlogPost() {
|
|
49
|
+
const blogPost = useContext(Context);
|
|
50
|
+
if (blogPost === null) {
|
|
51
|
+
throw new ReactContextError('BlogPostProvider');
|
|
52
|
+
}
|
|
53
|
+
return blogPost;
|
|
54
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
export { BlogPostProvider, type BlogPostContextValue, useBlogPost, useBlogMetadata, } from './contexts';
|
|
8
|
+
export { useBlogListPageStructuredData, useBlogPostStructuredData, } from './structuredDataUtils';
|
|
9
|
+
export { BlogSidebarItemList, groupBlogSidebarItemsByYear, useVisibleBlogSidebarItems, } from './sidebarUtils';
|
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
export { BlogPostProvider, useBlogPost, useBlogMetadata, } from './contexts';
|
|
8
|
+
export { useBlogListPageStructuredData, useBlogPostStructuredData, } from './structuredDataUtils';
|
|
9
|
+
export { BlogSidebarItemList, groupBlogSidebarItemsByYear, useVisibleBlogSidebarItems, } from './sidebarUtils';
|
|
@@ -0,0 +1,21 @@
|
|
|
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
|
+
import { type ReactNode } from 'react';
|
|
8
|
+
import type { BlogSidebarItem } from '@docusaurus/plugin-content-blog';
|
|
9
|
+
/**
|
|
10
|
+
* Return the visible blog sidebar items to display.
|
|
11
|
+
* Unlisted items are filtered.
|
|
12
|
+
*/
|
|
13
|
+
export declare function useVisibleBlogSidebarItems(items: BlogSidebarItem[]): BlogSidebarItem[];
|
|
14
|
+
export declare function groupBlogSidebarItemsByYear(items: BlogSidebarItem[]): [string, BlogSidebarItem[]][];
|
|
15
|
+
export declare function BlogSidebarItemList({ items, ulClassName, liClassName, linkClassName, linkActiveClassName, }: {
|
|
16
|
+
items: BlogSidebarItem[];
|
|
17
|
+
ulClassName?: string;
|
|
18
|
+
liClassName?: string;
|
|
19
|
+
linkClassName?: string;
|
|
20
|
+
linkActiveClassName?: string;
|
|
21
|
+
}): ReactNode;
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
import { useMemo } from 'react';
|
|
8
|
+
import { useLocation } from '@docusaurus/router';
|
|
9
|
+
import Link from '@docusaurus/Link';
|
|
10
|
+
import { groupBy } from '@docusaurus/theme-common';
|
|
11
|
+
import { isSamePath } from '@docusaurus/theme-common/internal';
|
|
12
|
+
function isVisible(item, pathname) {
|
|
13
|
+
if (item.unlisted && !isSamePath(item.permalink, pathname)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Return the visible blog sidebar items to display.
|
|
20
|
+
* Unlisted items are filtered.
|
|
21
|
+
*/
|
|
22
|
+
export function useVisibleBlogSidebarItems(items) {
|
|
23
|
+
const { pathname } = useLocation();
|
|
24
|
+
return useMemo(() => items.filter((item) => isVisible(item, pathname)), [items, pathname]);
|
|
25
|
+
}
|
|
26
|
+
export function groupBlogSidebarItemsByYear(items) {
|
|
27
|
+
const groupedByYear = groupBy(items, (item) => {
|
|
28
|
+
return `${new Date(item.date).getFullYear()}`;
|
|
29
|
+
});
|
|
30
|
+
// "as" is safe here
|
|
31
|
+
// see https://github.com/microsoft/TypeScript/pull/56805#issuecomment-2196526425
|
|
32
|
+
const entries = Object.entries(groupedByYear);
|
|
33
|
+
// We have to use entries because of https://x.com/sebastienlorber/status/1806371668614369486
|
|
34
|
+
// Objects with string/number keys are automatically sorted asc...
|
|
35
|
+
// Even if keys are strings like "2024"
|
|
36
|
+
// We want descending order for years
|
|
37
|
+
// Alternative: using Map.groupBy (not affected by this "reordering")
|
|
38
|
+
entries.reverse();
|
|
39
|
+
return entries;
|
|
40
|
+
}
|
|
41
|
+
export function BlogSidebarItemList({ items, ulClassName, liClassName, linkClassName, linkActiveClassName, }) {
|
|
42
|
+
return (<ul className={ulClassName}>
|
|
43
|
+
{items.map((item) => (<li key={item.permalink} className={liClassName}>
|
|
44
|
+
<Link isNavLink to={item.permalink} className={linkClassName} activeClassName={linkActiveClassName}>
|
|
45
|
+
{item.title}
|
|
46
|
+
</Link>
|
|
47
|
+
</li>))}
|
|
48
|
+
</ul>);
|
|
49
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
import type { Props as BlogListPageStructuredDataProps } from '@theme/BlogListPage/StructuredData';
|
|
8
|
+
import type { Blog, BlogPosting, WithContext } from 'schema-dts';
|
|
9
|
+
export declare function useBlogListPageStructuredData(props: BlogListPageStructuredDataProps): WithContext<Blog>;
|
|
10
|
+
export declare function useBlogPostStructuredData(): WithContext<BlogPosting>;
|