mirror of
https://github.com/nocobase/nocobase.git
synced 2026-08-29 02:03:53 +08:00
fix(docs): cross ref
This commit is contained in:
@@ -8,6 +8,7 @@ import type {
|
||||
SidebarItem,
|
||||
SidebarSectionHeader,
|
||||
} from '@rspress/core';
|
||||
import { encodeTargetBlankHref } from '../shared/blankTargetLink';
|
||||
|
||||
// ── 内部类型 ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -29,6 +30,23 @@ function shouldProxyLink(obj: Record<string, unknown>): boolean {
|
||||
return obj.proxy !== false;
|
||||
}
|
||||
|
||||
function isExternalLink(link: string): boolean {
|
||||
return link.startsWith('http://') || link.startsWith('https://');
|
||||
}
|
||||
|
||||
function resolveSidebarLink(
|
||||
obj: Record<string, unknown>,
|
||||
link: string,
|
||||
crossRefs: CrossRef[],
|
||||
topDir: string,
|
||||
): string {
|
||||
if (!shouldProxyLink(obj)) {
|
||||
return isExternalLink(link) ? link : encodeTargetBlankHref(link);
|
||||
}
|
||||
|
||||
return rewriteIfCrossRef(link, crossRefs, topDir);
|
||||
}
|
||||
|
||||
function collectLeafLinks(entries: unknown[]): string[] {
|
||||
const links: string[] = [];
|
||||
for (const entry of entries) {
|
||||
@@ -47,6 +65,23 @@ function collectLeafLinks(entries: unknown[]): string[] {
|
||||
return links;
|
||||
}
|
||||
|
||||
function hasUnproxiedLinks(entries: unknown[]): boolean {
|
||||
for (const entry of entries) {
|
||||
const obj = entry as Record<string, unknown>;
|
||||
if (!obj || typeof obj !== 'object') continue;
|
||||
|
||||
if (typeof obj.link === 'string' && !shouldProxyLink(obj)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj.items) && hasUnproxiedLinks(obj.items as unknown[])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 从相对路径取顶层模块名:'/api/auth/sub' → '/api' */
|
||||
function getTopLevelDir(relDir: string): string {
|
||||
const parts = relDir.split('/').filter(Boolean);
|
||||
@@ -166,9 +201,12 @@ function resolveEntries(
|
||||
collapsed: obj.collapsed === true,
|
||||
};
|
||||
if (typeof obj.link === 'string') {
|
||||
group.link = shouldProxyLink(obj)
|
||||
? rewriteIfCrossRef(obj.link as string, crossRefs, topDir)
|
||||
: (obj.link as string);
|
||||
group.link = resolveSidebarLink(
|
||||
obj,
|
||||
obj.link as string,
|
||||
crossRefs,
|
||||
topDir,
|
||||
);
|
||||
}
|
||||
result.push(group);
|
||||
continue;
|
||||
@@ -178,9 +216,7 @@ function resolveEntries(
|
||||
if (type === 'custom-link' && typeof obj.link === 'string') {
|
||||
result.push({
|
||||
text: (obj.label as string) || '',
|
||||
link: shouldProxyLink(obj)
|
||||
? rewriteIfCrossRef(obj.link as string, crossRefs, topDir)
|
||||
: (obj.link as string),
|
||||
link: resolveSidebarLink(obj, obj.link as string, crossRefs, topDir),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -270,6 +306,7 @@ export function pluginCrossRefSidebar(): RspressPlugin {
|
||||
|
||||
config(config) {
|
||||
const root = config.root || path.join(process.cwd(), 'docs');
|
||||
let hasTargetBlankLinks = false;
|
||||
crossRefs = [];
|
||||
|
||||
// 1. 扫描所有 _meta.json 检测跨模块引用(以顶层目录为单位判断)
|
||||
@@ -284,6 +321,7 @@ export function pluginCrossRefSidebar(): RspressPlugin {
|
||||
|
||||
const meta: unknown = JSON.parse(fs.readFileSync(metaFile, 'utf-8'));
|
||||
if (!Array.isArray(meta) || meta.length === 0) continue;
|
||||
hasTargetBlankLinks = hasTargetBlankLinks || hasUnproxiedLinks(meta);
|
||||
|
||||
for (const link of collectLeafLinks(meta)) {
|
||||
if (link.startsWith('http://') || link.startsWith('https://'))
|
||||
@@ -311,7 +349,7 @@ export function pluginCrossRefSidebar(): RspressPlugin {
|
||||
crossRefCanonicalMap[ref.aliasRoute] = ref.targetLink.split('#')[0];
|
||||
}
|
||||
|
||||
if (crossRefs.length === 0) return config;
|
||||
if (crossRefs.length === 0 && !hasTargetBlankLinks) return config;
|
||||
|
||||
// 2. 为所有顶层目录生成 sidebar(设置后 _meta.json 不再生效)
|
||||
const sidebar: Sidebar = (config.themeConfig?.sidebar as Sidebar) || {};
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export const TARGET_BLANK_PREFIX = '__target_blank__:';
|
||||
|
||||
export function encodeTargetBlankHref(href: string): string {
|
||||
return `${TARGET_BLANK_PREFIX}${href}`;
|
||||
}
|
||||
|
||||
export function decodeTargetBlankHref(href: string) {
|
||||
if (!href.startsWith(TARGET_BLANK_PREFIX)) {
|
||||
return {
|
||||
href,
|
||||
targetBlank: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
href: href.slice(TARGET_BLANK_PREFIX.length) || '/',
|
||||
targetBlank: true,
|
||||
};
|
||||
}
|
||||
Vendored
+93
-30
@@ -1,12 +1,30 @@
|
||||
import { NoSSR, useFrontmatter, useLang, useNavigate, usePage, usePages } from '@rspress/core/runtime';
|
||||
import {
|
||||
NoSSR,
|
||||
useFrontmatter,
|
||||
useLang,
|
||||
useNavigate,
|
||||
usePage,
|
||||
usePages,
|
||||
} from '@rspress/core/runtime';
|
||||
import type { Feature } from '@rspress/core';
|
||||
import { Badge, DocContent, DocLayout as OriginalDocLayout, getCustomMDXComponent as basicGetCustomMDXComponent, Layout as BasicLayout, HomeFooter, Link, renderHtmlOrText, Tab, Tabs } from '@rspress/core/theme-original';
|
||||
import {
|
||||
Badge,
|
||||
DocContent,
|
||||
DocLayout as OriginalDocLayout,
|
||||
getCustomMDXComponent as basicGetCustomMDXComponent,
|
||||
Layout as BasicLayout,
|
||||
HomeFooter,
|
||||
Link as OriginalLink,
|
||||
renderHtmlOrText,
|
||||
Tab,
|
||||
Tabs,
|
||||
} from '@rspress/core/theme-original';
|
||||
import {
|
||||
LlmsContainer,
|
||||
LlmsCopyButton,
|
||||
LlmsViewOptions,
|
||||
} from '@rspress/plugin-llms/runtime';
|
||||
import type { ComponentProps, JSX, ReactNode } from 'react';
|
||||
import React, { type ComponentProps, type JSX, type ReactNode } from 'react';
|
||||
import { PluginCard } from './components/PluginCard';
|
||||
import { PluginInfo } from './components/PluginInfo';
|
||||
import { PluginList } from './components/PluginList';
|
||||
@@ -14,6 +32,7 @@ import { ProvidedBy } from './components/ProvidedBy';
|
||||
import './index.scss';
|
||||
import { transformHref, useLangPrefix } from './utils';
|
||||
import { HomeHero } from './components/HomeHero';
|
||||
import { decodeTargetBlankHref } from '../shared/blankTargetLink';
|
||||
|
||||
function getCustomMDXComponent() {
|
||||
const { h1: H1, ...mdxComponents } = basicGetCustomMDXComponent();
|
||||
@@ -46,6 +65,24 @@ function getCustomMDXComponent() {
|
||||
|
||||
export { getCustomMDXComponent };
|
||||
|
||||
export function Link(props: ComponentProps<typeof OriginalLink>) {
|
||||
const { href = '/', rel, target, ...restProps } = props;
|
||||
const { href: nextHref, targetBlank } = decodeTargetBlankHref(href);
|
||||
|
||||
if (!targetBlank) {
|
||||
return <OriginalLink {...props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<OriginalLink
|
||||
{...restProps}
|
||||
href={nextHref}
|
||||
target={target ?? '_blank'}
|
||||
rel={rel ?? 'noopener noreferrer'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export interface HomeLayoutProps {
|
||||
beforeHero?: ReactNode;
|
||||
afterHero?: ReactNode;
|
||||
@@ -78,15 +115,20 @@ type ThemePage = {
|
||||
frontmatter?: ThemeFrontmatter & Record<string, unknown>;
|
||||
};
|
||||
|
||||
function getFeatureGroups(page?: ThemePage | { frontmatter?: ThemeFrontmatter }): HomeFeatureGroup[] {
|
||||
function getFeatureGroups(
|
||||
page?: ThemePage | { frontmatter?: ThemeFrontmatter },
|
||||
): HomeFeatureGroup[] {
|
||||
return page?.frontmatter?.features ?? [];
|
||||
}
|
||||
|
||||
function isPluginDetailPage(frontmatter?: ThemeFrontmatter, routePath?: string): boolean {
|
||||
function isPluginDetailPage(
|
||||
frontmatter?: ThemeFrontmatter,
|
||||
routePath?: string,
|
||||
): boolean {
|
||||
return Boolean(
|
||||
frontmatter?.displayName &&
|
||||
frontmatter?.packageName &&
|
||||
routePath?.includes('/plugins/@nocobase/'),
|
||||
frontmatter?.packageName &&
|
||||
routePath?.includes('/plugins/@nocobase/'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,16 +159,14 @@ export function HomeLayout(props: HomeLayoutProps) {
|
||||
>
|
||||
<div className="rp-pb-12">
|
||||
{beforeHero}
|
||||
{
|
||||
frontmatter.hero && (
|
||||
<HomeHero
|
||||
frontmatter={frontmatter}
|
||||
routePath={routePath}
|
||||
beforeHeroActions={beforeHeroActions}
|
||||
afterHeroActions={afterHeroActions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{frontmatter.hero && (
|
||||
<HomeHero
|
||||
frontmatter={frontmatter}
|
||||
routePath={routePath}
|
||||
beforeHeroActions={beforeHeroActions}
|
||||
afterHeroActions={afterHeroActions}
|
||||
/>
|
||||
)}
|
||||
{afterHero}
|
||||
{beforeFeatures}
|
||||
<HomeFeature />
|
||||
@@ -168,8 +208,8 @@ export const Layout = () => {
|
||||
};
|
||||
const isPluginDetailPage = Boolean(
|
||||
frontmatter?.displayName &&
|
||||
frontmatter?.packageName &&
|
||||
routePath?.startsWith('/plugins/@'),
|
||||
frontmatter?.packageName &&
|
||||
routePath?.startsWith('/plugins/@'),
|
||||
);
|
||||
const pageClassName = [
|
||||
isPluginDetailPage ? 'plugin-detail-page' : '',
|
||||
@@ -199,7 +239,9 @@ export const Layout = () => {
|
||||
|
||||
export function DocLayout(props: ComponentProps<typeof OriginalDocLayout>) {
|
||||
const { page } = usePage();
|
||||
const { frontmatter } = useFrontmatter() as { frontmatter?: ThemeFrontmatter };
|
||||
const { frontmatter } = useFrontmatter() as {
|
||||
frontmatter?: ThemeFrontmatter;
|
||||
};
|
||||
|
||||
if (!isPluginDetailPage(frontmatter, page.routePath)) {
|
||||
return <OriginalDocLayout {...props} />;
|
||||
@@ -237,7 +279,9 @@ function HomeFeatureItem({ feature }: { feature: Feature }): JSX.Element {
|
||||
<div className="rp-home-feature__item-wrapper">
|
||||
<article
|
||||
key={title}
|
||||
className={`rp-home-feature__card ${link ? 'rp-home-feature__card--clickable' : ''}`}
|
||||
className={`rp-home-feature__card ${
|
||||
link ? 'rp-home-feature__card--clickable' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (link) {
|
||||
navigate(transformHref(link, langPrefix));
|
||||
@@ -252,7 +296,9 @@ function HomeFeatureItem({ feature }: { feature: Feature }): JSX.Element {
|
||||
<h2 className="rp-home-feature__title">
|
||||
{link ? (
|
||||
<Link href={transformHref(link, langPrefix)}>{title}</Link>
|
||||
) : title}
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</h2>
|
||||
</div>
|
||||
<p
|
||||
@@ -266,7 +312,9 @@ function HomeFeatureItem({ feature }: { feature: Feature }): JSX.Element {
|
||||
}
|
||||
|
||||
export function HomeFeature() {
|
||||
const { frontmatter } = useFrontmatter() as { frontmatter?: ThemeFrontmatter };
|
||||
const { frontmatter } = useFrontmatter() as {
|
||||
frontmatter?: ThemeFrontmatter;
|
||||
};
|
||||
const { pages } = usePages() as { pages: ThemePage[] };
|
||||
const lang = useLang();
|
||||
const featureGroups = frontmatter?.features ?? [];
|
||||
@@ -278,17 +326,32 @@ export function HomeFeature() {
|
||||
let items = feature.items ?? [];
|
||||
|
||||
if (index === 2) {
|
||||
const page = pages.find((currentPage) => currentPage.lang === lang && currentPage.frontmatter?.pageName === 'guide');
|
||||
const page = pages.find(
|
||||
(currentPage) =>
|
||||
currentPage.lang === lang &&
|
||||
currentPage.frontmatter?.pageName === 'guide',
|
||||
);
|
||||
if (page) {
|
||||
const allItems = getFeatureGroups(page).flatMap((group) => group.items ?? []);
|
||||
const allItems = getFeatureGroups(page).flatMap(
|
||||
(group) => group.items ?? [],
|
||||
);
|
||||
items = [...allItems.filter((item) => item.showOnHome), ...items];
|
||||
}
|
||||
} else if (index === 3) {
|
||||
const page = pages.find((currentPage) => currentPage.lang === lang && currentPage.frontmatter?.pageName === 'development');
|
||||
const page = pages.find(
|
||||
(currentPage) =>
|
||||
currentPage.lang === lang &&
|
||||
currentPage.frontmatter?.pageName === 'development',
|
||||
);
|
||||
if (page) {
|
||||
// 把 page.frontmatter?.features 里的 items 都拍平合并,取前 8 个
|
||||
const allItems = getFeatureGroups(page).flatMap((group) => group.items ?? []);
|
||||
items = [...allItems.filter((item) => item.showOnHome), ...(feature.items ?? [])];
|
||||
const allItems = getFeatureGroups(page).flatMap(
|
||||
(group) => group.items ?? [],
|
||||
);
|
||||
items = [
|
||||
...allItems.filter((item) => item.showOnHome),
|
||||
...(feature.items ?? []),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,7 +367,7 @@ export function HomeFeature() {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
@@ -325,7 +388,7 @@ export function HomeFeature() {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user