refactor(v2): merge linkify function used in blog and docs and align properties (#4402)

* refactor(v2): merge linkify function used in blog and docs

* refactor(v2): rename docsDirPath and docsDirPathLocalized ad update types

* refactor(v2): rename blogPostsBySource and update types

* improve replaceMarkdownLinks api

Co-authored-by: slorber <lorber.sebastien@gmail.com>
This commit is contained in:
Armano 2021-03-12 15:11:08 +01:00 committed by GitHub
parent bfe52cdae3
commit 2f53b1a895
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 240 additions and 237 deletions

View file

@ -7,7 +7,7 @@
import fs from 'fs-extra'; import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import {linkify, LinkifyParams, getPostsBySource} from '../blogUtils'; import {linkify, LinkifyParams, getSourceToPermalink} from '../blogUtils';
import {BlogBrokenMarkdownLink, BlogContentPaths, BlogPost} from '../types'; import {BlogBrokenMarkdownLink, BlogContentPaths, BlogPost} from '../types';
const siteDir = path.join(__dirname, '__fixtures__', 'website'); const siteDir = path.join(__dirname, '__fixtures__', 'website');
@ -43,10 +43,10 @@ const transform = (filePath: string, options?: Partial<LinkifyParams>) => {
const fileContent = fs.readFileSync(filePath, 'utf-8'); const fileContent = fs.readFileSync(filePath, 'utf-8');
const transformedContent = linkify({ const transformedContent = linkify({
filePath, filePath,
fileContent, fileString: fileContent,
siteDir, siteDir,
contentPaths, contentPaths,
blogPostsBySource: getPostsBySource(blogPosts), sourceToPermalink: getSourceToPermalink(blogPosts),
onBrokenMarkdownLink: (brokenMarkdownLink) => { onBrokenMarkdownLink: (brokenMarkdownLink) => {
throw new Error( throw new Error(
`Broken markdown link found: ${JSON.stringify(brokenMarkdownLink)}`, `Broken markdown link found: ${JSON.stringify(brokenMarkdownLink)}`,
@ -82,12 +82,12 @@ test('report broken markdown links', () => {
expect(onBrokenMarkdownLink).toHaveBeenCalledTimes(2); expect(onBrokenMarkdownLink).toHaveBeenCalledTimes(2);
expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(1, { expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(1, {
filePath: path.resolve(folderPath, filePath), filePath: path.resolve(folderPath, filePath),
folderPath, contentPaths,
link: 'postNotExist1.md', link: 'postNotExist1.md',
} as BlogBrokenMarkdownLink); } as BlogBrokenMarkdownLink);
expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(2, { expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(2, {
filePath: path.resolve(folderPath, filePath), filePath: path.resolve(folderPath, filePath),
folderPath, contentPaths,
link: './postNotExist2.mdx', link: './postNotExist2.mdx',
} as BlogBrokenMarkdownLink); } as BlogBrokenMarkdownLink);
}); });

View file

@ -9,16 +9,14 @@ import fs from 'fs-extra';
import globby from 'globby'; import globby from 'globby';
import chalk from 'chalk'; import chalk from 'chalk';
import path from 'path'; import path from 'path';
import {resolve} from 'url';
import readingTime from 'reading-time'; import readingTime from 'reading-time';
import {Feed} from 'feed'; import {Feed} from 'feed';
import {keyBy} from 'lodash'; import {keyBy, mapValues} from 'lodash';
import { import {
PluginOptions, PluginOptions,
BlogPost, BlogPost,
DateLink, DateLink,
BlogContentPaths, BlogContentPaths,
BlogBrokenMarkdownLink,
BlogMarkdownLoaderOptions, BlogMarkdownLoaderOptions,
} from './types'; } from './types';
import { import {
@ -31,15 +29,19 @@ import {
getDateTimeFormat, getDateTimeFormat,
} from '@docusaurus/utils'; } from '@docusaurus/utils';
import {LoadContext} from '@docusaurus/types'; import {LoadContext} from '@docusaurus/types';
import {replaceMarkdownLinks} from '@docusaurus/utils/lib/markdownLinks';
export function truncate(fileString: string, truncateMarker: RegExp): string { export function truncate(fileString: string, truncateMarker: RegExp): string {
return fileString.split(truncateMarker, 1).shift()!; return fileString.split(truncateMarker, 1).shift()!;
} }
export function getPostsBySource( export function getSourceToPermalink(
blogPosts: BlogPost[], blogPosts: BlogPost[],
): Record<string, BlogPost> { ): Record<string, string> {
return keyBy(blogPosts, (item) => item.metadata.source); return mapValues(
keyBy(blogPosts, (item) => item.metadata.source),
(v) => v.metadata.permalink,
);
} }
// YYYY-MM-DD-{name}.mdx? // YYYY-MM-DD-{name}.mdx?
@ -250,73 +252,31 @@ export async function generateBlogPosts(
export type LinkifyParams = { export type LinkifyParams = {
filePath: string; filePath: string;
fileContent: string; fileString: string;
} & Pick< } & Pick<
BlogMarkdownLoaderOptions, BlogMarkdownLoaderOptions,
'blogPostsBySource' | 'siteDir' | 'contentPaths' | 'onBrokenMarkdownLink' 'sourceToPermalink' | 'siteDir' | 'contentPaths' | 'onBrokenMarkdownLink'
>; >;
export function linkify({ export function linkify({
filePath, filePath,
contentPaths, contentPaths,
fileContent, fileString,
siteDir, siteDir,
blogPostsBySource, sourceToPermalink,
onBrokenMarkdownLink, onBrokenMarkdownLink,
}: LinkifyParams): string { }: LinkifyParams): string {
// TODO temporary, should consider the file being in localized folder! const {newContent, brokenMarkdownLinks} = replaceMarkdownLinks({
const folderPath = contentPaths.contentPath; siteDir,
fileString,
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 aliasedSource = (source: string) =>
aliasedSitePath(source, siteDir);
const blogPost: BlogPost | undefined =
blogPostsBySource[aliasedSource(resolve(filePath, mdLink))] ||
blogPostsBySource[
aliasedSource(`${contentPaths.contentPathLocalized}/${mdLink}`)
] ||
blogPostsBySource[
aliasedSource(`${contentPaths.contentPath}/${mdLink}`)
];
if (blogPost) {
modifiedLine = modifiedLine.replace(
mdLink,
blogPost.metadata.permalink,
);
} else {
const brokenMarkdownLink: BlogBrokenMarkdownLink = {
folderPath,
filePath, filePath,
link: mdLink, contentPaths,
}; sourceToPermalink,
onBrokenMarkdownLink(brokenMarkdownLink);
}
mdMatch = mdRegex.exec(modifiedLine);
}
return modifiedLine;
}); });
return lines.join('\n'); brokenMarkdownLinks.forEach((l) => onBrokenMarkdownLink(l));
return newContent;
} }
// Order matters: we look in priority in localized folder // Order matters: we look in priority in localized folder

View file

@ -50,7 +50,7 @@ import {
generateBlogFeed, generateBlogFeed,
generateBlogPosts, generateBlogPosts,
getContentPathList, getContentPathList,
getPostsBySource, getSourceToPermalink,
} from './blogUtils'; } from './blogUtils';
export default function pluginContentBlog( export default function pluginContentBlog(
@ -416,7 +416,7 @@ export default function pluginContentBlog(
siteDir, siteDir,
contentPaths, contentPaths,
truncateMarker, truncateMarker,
blogPostsBySource: getPostsBySource(blogPosts), sourceToPermalink: getSourceToPermalink(blogPosts),
onBrokenMarkdownLink: (brokenMarkdownLink) => { onBrokenMarkdownLink: (brokenMarkdownLink) => {
if (onBrokenMarkdownLinks === 'ignore') { if (onBrokenMarkdownLinks === 'ignore') {
return; return;

View file

@ -12,13 +12,13 @@ import {BlogMarkdownLoaderOptions} from './types';
const markdownLoader: loader.Loader = function (source) { const markdownLoader: loader.Loader = function (source) {
const filePath = this.resourcePath; const filePath = this.resourcePath;
const fileContent = source as string; const fileString = source as string;
const callback = this.async(); const callback = this.async();
const markdownLoaderOptions = getOptions(this) as BlogMarkdownLoaderOptions; const markdownLoaderOptions = getOptions(this) as BlogMarkdownLoaderOptions;
// Linkify blog posts // Linkify blog posts
let finalContent = linkify({ let finalContent = linkify({
fileContent, fileString,
filePath, filePath,
...markdownLoaderOptions, ...markdownLoaderOptions,
}); });

View file

@ -5,10 +5,12 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
export type BlogContentPaths = { import {
contentPath: string; BrokenMarkdownLink,
contentPathLocalized: string; ContentPaths,
}; } from '@docusaurus/utils/lib/markdownLinks';
export type BlogContentPaths = ContentPaths;
export interface BlogContent { export interface BlogContent {
blogPosts: BlogPost[]; blogPosts: BlogPost[];
@ -142,15 +144,11 @@ export interface TagModule {
permalink: string; permalink: string;
} }
export type BlogBrokenMarkdownLink = { export type BlogBrokenMarkdownLink = BrokenMarkdownLink<BlogContentPaths>;
folderPath: string;
filePath: string;
link: string;
};
export type BlogMarkdownLoaderOptions = { export type BlogMarkdownLoaderOptions = {
siteDir: string; siteDir: string;
contentPaths: BlogContentPaths; contentPaths: BlogContentPaths;
truncateMarker: RegExp; truncateMarker: RegExp;
blogPostsBySource: Record<string, BlogPost>; sourceToPermalink: Record<string, string>;
onBrokenMarkdownLink: (brokenMarkdownLink: BlogBrokenMarkdownLink) => void; onBrokenMarkdownLink: (brokenMarkdownLink: BlogBrokenMarkdownLink) => void;
}; };

View file

@ -60,6 +60,8 @@ exports[`translateLoadedContent should return translated loaded content matching
Object { Object {
"loadedVersions": Array [ "loadedVersions": Array [
Object { Object {
"contentPath": "any",
"contentPathLocalized": "any",
"docs": Array [ "docs": Array [
Object { Object {
"description": "doc1 description", "description": "doc1 description",
@ -147,8 +149,6 @@ Object {
"version": "any", "version": "any",
}, },
], ],
"docsDirPath": "any",
"docsDirPathLocalized": "any",
"isLast": true, "isLast": true,
"mainDocId": "", "mainDocId": "",
"permalinkToSidebar": Object {}, "permalinkToSidebar": Object {},
@ -201,6 +201,8 @@ Object {
"versionPath": "/docs/", "versionPath": "/docs/",
}, },
Object { Object {
"contentPath": "any",
"contentPathLocalized": "any",
"docs": Array [ "docs": Array [
Object { Object {
"description": "doc1 description", "description": "doc1 description",
@ -288,8 +290,6 @@ Object {
"version": "any", "version": "any",
}, },
], ],
"docsDirPath": "any",
"docsDirPathLocalized": "any",
"isLast": true, "isLast": true,
"mainDocId": "", "mainDocId": "",
"permalinkToSidebar": Object {}, "permalinkToSidebar": Object {},
@ -342,6 +342,8 @@ Object {
"versionPath": "/docs/", "versionPath": "/docs/",
}, },
Object { Object {
"contentPath": "any",
"contentPathLocalized": "any",
"docs": Array [ "docs": Array [
Object { Object {
"description": "doc1 description", "description": "doc1 description",
@ -429,8 +431,6 @@ Object {
"version": "any", "version": "any",
}, },
], ],
"docsDirPath": "any",
"docsDirPathLocalized": "any",
"isLast": true, "isLast": true,
"mainDocId": "", "mainDocId": "",
"permalinkToSidebar": Object {}, "permalinkToSidebar": Object {},

View file

@ -45,7 +45,7 @@ ${markdown}
source, source,
content, content,
lastUpdate: {}, lastUpdate: {},
docsDirPath: 'docs', contentPath: 'docs',
filePath: source, filePath: source,
}; };
}; };
@ -93,7 +93,7 @@ function createTestUtils({
editUrl: undefined, editUrl: undefined,
source: path.posix.join( source: path.posix.join(
'@site', '@site',
posixPath(path.relative(siteDir, versionMetadata.docsDirPath)), posixPath(path.relative(siteDir, versionMetadata.contentPath)),
posixPath(docFileSource), posixPath(docFileSource),
), ),
...expectedMetadata, ...expectedMetadata,

View file

@ -254,7 +254,7 @@ describe('simple website', () => {
sidebar: 'docs', sidebar: 'docs',
source: path.posix.join( source: path.posix.join(
'@site', '@site',
posixPath(path.relative(siteDir, currentVersion.docsDirPath)), posixPath(path.relative(siteDir, currentVersion.contentPath)),
'hello.md', 'hello.md',
), ),
title: 'Hello, World !', title: 'Hello, World !',
@ -276,7 +276,7 @@ describe('simple website', () => {
sidebar: 'docs', sidebar: 'docs',
source: path.posix.join( source: path.posix.join(
'@site', '@site',
posixPath(path.relative(siteDir, currentVersion.docsDirPath)), posixPath(path.relative(siteDir, currentVersion.contentPath)),
'foo', 'foo',
'bar.md', 'bar.md',
), ),
@ -424,7 +424,7 @@ describe('versioned website', () => {
slug: '/foo/barSlug', slug: '/foo/barSlug',
source: path.posix.join( source: path.posix.join(
'@site', '@site',
posixPath(path.relative(siteDir, currentVersion.docsDirPath)), posixPath(path.relative(siteDir, currentVersion.contentPath)),
'foo', 'foo',
'bar.md', 'bar.md',
), ),
@ -446,7 +446,7 @@ describe('versioned website', () => {
slug: '/', slug: '/',
source: path.posix.join( source: path.posix.join(
'@site', '@site',
posixPath(path.relative(siteDir, currentVersion.docsDirPath)), posixPath(path.relative(siteDir, currentVersion.contentPath)),
'hello.md', 'hello.md',
), ),
title: 'hello', title: 'hello',
@ -467,7 +467,7 @@ describe('versioned website', () => {
slug: '/', slug: '/',
source: path.posix.join( source: path.posix.join(
'@site', '@site',
posixPath(path.relative(siteDir, version101.docsDirPath)), posixPath(path.relative(siteDir, version101.contentPath)),
'hello.md', 'hello.md',
), ),
title: 'hello', title: 'hello',
@ -488,7 +488,7 @@ describe('versioned website', () => {
slug: '/foo/baz', slug: '/foo/baz',
source: path.posix.join( source: path.posix.join(
'@site', '@site',
posixPath(path.relative(siteDir, version100.docsDirPath)), posixPath(path.relative(siteDir, version100.contentPath)),
'foo', 'foo',
'baz.md', 'baz.md',
), ),
@ -649,7 +649,7 @@ describe('versioned website (community)', () => {
slug: '/team', slug: '/team',
source: path.posix.join( source: path.posix.join(
'@site', '@site',
posixPath(path.relative(siteDir, version100.docsDirPath)), posixPath(path.relative(siteDir, version100.contentPath)),
'team.md', 'team.md',
), ),
title: 'team', title: 'team',

View file

@ -44,8 +44,8 @@ function createSampleVersion(
routePriority: undefined, routePriority: undefined,
sidebarFilePath: 'any', sidebarFilePath: 'any',
isLast: true, isLast: true,
docsDirPath: 'any', contentPath: 'any',
docsDirPathLocalized: 'any', contentPathLocalized: 'any',
docs: [ docs: [
createSampleDoc({ createSampleDoc({
id: 'doc1', id: 'doc1',

View file

@ -68,8 +68,8 @@ describe('simple site', () => {
}; };
const vCurrent: VersionMetadata = { const vCurrent: VersionMetadata = {
docsDirPath: path.join(simpleSiteDir, 'docs'), contentPath: path.join(simpleSiteDir, 'docs'),
docsDirPathLocalized: path.join( contentPathLocalized: path.join(
simpleSiteDir, simpleSiteDir,
'i18n/en/docusaurus-plugin-content-docs/current', 'i18n/en/docusaurus-plugin-content-docs/current',
), ),
@ -218,8 +218,8 @@ describe('versioned site, pluginId=default', () => {
}; };
const vCurrent: VersionMetadata = { const vCurrent: VersionMetadata = {
docsDirPath: path.join(versionedSiteDir, 'docs'), contentPath: path.join(versionedSiteDir, 'docs'),
docsDirPathLocalized: path.join( contentPathLocalized: path.join(
versionedSiteDir, versionedSiteDir,
'i18n/en/docusaurus-plugin-content-docs/current', 'i18n/en/docusaurus-plugin-content-docs/current',
), ),
@ -232,8 +232,8 @@ describe('versioned site, pluginId=default', () => {
}; };
const v101: VersionMetadata = { const v101: VersionMetadata = {
docsDirPath: path.join(versionedSiteDir, 'versioned_docs/version-1.0.1'), contentPath: path.join(versionedSiteDir, 'versioned_docs/version-1.0.1'),
docsDirPathLocalized: path.join( contentPathLocalized: path.join(
versionedSiteDir, versionedSiteDir,
'i18n/en/docusaurus-plugin-content-docs/version-1.0.1', 'i18n/en/docusaurus-plugin-content-docs/version-1.0.1',
), ),
@ -249,8 +249,8 @@ describe('versioned site, pluginId=default', () => {
}; };
const v100: VersionMetadata = { const v100: VersionMetadata = {
docsDirPath: path.join(versionedSiteDir, 'versioned_docs/version-1.0.0'), contentPath: path.join(versionedSiteDir, 'versioned_docs/version-1.0.0'),
docsDirPathLocalized: path.join( contentPathLocalized: path.join(
versionedSiteDir, versionedSiteDir,
'i18n/en/docusaurus-plugin-content-docs/version-1.0.0', 'i18n/en/docusaurus-plugin-content-docs/version-1.0.0',
), ),
@ -266,11 +266,11 @@ describe('versioned site, pluginId=default', () => {
}; };
const vwithSlugs: VersionMetadata = { const vwithSlugs: VersionMetadata = {
docsDirPath: path.join( contentPath: path.join(
versionedSiteDir, versionedSiteDir,
'versioned_docs/version-withSlugs', 'versioned_docs/version-withSlugs',
), ),
docsDirPathLocalized: path.join( contentPathLocalized: path.join(
versionedSiteDir, versionedSiteDir,
'i18n/en/docusaurus-plugin-content-docs/version-withSlugs', 'i18n/en/docusaurus-plugin-content-docs/version-withSlugs',
), ),
@ -615,8 +615,8 @@ describe('versioned site, pluginId=community', () => {
}; };
const vCurrent: VersionMetadata = { const vCurrent: VersionMetadata = {
docsDirPath: path.join(versionedSiteDir, 'community'), contentPath: path.join(versionedSiteDir, 'community'),
docsDirPathLocalized: path.join( contentPathLocalized: path.join(
versionedSiteDir, versionedSiteDir,
'i18n/en/docusaurus-plugin-content-docs-community/current', 'i18n/en/docusaurus-plugin-content-docs-community/current',
), ),
@ -629,11 +629,11 @@ describe('versioned site, pluginId=community', () => {
}; };
const v100: VersionMetadata = { const v100: VersionMetadata = {
docsDirPath: path.join( contentPath: path.join(
versionedSiteDir, versionedSiteDir,
'community_versioned_docs/version-1.0.0', 'community_versioned_docs/version-1.0.0',
), ),
docsDirPathLocalized: path.join( contentPathLocalized: path.join(
versionedSiteDir, versionedSiteDir,
'i18n/en/docusaurus-plugin-content-docs-community/version-1.0.0', 'i18n/en/docusaurus-plugin-content-docs-community/version-1.0.0',
), ),

View file

@ -67,23 +67,23 @@ async function readLastUpdateData(
export async function readDocFile( export async function readDocFile(
versionMetadata: Pick< versionMetadata: Pick<
VersionMetadata, VersionMetadata,
'docsDirPath' | 'docsDirPathLocalized' 'contentPath' | 'contentPathLocalized'
>, >,
source: string, source: string,
options: LastUpdateOptions, options: LastUpdateOptions,
): Promise<DocFile> { ): Promise<DocFile> {
const docsDirPath = await getFolderContainingFile( const contentPath = await getFolderContainingFile(
getDocsDirPaths(versionMetadata), getDocsDirPaths(versionMetadata),
source, source,
); );
const filePath = path.join(docsDirPath, source); const filePath = path.join(contentPath, source);
const [content, lastUpdate] = await Promise.all([ const [content, lastUpdate] = await Promise.all([
fs.readFile(filePath, 'utf-8'), fs.readFile(filePath, 'utf-8'),
readLastUpdateData(filePath, options), readLastUpdateData(filePath, options),
]); ]);
return {source, content, lastUpdate, docsDirPath, filePath}; return {source, content, lastUpdate, contentPath, filePath};
} }
export async function readVersionDocs( export async function readVersionDocs(
@ -94,7 +94,7 @@ export async function readVersionDocs(
>, >,
): Promise<DocFile[]> { ): Promise<DocFile[]> {
const sources = await globby(options.include, { const sources = await globby(options.include, {
cwd: versionMetadata.docsDirPath, cwd: versionMetadata.contentPath,
}); });
return Promise.all( return Promise.all(
sources.map((source) => readDocFile(versionMetadata, source, options)), sources.map((source) => readDocFile(versionMetadata, source, options)),
@ -112,7 +112,7 @@ export function processDocMetadata({
context: LoadContext; context: LoadContext;
options: MetadataOptions; options: MetadataOptions;
}): DocMetadataBase { }): DocMetadataBase {
const {source, content, lastUpdate, docsDirPath, filePath} = docFile; const {source, content, lastUpdate, contentPath, filePath} = docFile;
const {homePageId} = options; const {homePageId} = options;
const {siteDir, i18n} = context; const {siteDir, i18n} = context;
@ -170,20 +170,20 @@ export function processDocMetadata({
const permalink = normalizeUrl([versionMetadata.versionPath, docSlug]); const permalink = normalizeUrl([versionMetadata.versionPath, docSlug]);
function getDocEditUrl() { function getDocEditUrl() {
const relativeFilePath = path.relative(docsDirPath, filePath); const relativeFilePath = path.relative(contentPath, filePath);
if (typeof options.editUrl === 'function') { if (typeof options.editUrl === 'function') {
return options.editUrl({ return options.editUrl({
version: versionMetadata.versionName, version: versionMetadata.versionName,
versionDocsDirPath: posixPath( versionDocsDirPath: posixPath(
path.relative(siteDir, versionMetadata.docsDirPath), path.relative(siteDir, versionMetadata.contentPath),
), ),
docPath: posixPath(relativeFilePath), docPath: posixPath(relativeFilePath),
permalink, permalink,
locale: context.i18n.currentLocale, locale: context.i18n.currentLocale,
}); });
} else if (typeof options.editUrl === 'string') { } else if (typeof options.editUrl === 'string') {
const isLocalized = docsDirPath === versionMetadata.docsDirPathLocalized; const isLocalized = contentPath === versionMetadata.contentPathLocalized;
const baseVersionEditUrl = const baseVersionEditUrl =
isLocalized && options.editLocalizedFiles isLocalized && options.editLocalizedFiles
? versionMetadata.versionEditUrlLocalized ? versionMetadata.versionEditUrlLocalized

View file

@ -145,7 +145,7 @@ export default function pluginContentDocs(
versionMetadata.versionName versionMetadata.versionName
} has no docs! At least one doc should exist at path=[${path.relative( } has no docs! At least one doc should exist at path=[${path.relative(
siteDir, siteDir,
versionMetadata.docsDirPath, versionMetadata.contentPath,
)}]`, )}]`,
); );
} }
@ -337,7 +337,7 @@ export default function pluginContentDocs(
return; return;
} }
reportMessage( reportMessage(
`Docs markdown link couldn't be resolved: (${brokenMarkdownLink.link}) in ${brokenMarkdownLink.filePath} for version ${brokenMarkdownLink.version.versionName}`, `Docs markdown link couldn't be resolved: (${brokenMarkdownLink.link}) in ${brokenMarkdownLink.filePath} for version ${brokenMarkdownLink.contentPaths.versionName}`,
siteConfig.onBrokenMarkdownLinks, siteConfig.onBrokenMarkdownLinks,
); );
}, },

View file

@ -18,19 +18,19 @@ import {VERSIONED_DOCS_DIR, CURRENT_VERSION_NAME} from '../../constants';
function createFakeVersion({ function createFakeVersion({
versionName, versionName,
docsDirPath, contentPath,
docsDirPathLocalized, contentPathLocalized,
}: { }: {
versionName: string; versionName: string;
docsDirPath: string; contentPath: string;
docsDirPathLocalized: string; contentPathLocalized: string;
}): VersionMetadata { }): VersionMetadata {
return { return {
versionName, versionName,
versionLabel: 'Any', versionLabel: 'Any',
versionPath: 'any', versionPath: 'any',
docsDirPath, contentPath,
docsDirPathLocalized, contentPathLocalized,
sidebarFilePath: 'any', sidebarFilePath: 'any',
routePriority: undefined, routePriority: undefined,
isLast: false, isLast: false,
@ -41,8 +41,8 @@ const siteDir = path.join(__dirname, '__fixtures__');
const versionCurrent = createFakeVersion({ const versionCurrent = createFakeVersion({
versionName: CURRENT_VERSION_NAME, versionName: CURRENT_VERSION_NAME,
docsDirPath: path.join(siteDir, 'docs'), contentPath: path.join(siteDir, 'docs'),
docsDirPathLocalized: path.join( contentPathLocalized: path.join(
siteDir, siteDir,
'i18n', 'i18n',
'fr', 'fr',
@ -53,8 +53,8 @@ const versionCurrent = createFakeVersion({
const version100 = createFakeVersion({ const version100 = createFakeVersion({
versionName: '1.0.0', versionName: '1.0.0',
docsDirPath: path.join(siteDir, VERSIONED_DOCS_DIR, 'version-1.0.0'), contentPath: path.join(siteDir, VERSIONED_DOCS_DIR, 'version-1.0.0'),
docsDirPathLocalized: path.join( contentPathLocalized: path.join(
siteDir, siteDir,
'i18n', 'i18n',
'fr', 'fr',
@ -97,14 +97,14 @@ const transform = (filepath: string, options?: Partial<DocsMarkdownOption>) => {
}; };
test('transform nothing', () => { test('transform nothing', () => {
const doc1 = path.join(versionCurrent.docsDirPath, 'doc1.md'); const doc1 = path.join(versionCurrent.contentPath, 'doc1.md');
const [content, transformedContent] = transform(doc1); const [content, transformedContent] = transform(doc1);
expect(transformedContent).toMatchSnapshot(); expect(transformedContent).toMatchSnapshot();
expect(content).toEqual(transformedContent); expect(content).toEqual(transformedContent);
}); });
test('transform to correct links', () => { test('transform to correct links', () => {
const doc2 = path.join(versionCurrent.docsDirPath, 'doc2.md'); const doc2 = path.join(versionCurrent.contentPath, 'doc2.md');
const [content, transformedContent] = transform(doc2); const [content, transformedContent] = transform(doc2);
expect(transformedContent).toMatchSnapshot(); expect(transformedContent).toMatchSnapshot();
expect(transformedContent).toContain('](/docs/doc1'); expect(transformedContent).toContain('](/docs/doc1');
@ -119,7 +119,7 @@ test('transform to correct links', () => {
}); });
test('transform relative links', () => { test('transform relative links', () => {
const doc3 = path.join(versionCurrent.docsDirPath, 'subdir', 'doc3.md'); const doc3 = path.join(versionCurrent.contentPath, 'subdir', 'doc3.md');
const [content, transformedContent] = transform(doc3); const [content, transformedContent] = transform(doc3);
expect(transformedContent).toMatchSnapshot(); expect(transformedContent).toMatchSnapshot();
@ -129,7 +129,7 @@ test('transform relative links', () => {
}); });
test('transforms reference links', () => { test('transforms reference links', () => {
const doc4 = path.join(versionCurrent.docsDirPath, 'doc4.md'); const doc4 = path.join(versionCurrent.contentPath, 'doc4.md');
const [content, transformedContent] = transform(doc4); const [content, transformedContent] = transform(doc4);
expect(transformedContent).toMatchSnapshot(); expect(transformedContent).toMatchSnapshot();
expect(transformedContent).toContain('[doc1]: /docs/doc1'); expect(transformedContent).toContain('[doc1]: /docs/doc1');
@ -140,7 +140,7 @@ test('transforms reference links', () => {
}); });
test('report broken markdown links', () => { test('report broken markdown links', () => {
const doc5 = path.join(versionCurrent.docsDirPath, 'doc5.md'); const doc5 = path.join(versionCurrent.contentPath, 'doc5.md');
const onBrokenMarkdownLink = jest.fn(); const onBrokenMarkdownLink = jest.fn();
const [content, transformedContent] = transform(doc5, { const [content, transformedContent] = transform(doc5, {
onBrokenMarkdownLink, onBrokenMarkdownLink,
@ -150,27 +150,27 @@ test('report broken markdown links', () => {
expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(1, { expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(1, {
filePath: doc5, filePath: doc5,
link: 'docNotExist1.md', link: 'docNotExist1.md',
version: versionCurrent, contentPaths: versionCurrent,
} as BrokenMarkdownLink); } as BrokenMarkdownLink);
expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(2, { expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(2, {
filePath: doc5, filePath: doc5,
link: './docNotExist2.mdx', link: './docNotExist2.mdx',
version: versionCurrent, contentPaths: versionCurrent,
} as BrokenMarkdownLink); } as BrokenMarkdownLink);
expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(3, { expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(3, {
filePath: doc5, filePath: doc5,
link: '../docNotExist3.mdx', link: '../docNotExist3.mdx',
version: versionCurrent, contentPaths: versionCurrent,
} as BrokenMarkdownLink); } as BrokenMarkdownLink);
expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(4, { expect(onBrokenMarkdownLink).toHaveBeenNthCalledWith(4, {
filePath: doc5, filePath: doc5,
link: './subdir/docNotExist4.md', link: './subdir/docNotExist4.md',
version: versionCurrent, contentPaths: versionCurrent,
} as BrokenMarkdownLink); } as BrokenMarkdownLink);
}); });
test('transforms absolute links in versioned docs', () => { test('transforms absolute links in versioned docs', () => {
const doc2 = path.join(version100.docsDirPath, 'doc2.md'); const doc2 = path.join(version100.contentPath, 'doc2.md');
const [content, transformedContent] = transform(doc2); const [content, transformedContent] = transform(doc2);
expect(transformedContent).toMatchSnapshot(); expect(transformedContent).toMatchSnapshot();
expect(transformedContent).toContain('](/docs/1.0.0/subdir/doc1'); expect(transformedContent).toContain('](/docs/1.0.0/subdir/doc1');
@ -181,7 +181,7 @@ test('transforms absolute links in versioned docs', () => {
}); });
test('transforms relative links in versioned docs', () => { test('transforms relative links in versioned docs', () => {
const doc1 = path.join(version100.docsDirPath, 'subdir', 'doc1.md'); const doc1 = path.join(version100.contentPath, 'subdir', 'doc1.md');
const [content, transformedContent] = transform(doc1); const [content, transformedContent] = transform(doc1);
expect(transformedContent).toMatchSnapshot(); expect(transformedContent).toMatchSnapshot();
expect(transformedContent).toContain('](/docs/1.0.0/doc2'); expect(transformedContent).toContain('](/docs/1.0.0/doc2');

View file

@ -5,14 +5,9 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import {resolve} from 'url'; import {DocsMarkdownOption} from '../types';
import {
DocsMarkdownOption,
VersionMetadata,
BrokenMarkdownLink,
} from '../types';
import {getDocsDirPaths} from '../versions'; import {getDocsDirPaths} from '../versions';
import {aliasedSitePath} from '@docusaurus/utils'; import {replaceMarkdownLinks} from '@docusaurus/utils/lib/markdownLinks';
function getVersion(filePath: string, options: DocsMarkdownOption) { function getVersion(filePath: string, options: DocsMarkdownOption) {
const versionFound = options.versionsMetadata.find((version) => const versionFound = options.versionsMetadata.find((version) =>
@ -28,66 +23,22 @@ function getVersion(filePath: string, options: DocsMarkdownOption) {
return versionFound; return versionFound;
} }
function replaceMarkdownLinks(
fileString: string,
filePath: string,
version: VersionMetadata,
options: DocsMarkdownOption,
) {
const {siteDir, sourceToPermalink, onBrokenMarkdownLink} = options;
const {docsDirPath, docsDirPathLocalized} = version;
// Replace internal markdown linking (except in fenced blocks).
let fencedBlock = false;
const lines = fileString.split('\n').map((line) => {
if (line.trim().startsWith('```')) {
fencedBlock = !fencedBlock;
}
if (fencedBlock) {
return line;
}
let modifiedLine = line;
// Replace inline-style links or reference-style links e.g:
// This is [Document 1](doc1.md) -> we replace this doc1.md with correct link
// [doc1]: doc1.md -> we replace this doc1.md with correct link
const mdRegex = /(?:(?:\]\()|(?:\]:\s?))(?!https)([^'")\]\s>]+\.mdx?)/g;
let mdMatch = mdRegex.exec(modifiedLine);
while (mdMatch !== null) {
// Replace it to correct html link.
const mdLink = mdMatch[1];
const aliasedSource = (source: string) =>
aliasedSitePath(source, siteDir);
const permalink =
sourceToPermalink[aliasedSource(resolve(filePath, mdLink))] ||
sourceToPermalink[aliasedSource(`${docsDirPathLocalized}/${mdLink}`)] ||
sourceToPermalink[aliasedSource(`${docsDirPath}/${mdLink}`)];
if (permalink) {
modifiedLine = modifiedLine.replace(mdLink, permalink);
} else {
const brokenMarkdownLink: BrokenMarkdownLink = {
version,
filePath,
link: mdLink,
};
onBrokenMarkdownLink(brokenMarkdownLink);
}
mdMatch = mdRegex.exec(modifiedLine);
}
return modifiedLine;
});
return lines.join('\n');
}
export function linkify( export function linkify(
fileString: string, fileString: string,
filePath: string, filePath: string,
options: DocsMarkdownOption, options: DocsMarkdownOption,
): string { ): string {
const version = getVersion(filePath, options); const {siteDir, sourceToPermalink, onBrokenMarkdownLink} = options;
return replaceMarkdownLinks(fileString, filePath, version, options);
const {newContent, brokenMarkdownLinks} = replaceMarkdownLinks({
siteDir,
fileString,
filePath,
contentPaths: getVersion(filePath, options),
sourceToPermalink,
});
brokenMarkdownLinks.forEach((l) => onBrokenMarkdownLink(l));
return newContent;
} }

View file

@ -8,8 +8,13 @@
// eslint-disable-next-line spaced-comment // eslint-disable-next-line spaced-comment
/// <reference types="@docusaurus/module-type-aliases" /> /// <reference types="@docusaurus/module-type-aliases" />
import {
BrokenMarkdownLink as IBrokenMarkdownLink,
ContentPaths,
} from '@docusaurus/utils/lib/markdownLinks';
export type DocFile = { export type DocFile = {
docsDirPath: string; // /!\ may be localized contentPath: string; // /!\ may be localized
filePath: string; // /!\ may be localized filePath: string; // /!\ may be localized
source: string; source: string;
content: string; content: string;
@ -18,15 +23,15 @@ export type DocFile = {
export type VersionName = string; export type VersionName = string;
export type VersionMetadata = { export type VersionMetadata = ContentPaths & {
versionName: VersionName; // 1.0.0 versionName: VersionName; // 1.0.0
versionLabel: string; // Version 1.0.0 versionLabel: string; // Version 1.0.0
versionPath: string; // /baseUrl/docs/1.0.0 versionPath: string; // /baseUrl/docs/1.0.0
versionEditUrl?: string | undefined; versionEditUrl?: string | undefined;
versionEditUrlLocalized?: string | undefined; versionEditUrlLocalized?: string | undefined;
isLast: boolean; isLast: boolean;
docsDirPath: string; // "versioned_docs/version-1.0.0" // contentPath: string; // "versioned_docs/version-1.0.0"
docsDirPathLocalized: string; // "i18n/fr/version-1.0.0/default" // contentPathLocalized: string; // "i18n/fr/version-1.0.0/default"
sidebarFilePath: string; // versioned_sidebars/1.0.0.json sidebarFilePath: string; // versioned_sidebars/1.0.0.json
routePriority: number | undefined; // -1 for the latest docs routePriority: number | undefined; // -1 for the latest docs
}; };
@ -192,11 +197,7 @@ export type GlobalPluginData = {
versions: GlobalVersion[]; versions: GlobalVersion[];
}; };
export type BrokenMarkdownLink = { export type BrokenMarkdownLink = IBrokenMarkdownLink<VersionMetadata>;
filePath: string;
version: VersionMetadata;
link: string;
};
export type DocsMarkdownOption = { export type DocsMarkdownOption = {
versionsMetadata: VersionMetadata[]; versionsMetadata: VersionMetadata[];

View file

@ -165,18 +165,18 @@ function getVersionMetadataPaths({
options: Pick<PluginOptions, 'id' | 'path' | 'sidebarPath'>; options: Pick<PluginOptions, 'id' | 'path' | 'sidebarPath'>;
}): Pick< }): Pick<
VersionMetadata, VersionMetadata,
'docsDirPath' | 'docsDirPathLocalized' | 'sidebarFilePath' 'contentPath' | 'contentPathLocalized' | 'sidebarFilePath'
> { > {
const isCurrentVersion = versionName === CURRENT_VERSION_NAME; const isCurrentVersion = versionName === CURRENT_VERSION_NAME;
const docsDirPath = isCurrentVersion const contentPath = isCurrentVersion
? path.resolve(context.siteDir, options.path) ? path.resolve(context.siteDir, options.path)
: path.join( : path.join(
getVersionedDocsDirPath(context.siteDir, options.id), getVersionedDocsDirPath(context.siteDir, options.id),
`version-${versionName}`, `version-${versionName}`,
); );
const docsDirPathLocalized = getDocsDirPathLocalized({ const contentPathLocalized = getDocsDirPathLocalized({
siteDir: context.siteDir, siteDir: context.siteDir,
locale: context.i18n.currentLocale, locale: context.i18n.currentLocale,
pluginId: options.id, pluginId: options.id,
@ -190,17 +190,17 @@ function getVersionMetadataPaths({
`version-${versionName}-sidebars.json`, `version-${versionName}-sidebars.json`,
); );
return {docsDirPath, docsDirPathLocalized, sidebarFilePath}; return {contentPath, contentPathLocalized, sidebarFilePath};
} }
function getVersionEditUrls({ function getVersionEditUrls({
docsDirPath, contentPath,
docsDirPathLocalized, contentPathLocalized,
context: {siteDir, i18n}, context: {siteDir, i18n},
options: {id, path: currentVersionPath, editUrl, editCurrentVersion}, options: {id, path: currentVersionPath, editUrl, editCurrentVersion},
}: { }: {
docsDirPath: string; contentPath: string;
docsDirPathLocalized: string; contentPathLocalized: string;
context: Pick<LoadContext, 'siteDir' | 'i18n'>; context: Pick<LoadContext, 'siteDir' | 'i18n'>;
options: Pick< options: Pick<
PluginOptions, PluginOptions,
@ -217,7 +217,7 @@ function getVersionEditUrls({
return undefined; return undefined;
} }
const editDirPath = editCurrentVersion ? currentVersionPath : docsDirPath; const editDirPath = editCurrentVersion ? currentVersionPath : contentPath;
const editDirPathLocalized = editCurrentVersion const editDirPathLocalized = editCurrentVersion
? getDocsDirPathLocalized({ ? getDocsDirPathLocalized({
siteDir, siteDir,
@ -225,7 +225,7 @@ function getVersionEditUrls({
versionName: CURRENT_VERSION_NAME, versionName: CURRENT_VERSION_NAME,
pluginId: id, pluginId: id,
}) })
: docsDirPathLocalized; : contentPathLocalized;
const versionPathSegment = posixPath( const versionPathSegment = posixPath(
path.relative(siteDir, path.resolve(siteDir, editDirPath)), path.relative(siteDir, path.resolve(siteDir, editDirPath)),
@ -269,8 +269,8 @@ function createVersionMetadata({
}): VersionMetadata { }): VersionMetadata {
const { const {
sidebarFilePath, sidebarFilePath,
docsDirPath, contentPath,
docsDirPathLocalized, contentPathLocalized,
} = getVersionMetadataPaths({ } = getVersionMetadataPaths({
versionName, versionName,
context, context,
@ -298,8 +298,8 @@ function createVersionMetadata({
]); ]);
const versionEditUrls = getVersionEditUrls({ const versionEditUrls = getVersionEditUrls({
docsDirPath, contentPath,
docsDirPathLocalized, contentPathLocalized,
context, context,
options, options,
}); });
@ -316,8 +316,8 @@ function createVersionMetadata({
isLast, isLast,
routePriority, routePriority,
sidebarFilePath, sidebarFilePath,
docsDirPath, contentPath,
docsDirPathLocalized, contentPathLocalized,
}; };
} }
@ -328,14 +328,14 @@ function checkVersionMetadataPaths({
versionMetadata: VersionMetadata; versionMetadata: VersionMetadata;
context: Pick<LoadContext, 'siteDir'>; context: Pick<LoadContext, 'siteDir'>;
}) { }) {
const {versionName, docsDirPath, sidebarFilePath} = versionMetadata; const {versionName, contentPath, sidebarFilePath} = versionMetadata;
const {siteDir} = context; const {siteDir} = context;
if (!fs.existsSync(docsDirPath)) { if (!fs.existsSync(contentPath)) {
throw new Error( throw new Error(
`The docs folder does not exist for version [${versionName}]. A docs folder is expected to be found at ${path.relative( `The docs folder does not exist for version [${versionName}]. A docs folder is expected to be found at ${path.relative(
siteDir, siteDir,
docsDirPath, contentPath,
)}`, )}`,
); );
} }
@ -483,8 +483,8 @@ export function readVersionsMetadata({
export function getDocsDirPaths( export function getDocsDirPaths(
versionMetadata: Pick< versionMetadata: Pick<
VersionMetadata, VersionMetadata,
'docsDirPath' | 'docsDirPathLocalized' 'contentPath' | 'contentPathLocalized'
>, >,
): [string, string] { ): [string, string] {
return [versionMetadata.docsDirPathLocalized, versionMetadata.docsDirPath]; return [versionMetadata.contentPathLocalized, versionMetadata.contentPath];
} }

View file

@ -0,0 +1,93 @@
/**
* 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 {resolve} from 'url';
import {aliasedSitePath} from './index';
export type ContentPaths = {
contentPath: string;
contentPathLocalized: string;
};
export type BrokenMarkdownLink<T extends ContentPaths> = {
filePath: string;
contentPaths: T;
link: string;
};
export type ReplaceMarkdownLinksParams<T extends ContentPaths> = {
siteDir: string;
fileString: string;
filePath: string;
contentPaths: T;
sourceToPermalink: Record<string, string>;
};
export type ReplaceMarkdownLinksReturn<T extends ContentPaths> = {
newContent: string;
brokenMarkdownLinks: BrokenMarkdownLink<T>[];
};
export function replaceMarkdownLinks<T extends ContentPaths>({
siteDir,
fileString,
filePath,
contentPaths,
sourceToPermalink,
}: ReplaceMarkdownLinksParams<T>): ReplaceMarkdownLinksReturn<T> {
const {contentPath, contentPathLocalized} = contentPaths;
const brokenMarkdownLinks: BrokenMarkdownLink<T>[] = [];
// Replace internal markdown linking (except in fenced blocks).
let fencedBlock = false;
const lines = fileString.split('\n').map((line) => {
if (line.trim().startsWith('```')) {
fencedBlock = !fencedBlock;
}
if (fencedBlock) {
return line;
}
let modifiedLine = line;
// Replace inline-style links or reference-style links e.g:
// This is [Document 1](doc1.md) -> we replace this doc1.md with correct link
// [doc1]: doc1.md -> we replace this doc1.md with correct link
const mdRegex = /(?:(?:\]\()|(?:\]:\s?))(?!https)([^'")\]\s>]+\.mdx?)/g;
let mdMatch = mdRegex.exec(modifiedLine);
while (mdMatch !== null) {
// Replace it to correct html link.
const mdLink = mdMatch[1];
const aliasedSource = (source: string) =>
aliasedSitePath(source, siteDir);
const permalink: string | undefined =
sourceToPermalink[aliasedSource(resolve(filePath, mdLink))] ||
sourceToPermalink[aliasedSource(`${contentPathLocalized}/${mdLink}`)] ||
sourceToPermalink[aliasedSource(`${contentPath}/${mdLink}`)];
if (permalink) {
modifiedLine = modifiedLine.replace(mdLink, permalink);
} else {
const brokenMarkdownLink: BrokenMarkdownLink<T> = {
contentPaths,
filePath,
link: mdLink,
};
brokenMarkdownLinks.push(brokenMarkdownLink);
}
mdMatch = mdRegex.exec(modifiedLine);
}
return modifiedLine;
});
const newContent = lines.join('\n');
return {newContent, brokenMarkdownLinks};
}