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