From 8dfe3d558eeeaab2a3306803f8c30116415175e2 Mon Sep 17 00:00:00 2001 From: JounQin Date: Tue, 17 Dec 2024 22:34:29 +0800 Subject: [PATCH] feat: add externalLinkPrefixes support close #1673 --- packages/core/src/node/mdx/options.ts | 1 + .../src/node/mdx/remarkPlugins/checkDeadLink.ts | 8 +++----- .../src/node/mdx/remarkPlugins/normalizeLink.ts | 13 ++++++++++--- packages/core/src/node/route/RouteService.ts | 7 +++++++ .../core/src/node/runtimeModule/siteData/index.ts | 2 ++ packages/shared/src/runtime-utils/index.ts | 8 +++----- packages/shared/src/types/index.ts | 5 +++++ .../theme-default/src/components/Link/index.tsx | 4 +++- 8 files changed, 34 insertions(+), 14 deletions(-) diff --git a/packages/core/src/node/mdx/options.ts b/packages/core/src/node/mdx/options.ts index 1b7444ade..ff0400a60 100644 --- a/packages/core/src/node/mdx/options.ts +++ b/packages/core/src/node/mdx/options.ts @@ -52,6 +52,7 @@ export async function createMDXOptions( remarkPluginNormalizeLink, { base: config?.base || '', + externalLinkPrefixes: config?.externalLinkPrefixes, cleanUrls, defaultLang, root: docDirectory, diff --git a/packages/core/src/node/mdx/remarkPlugins/checkDeadLink.ts b/packages/core/src/node/mdx/remarkPlugins/checkDeadLink.ts index 7897e8df5..ce5e7f585 100644 --- a/packages/core/src/node/mdx/remarkPlugins/checkDeadLink.ts +++ b/packages/core/src/node/mdx/remarkPlugins/checkDeadLink.ts @@ -2,7 +2,7 @@ import path from 'node:path'; import { visit } from 'unist-util-visit'; import type { Plugin } from 'unified'; import { logger } from '@rspress/shared/logger'; -import { cleanUrl, isProduction } from '@rspress/shared'; +import { cleanUrl, isExternalUrl, isProduction } from '@rspress/shared'; import type { RouteService } from '@/node/route/RouteService'; import { normalizePath } from '@/node/utils'; @@ -12,8 +12,6 @@ export interface DeadLinkCheckOptions { routeService: RouteService; } -const IGNORE_REGEXP = /^(https?|mailto|tel|#)/; - export function checkLinks( links: string[], filepath: string, @@ -22,7 +20,7 @@ export function checkLinks( ) { const errorInfos: string[] = []; links - .filter(link => !IGNORE_REGEXP.test(link)) + .filter(link => !isExternalUrl(link, routeService.externalLinkPrefixes)) .map(link => normalizePath(link)) .forEach(link => { const relativePath = path.relative(root, filepath); @@ -64,7 +62,7 @@ export const remarkCheckDeadLinks: Plugin< return; } - if (!url.startsWith('http') && !url.startsWith('https')) { + if (!isExternalUrl(url, routeService.externalLinkPrefixes)) { const { routePath: normalizeUrl } = routeService.normalizeRoutePath( // fix: windows path url.split(path.sep).join('/')?.split('#')[0], diff --git a/packages/core/src/node/mdx/remarkPlugins/normalizeLink.ts b/packages/core/src/node/mdx/remarkPlugins/normalizeLink.ts index a8f1c49c7..bcc42a91b 100644 --- a/packages/core/src/node/mdx/remarkPlugins/normalizeLink.ts +++ b/packages/core/src/node/mdx/remarkPlugins/normalizeLink.ts @@ -15,10 +15,17 @@ interface LinkNode { * Remark plugin to normalize a link href */ export const remarkPluginNormalizeLink: Plugin< - [{ base: string; root: string; cleanUrls: boolean }], + [ + { + base: string; + externalLinkPrefixes?: string[]; + root: string; + cleanUrls: boolean; + }, + ], Root > = - ({ base, root, cleanUrls }) => + ({ base, externalLinkPrefixes, root, cleanUrls }) => (tree, file) => { const images: MdxjsEsm[] = []; visit( @@ -36,7 +43,7 @@ export const remarkPluginNormalizeLink: Plugin< // eslint-disable-next-line prefer-const let { url, hash } = parseUrl(node.url); - if (isExternalUrl(url)) { + if (isExternalUrl(url, externalLinkPrefixes)) { node.url = url + (hash ? `#${hash}` : ''); return; } diff --git a/packages/core/src/node/route/RouteService.ts b/packages/core/src/node/route/RouteService.ts index 5483f5d92..2d899c096 100644 --- a/packages/core/src/node/route/RouteService.ts +++ b/packages/core/src/node/route/RouteService.ts @@ -110,6 +110,12 @@ export class RouteService { #pluginDriver: PluginDriver; + #externalLinkPrefixes?: string[]; + + get externalLinkPrefixes() { + return this.#externalLinkPrefixes; + } + constructor( scanDir: string, userConfig: UserConfig, @@ -128,6 +134,7 @@ export class RouteService { [] ).map(item => item.lang); this.#base = userConfig?.base || ''; + this.#externalLinkPrefixes = userConfig?.externalLinkPrefixes; this.#tempDir = tempDir; this.#pluginDriver = pluginDriver; diff --git a/packages/core/src/node/runtimeModule/siteData/index.ts b/packages/core/src/node/runtimeModule/siteData/index.ts index 78469e35a..074407b93 100644 --- a/packages/core/src/node/runtimeModule/siteData/index.ts +++ b/packages/core/src/node/runtimeModule/siteData/index.ts @@ -108,12 +108,14 @@ export async function siteDataVMPlugin(context: FactoryContext) { ); const siteData: SiteData = { + root: userDocRoot, title: userConfig?.title || '', description: userConfig?.description || '', icon: userConfig?.icon || '', route: userConfig?.route, themeConfig: normalizeThemeConfig(userConfig, pages), base: userConfig?.base || '/', + externalLinkPrefixes: userConfig?.externalLinkPrefixes || [], lang: userConfig?.lang || '', locales: userConfig?.locales || userConfig.themeConfig?.locales || [], logo: userConfig?.logo || '', diff --git a/packages/shared/src/runtime-utils/index.ts b/packages/shared/src/runtime-utils/index.ts index eda88f8f7..a88b39511 100644 --- a/packages/shared/src/runtime-utils/index.ts +++ b/packages/shared/src/runtime-utils/index.ts @@ -96,12 +96,10 @@ export function normalizeSlash(url: string) { return removeTrailingSlash(addLeadingSlash(normalizePosixPath(url))); } -export function isExternalUrl(url = '') { +export function isExternalUrl(url = '', externalLinkPrefixes?: string[]) { return ( - url.startsWith('http://') || - url.startsWith('https://') || - url.startsWith('mailto:') || - url.startsWith('tel:') + /^((https?|mailto|tel):)?\/\//.test(url) || + externalLinkPrefixes?.some(prefix => url.startsWith(prefix)) ); } diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index 530ef98d0..d30e6e9ad 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -71,6 +71,10 @@ export interface UserConfig { * @default '/' */ base?: string; + /** + * Recognize the links with following prefixes as external links. + */ + externalLinkPrefixes?: string[]; /** * Path to html icon file. */ @@ -195,6 +199,7 @@ export type BaseRuntimePageInfo = Omit< export interface SiteData { root: string; base: string; + externalLinkPrefixes: string[]; lang: string; route: RouteOptions; locales: { lang: string; label: string }[]; diff --git a/packages/theme-default/src/components/Link/index.tsx b/packages/theme-default/src/components/Link/index.tsx index 336c6f1a3..29776465d 100644 --- a/packages/theme-default/src/components/Link/index.tsx +++ b/packages/theme-default/src/components/Link/index.tsx @@ -8,6 +8,7 @@ import { normalizeRoutePath, withBase, isEqualPath, + usePageData, } from '@rspress/runtime'; import nprogress from 'nprogress'; import { routes } from 'virtual-routes'; @@ -35,7 +36,8 @@ export function Link(props: LinkProps) { keepCurrentParams = false, ...rest } = props; - const isExternal = isExternalUrl(href); + const { siteData } = usePageData(); + const isExternal = isExternalUrl(href, siteData.externalLinkPrefixes); const target = isExternal ? '_blank' : ''; const rel = isExternal ? 'noopener noreferrer' : undefined; const withBaseUrl = isExternal ? href : withBase(normalizeHref(href));