mirror of
https://github.com/facebook/docusaurus.git
synced 2025-07-30 15:00:09 +02:00
203 lines
5.3 KiB
TypeScript
203 lines
5.3 KiB
TypeScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
import fs from 'fs-extra';
|
|
import globby from 'globby';
|
|
import path from 'path';
|
|
import {Feed} from 'feed';
|
|
import {PluginOptions, BlogPost, DateLink} from './types';
|
|
import {parse, normalizeUrl, aliasedSitePath} from '@docusaurus/utils';
|
|
import {LoadContext} from '@docusaurus/types';
|
|
|
|
export function truncate(fileString: string, truncateMarker: RegExp) {
|
|
return fileString.split(truncateMarker, 1).shift()!;
|
|
}
|
|
|
|
// YYYY-MM-DD-{name}.mdx?
|
|
// Prefer named capture, but older Node versions do not support it.
|
|
const FILENAME_PATTERN = /^(\d{4}-\d{1,2}-\d{1,2})-?(.*?).mdx?$/;
|
|
|
|
function toUrl({date, link}: DateLink) {
|
|
return `${date
|
|
.toISOString()
|
|
.substring(0, '2019-01-01'.length)
|
|
.replace(/-/g, '/')}/${link}`;
|
|
}
|
|
|
|
export async function generateBlogFeed(
|
|
context: LoadContext,
|
|
options: PluginOptions,
|
|
) {
|
|
if (!options.feedOptions) {
|
|
throw new Error(
|
|
'Invalid options - `feedOptions` is not expected to be null.',
|
|
);
|
|
}
|
|
const {siteDir, siteConfig} = context;
|
|
const contentPath = path.resolve(siteDir, options.path);
|
|
const blogPosts = await generateBlogPosts(contentPath, context, options);
|
|
if (blogPosts == null) {
|
|
return null;
|
|
}
|
|
|
|
const {feedOptions, routeBasePath} = options;
|
|
const {url: siteUrl, title, favicon} = siteConfig;
|
|
const blogBaseUrl = normalizeUrl([siteUrl, routeBasePath]);
|
|
|
|
const updated =
|
|
(blogPosts[0] && blogPosts[0].metadata.date) ||
|
|
new Date('2015-10-25T16:29:00.000-07:00');
|
|
|
|
const feed = new Feed({
|
|
id: blogBaseUrl,
|
|
title: feedOptions.title || `${title} Blog`,
|
|
updated,
|
|
language: feedOptions.language,
|
|
link: blogBaseUrl,
|
|
description: feedOptions.description || `${siteConfig.title} Blog`,
|
|
favicon: normalizeUrl([siteUrl, favicon]),
|
|
copyright: feedOptions.copyright,
|
|
});
|
|
|
|
blogPosts.forEach(post => {
|
|
const {
|
|
id,
|
|
metadata: {title, permalink, date, description},
|
|
} = post;
|
|
feed.addItem({
|
|
title,
|
|
id: id,
|
|
link: normalizeUrl([siteUrl, permalink]),
|
|
date,
|
|
description,
|
|
});
|
|
});
|
|
|
|
return feed;
|
|
}
|
|
|
|
export async function generateBlogPosts(
|
|
blogDir: string,
|
|
{siteConfig, siteDir}: LoadContext,
|
|
options: PluginOptions,
|
|
) {
|
|
const {include, routeBasePath, truncateMarker} = options;
|
|
|
|
if (!fs.existsSync(blogDir)) {
|
|
return [];
|
|
}
|
|
|
|
const {baseUrl = ''} = siteConfig;
|
|
const blogFiles = await globby(include, {
|
|
cwd: blogDir,
|
|
});
|
|
|
|
const blogPosts: BlogPost[] = [];
|
|
|
|
await Promise.all(
|
|
blogFiles.map(async (relativeSource: string) => {
|
|
const source = path.join(blogDir, relativeSource);
|
|
const aliasedSource = aliasedSitePath(source, siteDir);
|
|
const blogFileName = path.basename(relativeSource);
|
|
|
|
const fileString = await fs.readFile(source, 'utf-8');
|
|
const {frontMatter, content, excerpt} = parse(fileString);
|
|
|
|
if (frontMatter.draft && process.env.NODE_ENV === 'production') {
|
|
return;
|
|
}
|
|
|
|
let date;
|
|
// Extract date and title from filename.
|
|
const match = blogFileName.match(FILENAME_PATTERN);
|
|
let linkName = blogFileName.replace(/\.mdx?$/, '');
|
|
|
|
if (match) {
|
|
const [, dateString, name] = match;
|
|
date = new Date(dateString);
|
|
linkName = name;
|
|
}
|
|
|
|
// Prefer user-defined date.
|
|
if (frontMatter.date) {
|
|
date = new Date(frontMatter.date);
|
|
}
|
|
|
|
// Use file create time for blog.
|
|
date = date || (await fs.stat(source)).birthtime;
|
|
frontMatter.title = frontMatter.title || linkName;
|
|
|
|
blogPosts.push({
|
|
id: frontMatter.id || frontMatter.title,
|
|
metadata: {
|
|
permalink: normalizeUrl([
|
|
baseUrl,
|
|
routeBasePath,
|
|
frontMatter.id || toUrl({date, link: linkName}),
|
|
]),
|
|
source: aliasedSource,
|
|
description: frontMatter.description || excerpt,
|
|
date,
|
|
tags: frontMatter.tags,
|
|
title: frontMatter.title,
|
|
truncated: truncateMarker?.test(content) || false,
|
|
},
|
|
});
|
|
}),
|
|
);
|
|
|
|
blogPosts.sort(
|
|
(a, b) => b.metadata.date.getTime() - a.metadata.date.getTime(),
|
|
);
|
|
|
|
return blogPosts;
|
|
}
|
|
|
|
export function linkify(
|
|
fileContent: string,
|
|
siteDir: string,
|
|
blogPath: string,
|
|
blogPosts: BlogPost[],
|
|
) {
|
|
let fencedBlock = false;
|
|
const lines = fileContent.split('\n').map(line => {
|
|
if (line.trim().startsWith('```')) {
|
|
fencedBlock = !fencedBlock;
|
|
}
|
|
|
|
if (fencedBlock) return line;
|
|
|
|
let modifiedLine = line;
|
|
const mdRegex = /(?:(?:\]\()|(?:\]:\s?))(?!https)([^'")\]\s>]+\.mdx?)/g;
|
|
let mdMatch = mdRegex.exec(modifiedLine);
|
|
|
|
while (mdMatch !== null) {
|
|
const mdLink = mdMatch[1];
|
|
const aliasedPostSource = `@site/${path.relative(
|
|
siteDir,
|
|
path.resolve(blogPath, mdLink),
|
|
)}`;
|
|
let blogPostPermalink = null;
|
|
|
|
blogPosts.forEach(blogPost => {
|
|
if (blogPost.metadata.source === aliasedPostSource) {
|
|
blogPostPermalink = blogPost.metadata.permalink;
|
|
}
|
|
});
|
|
|
|
if (blogPostPermalink) {
|
|
modifiedLine = modifiedLine.replace(mdLink, blogPostPermalink);
|
|
}
|
|
|
|
mdMatch = mdRegex.exec(modifiedLine);
|
|
}
|
|
|
|
return modifiedLine;
|
|
});
|
|
|
|
return lines.join('\n');
|
|
}
|