mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 23:49:51 +00:00
Core: Support non-external link in meta.json
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"fumadocs-core": patch
|
||||
---
|
||||
|
||||
Support relative paths in meta.json
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"fumadocs-core": patch
|
||||
---
|
||||
|
||||
Support non-external link in meta.json
|
||||
@@ -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('/')}`;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user