docusaurus/packages/docusaurus-plugin-content-blog/src/blogUtils.ts
Alexey Pyltsyn 36163773ec
fix(v2): linkify blog posts (#2326)
* fix(v2): linkify blog posts

* Fix tests
2020-02-29 14:49:00 +08:00

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');
}