Core: Support non-external link in meta.json

This commit is contained in:
fuma-nama
2024-05-18 12:17:42 +08:00
parent 771314cd30
commit 26f464d46f
6 changed files with 74 additions and 38 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"fumadocs-core": patch
---
Support relative paths in meta.json
+5
View File
@@ -0,0 +1,5 @@
---
"fumadocs-core": patch
---
Support non-external link in meta.json
+5 -4
View File
@@ -8,7 +8,7 @@ import {
import type { FileData, MetaData, PageData } from './types';
import type { CreatePageTreeBuilderOptions } from './page-tree-builder';
import { createPageTreeBuilder } from './page-tree-builder';
import { joinPaths, splitPath, type FileInfo } from './path';
import { splitPath, type FileInfo } from './path';
import type { File, Storage } from './file-system';
export interface LoaderConfig {
@@ -115,10 +115,11 @@ export function createGetUrl(
baseUrl: string,
): (slugs: string[], locale?: string) => string {
return (slugs, locale) => {
let paths = [baseUrl, ...slugs];
if (locale) paths = [baseUrl, locale, ...slugs];
const paths = locale
? [...splitPath(baseUrl), locale, ...slugs]
: [...splitPath(baseUrl), ...slugs];
return joinPaths(paths, 'leading');
return `/${paths.join('/')}`;
};
}
+19 -14
View File
@@ -1,7 +1,7 @@
import type { ReactElement } from 'react';
import type * as PageTree from '../server/page-tree';
import type { File, Folder, Storage } from './file-system';
import { joinPaths } from './path';
import { resolvePath } from './path';
import { type FileData } from './types';
interface PageTreeBuilderContext {
@@ -20,7 +20,7 @@ export interface PageTreeBuilder {
build: () => PageTree.Root;
/**
* Build page tree and fallback to the default language if the page doesn't exist
* Build page tree and fallback to the default language if the localized page doesn't exist
*/
buildI18n: (
options?: Partial<BuildPageTreeOptionsWithI18n>,
@@ -32,12 +32,14 @@ export interface CreatePageTreeBuilderOptions {
resolveIcon?: (icon: string) => ReactElement | undefined;
}
const external = /\[(?<text>.+)\]\((?<url>.+)\)/;
const separator = /---(?<name>.*?)---/;
const link = /^\[(?<text>.+)]\((?<url>.+)\)$/;
const separator = /^---(?<name>.*?)---$/;
const rest = '...';
const extractor = /\.\.\.(?<name>.+)/;
const extractor = /^\.\.\.(?<name>.+)$/;
/**
* @param nodes - All nodes to be built
* @param ctx - Context
* @param skipIndex - Skip index
* @returns Nodes with specified locale in context (sorted)
*/
@@ -88,24 +90,27 @@ function resolveFolderItem(
];
}
const externalResult = external.exec(item);
if (externalResult?.groups) {
const linkResult = link.exec(item);
if (linkResult?.groups) {
const { url, text } = linkResult.groups;
const isRelative =
url.startsWith('/') || url.startsWith('#') || url.startsWith('.');
return [
{
type: 'page',
name: externalResult.groups.text,
url: externalResult.groups.url,
external: true,
name: text,
url,
external: !isRelative,
},
];
}
const extractResult = extractor.exec(item);
const path = joinPaths([
const path = resolvePath(
folder.file.path,
extractResult?.groups?.name ?? item,
]);
);
const itemNode = ctx.storage.readDir(path) ?? ctx.storage.read(path, 'page');
if (!itemNode) return [];
@@ -126,11 +131,11 @@ function buildFolderNode(
defaultIsRoot: boolean,
ctx: PageTreeBuilderContext,
): PageTree.Folder {
const metaPath = joinPaths([folder.file.path, 'meta']);
const metaPath = resolvePath(folder.file.path, 'meta');
let meta = ctx.storage.read(metaPath, 'meta');
meta = findLocalizedFile(metaPath, 'meta', ctx) ?? meta;
const indexFile = ctx.storage.read(
joinPaths([folder.file.flattenedPath, 'index']),
resolvePath(folder.file.flattenedPath, 'index'),
'page',
);
+30 -12
View File
@@ -25,7 +25,9 @@ export interface FileInfo {
export function parseFilePath(path: string): FileInfo {
const normalized = normalizePath(path);
const parsed = parse(normalized);
const flattenedPath = joinPaths([parsed.dir, parsed.name]);
const flattenedPath = [parsed.dir, parsed.name]
.filter((p) => p.length > 0)
.join('/');
const [name, locale] = parsed.name.split('.');
return {
@@ -60,7 +62,7 @@ export function normalizePath(path: string): string {
const segments = splitPath(slash(path));
if (segments[0] === '.' || segments[0] === '..')
throw new Error("It must not start with './' or '../'");
return joinPaths(segments);
return segments.join('/');
}
/**
@@ -71,25 +73,41 @@ export function splitPath(path: string): string[] {
}
/**
* Convert paths to an array, slashes within the path will be ignored
* @param paths - Paths to join
* Resolve paths, slashes within the path will be ignored
* @param from - Path to resolve from
* @param join - Paths to resolve
* @param slashMode - whether to add a trailing/leading slash to path
* @example
* ```
* ['a','b','c'] // 'a/b/c'
* ['a','b'] // 'a/b'
* ['/a'] // 'a'
* ['a', '/b'] // 'a/b'
* ['a', 'b/c'] // 'a/b/c'
* ['a', '../b/c'] // 'b/c'
* ```
*/
export function joinPaths(
paths: string[],
export function resolvePath(
from: string,
join: string,
slashMode: 'leading' | 'trailing' | 'none' = 'none',
): string {
const joined = paths
// avoid slashes in path and filter empty
.flatMap((path) => splitPath(path))
.join('/');
const v1 = splitPath(from),
v2 = splitPath(join);
while (v2.length > 0) {
switch (v2[0]) {
case '..':
v1.pop();
break;
case '.':
break;
default:
v1.push(v2[0]);
}
v2.shift();
}
const joined = v1.join('/');
switch (slashMode) {
case 'leading':
+10 -8
View File
@@ -1,5 +1,5 @@
import {
joinPaths,
resolvePath,
parseFilePath,
parseFolderPath,
splitPath,
@@ -91,14 +91,16 @@ describe('Path utilities', () => {
);
});
test('join paths', () => {
expect(joinPaths(['a', 'b', 'c'])).toBe('a/b/c');
expect(joinPaths(['/a'])).toBe('a');
expect(joinPaths(['a/', '/b'])).toBe('a/b');
expect(joinPaths(['a/', 'b/c'])).toBe('a/b/c');
test('resolve paths', () => {
expect(resolvePath('a', 'b')).toBe('a/b');
expect(resolvePath('/a', '')).toBe('a');
expect(resolvePath('a/', '/b')).toBe('a/b');
expect(joinPaths(['a', 'b'], 'leading')).toBe('/a/b');
expect(joinPaths(['a', 'b'], 'trailing')).toBe('a/b/');
expect(resolvePath('a/', '../b/c')).toBe('b/c');
expect(resolvePath('a/', './b/c')).toBe('a/b/c');
expect(resolvePath('a', 'b', 'leading')).toBe('/a/b');
expect(resolvePath('a', 'b', 'trailing')).toBe('a/b/');
});
test('split paths', () => {