mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
Core & UI: Hotfix URL normalization logic
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'fumadocs-core': patch
|
||||
'fumadocs-ui': patch
|
||||
---
|
||||
|
||||
Hotfix URL normalization logic
|
||||
@@ -9,5 +9,11 @@
|
||||
"module": "ESNext",
|
||||
"target": "ES2020"
|
||||
},
|
||||
"exclude": ["node_modules", "test/repo", "test/repo-2", "eslint.config.mjs"]
|
||||
"exclude": [
|
||||
"dist",
|
||||
"node_modules",
|
||||
"test/repo",
|
||||
"test/repo-2",
|
||||
"eslint.config.mjs"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type ReactNode, useMemo } from 'react';
|
||||
import type * as PageTree from '@/source/page-tree/definitions';
|
||||
import { normalizeUrl } from '@/utils/normalize-url';
|
||||
|
||||
export interface BreadcrumbItem {
|
||||
name: ReactNode;
|
||||
@@ -117,42 +118,42 @@ export function searchPath(
|
||||
nodes: PageTree.Node[],
|
||||
url: string,
|
||||
): PageTree.Node[] | null {
|
||||
if (url.endsWith('/')) url = url.slice(0, -1);
|
||||
const items: PageTree.Node[] = [];
|
||||
url = normalizeUrl(url);
|
||||
|
||||
let separator: PageTree.Separator | undefined;
|
||||
function run(nodes: PageTree.Node[]): boolean {
|
||||
let separator: PageTree.Separator | undefined;
|
||||
|
||||
for (const node of nodes) {
|
||||
if (node.type === 'separator') separator = node;
|
||||
for (const node of nodes) {
|
||||
if (node.type === 'separator') separator = node;
|
||||
|
||||
if (node.type === 'folder') {
|
||||
if (node.index?.url === url) {
|
||||
const items: PageTree.Node[] = [];
|
||||
if (node.type === 'folder') {
|
||||
if (node.index?.url === url) {
|
||||
if (separator) items.push(separator);
|
||||
items.push(node, node.index);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (run(node.children)) {
|
||||
items.unshift(node);
|
||||
if (separator) items.unshift(separator);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === 'page' && node.url === url) {
|
||||
if (separator) items.push(separator);
|
||||
items.push(node, node.index);
|
||||
items.push(node);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
const items = searchPath(node.children, url);
|
||||
|
||||
if (items) {
|
||||
items.unshift(node);
|
||||
if (separator) items.unshift(separator);
|
||||
|
||||
return items;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === 'page' && node.url === url) {
|
||||
const items: PageTree.Node[] = [];
|
||||
|
||||
if (separator) items.push(separator);
|
||||
items.push(node);
|
||||
|
||||
return items;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
run(nodes);
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { Transformer } from 'unified';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import { imageSize } from 'image-size';
|
||||
import type { MdxjsEsm } from 'mdast-util-mdxjs-esm';
|
||||
import { joinPath, slash } from '@/utils/path';
|
||||
import type { ISizeCalculationResult } from 'image-size/types/interface';
|
||||
import { imageSizeFromFile } from 'image-size/fromFile';
|
||||
import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
|
||||
@@ -252,7 +251,7 @@ export function remarkImage({
|
||||
}
|
||||
|
||||
function getImportPath(file: string, dir: string): string {
|
||||
const relative = slash(path.relative(dir, file));
|
||||
const relative = path.relative(dir, file).replaceAll(path.sep, '/');
|
||||
|
||||
return relative.startsWith('../') ? relative : `./${relative}`;
|
||||
}
|
||||
@@ -280,7 +279,11 @@ function parseSrc(
|
||||
if (src.startsWith('/')) {
|
||||
if (EXTERNAL_URL_REGEX.test(publicDir)) {
|
||||
const url = new URL(publicDir);
|
||||
url.pathname = joinPath(url.pathname, src);
|
||||
const segs = [...url.pathname.split('/'), ...src.split('/')].filter(
|
||||
(v) => v.length > 0,
|
||||
);
|
||||
|
||||
url.pathname = `/${segs.join('/')}`;
|
||||
return { type: 'url', url };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { dirname } from './path';
|
||||
import { splitPath } from '@/utils/path';
|
||||
import { dirname, splitPath } from './path';
|
||||
|
||||
/**
|
||||
* In memory file system.
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { type MetaData, type PageData } from '@/source/types';
|
||||
import { FileSystem } from './file-system';
|
||||
import { joinPath, slash, splitPath } from '@/utils/path';
|
||||
import { type VirtualFile } from '@/source/loader';
|
||||
import { basename, dirname } from '@/source/path';
|
||||
import { basename, dirname, joinPath, slash, splitPath } from '@/source/path';
|
||||
import type { I18nConfig } from '@/i18n';
|
||||
|
||||
export interface LoadOptions {
|
||||
|
||||
@@ -14,9 +14,10 @@ import {
|
||||
dirname,
|
||||
extname,
|
||||
type FileInfo,
|
||||
joinPath,
|
||||
parseFilePath,
|
||||
} from './path';
|
||||
import { joinPath } from '@/utils/path';
|
||||
import { normalizeUrl } from '@/utils/normalize-url';
|
||||
|
||||
export interface LoaderConfig {
|
||||
source: SourceConfig;
|
||||
@@ -278,9 +279,12 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
|
||||
baseUrl = '/',
|
||||
i18n,
|
||||
slugs: slugsFn,
|
||||
url: getUrl = createGetUrl(baseUrl ?? '/', i18n),
|
||||
url: urlFn,
|
||||
transformers = [],
|
||||
} = options;
|
||||
const getUrl: UrlFn = urlFn
|
||||
? (...args) => normalizeUrl(urlFn(...args))
|
||||
: createGetUrl(baseUrl, i18n);
|
||||
const defaultLanguage = i18n?.defaultLanguage ?? '';
|
||||
const files = loadSource(source);
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import type * as PageTree from './definitions';
|
||||
import { joinPath } from '@/utils/path';
|
||||
import type { MetaData, PageData, UrlFn } from '../types';
|
||||
import type { ContentStorage } from '@/source/load-files';
|
||||
import { basename, extname } from '@/source/path';
|
||||
import { basename, extname, joinPath } from '@/source/path';
|
||||
import {
|
||||
legacyTransformer,
|
||||
type LegacyTransformerOptions,
|
||||
|
||||
@@ -89,3 +89,50 @@ export function parseFolderPath(path: string): FolderInfo {
|
||||
path,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Split path into segments, trailing/leading slashes are removed
|
||||
*/
|
||||
export function splitPath(path: string): string[] {
|
||||
return path.split('/').filter((p) => p.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve paths, slashes within the path will be ignored
|
||||
* @param paths - Paths to join
|
||||
* @example
|
||||
* ```
|
||||
* ['a','b'] // 'a/b'
|
||||
* ['/a'] // 'a'
|
||||
* ['a', '/b'] // 'a/b'
|
||||
* ['a', '../b/c'] // 'b/c'
|
||||
* ```
|
||||
*/
|
||||
export function joinPath(...paths: string[]): string {
|
||||
const out = [];
|
||||
const parsed = paths.flatMap(splitPath);
|
||||
|
||||
for (const seg of parsed) {
|
||||
switch (seg) {
|
||||
case '..':
|
||||
out.pop();
|
||||
break;
|
||||
case '.':
|
||||
break;
|
||||
default:
|
||||
out.push(seg);
|
||||
}
|
||||
}
|
||||
|
||||
return out.join('/');
|
||||
}
|
||||
|
||||
export function slash(path: string): string {
|
||||
const isExtendedLengthPath = path.startsWith('\\\\?\\');
|
||||
|
||||
if (isExtendedLengthPath) {
|
||||
return path;
|
||||
}
|
||||
|
||||
return path.replaceAll('\\', '/');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* normalize URL into the Fumadocs standard form (`/slug-1/slug-2`)
|
||||
*/
|
||||
export function normalizeUrl(url: string) {
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) return url;
|
||||
|
||||
if (!url.startsWith('/')) url = '/' + url;
|
||||
if (url.length > 1 && url.endsWith('/')) url = url.slice(0, -1);
|
||||
return url;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* Split path into segments, trailing/leading slashes are removed
|
||||
*/
|
||||
export function splitPath(path: string): string[] {
|
||||
return path.split('/').filter((p) => p.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve paths, slashes within the path will be ignored
|
||||
* @param paths - Paths to join
|
||||
* @example
|
||||
* ```
|
||||
* ['a','b'] // 'a/b'
|
||||
* ['/a'] // 'a'
|
||||
* ['a', '/b'] // 'a/b'
|
||||
* ['a', '../b/c'] // 'b/c'
|
||||
* ```
|
||||
*/
|
||||
export function joinPath(...paths: string[]): string {
|
||||
const out = [];
|
||||
const parsed = paths.flatMap(splitPath);
|
||||
|
||||
for (const seg of parsed) {
|
||||
switch (seg) {
|
||||
case '..':
|
||||
out.pop();
|
||||
break;
|
||||
case '.':
|
||||
break;
|
||||
default:
|
||||
out.push(seg);
|
||||
}
|
||||
}
|
||||
|
||||
return out.join('/');
|
||||
}
|
||||
|
||||
export function slash(path: string): string {
|
||||
const isExtendedLengthPath = path.startsWith('\\\\?\\');
|
||||
|
||||
if (isExtendedLengthPath) {
|
||||
return path;
|
||||
}
|
||||
|
||||
return path.replaceAll('\\', '/');
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
import { parseFilePath, parseFolderPath } from '@/source/path';
|
||||
import {
|
||||
joinPath,
|
||||
parseFilePath,
|
||||
parseFolderPath,
|
||||
splitPath,
|
||||
} from '@/source/path';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import type { Root } from '@/source/page-tree/definitions';
|
||||
import { findNeighbour } from '@/utils/page-tree';
|
||||
import { PageTree } from '../dist/server';
|
||||
import { getBreadcrumbItems } from '@/breadcrumb';
|
||||
import { joinPath, splitPath } from '@/utils/path';
|
||||
|
||||
test('Find Neighbours', () => {
|
||||
const tree: Root = {
|
||||
@@ -117,6 +121,11 @@ test('Breadcrumbs', () => {
|
||||
const tree: PageTree.Root = {
|
||||
name: 'Hello World',
|
||||
children: [
|
||||
{
|
||||
type: 'page',
|
||||
name: 'Introduction',
|
||||
url: '/',
|
||||
},
|
||||
{
|
||||
type: 'page',
|
||||
name: 'Hello World',
|
||||
@@ -148,4 +157,13 @@ test('Breadcrumbs', () => {
|
||||
expect(getBreadcrumbItems('/docs/folder', tree)).toStrictEqual([
|
||||
{ name: 'World', url: '/docs/folder' },
|
||||
]);
|
||||
|
||||
expect(getBreadcrumbItems('/', tree)).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"name": "Introduction",
|
||||
"url": "/",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
+4
-1
@@ -1,4 +1,4 @@
|
||||
This is your new *vault*.
|
||||
This is your new _vault_.
|
||||
|
||||
Make a note of something, [[create a link]], or try [the Importer](https://help.obsidian.md/Plugins/Importer)!
|
||||
|
||||
@@ -6,8 +6,11 @@ Make a note of something, [[create a link]], or try [the Importer](https://help.
|
||||
|
||||
When you're ready, delete this note and make the vault your own.
|
||||
![[Xmas.png]]
|
||||
|
||||
## Introduction!!
|
||||
|
||||
## Introduction!!
|
||||
|
||||
## Introduction!!
|
||||
|
||||
## Introduction!!
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
hi [[Welcome]] [[Welcome#Introduction!!]]
|
||||
|
||||
```ts
|
||||
console.log("Hello World")
|
||||
```
|
||||
console.log('Hello World');
|
||||
```
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
hi [Welcome](./Welcome.mdx)[Welcome](./Welcome.mdx#introduction)
|
||||
|
||||
```ts
|
||||
console.log("Hello World")
|
||||
console.log('Hello World');
|
||||
```
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
function normalize(url: string) {
|
||||
if (url.length > 1 && url.endsWith('/')) return url.slice(0, -1);
|
||||
return url;
|
||||
}
|
||||
|
||||
export function isActive(
|
||||
url: string,
|
||||
pathname: string,
|
||||
nested = true,
|
||||
): boolean {
|
||||
if (url.endsWith('/')) url = url.slice(0, -1);
|
||||
if (pathname.endsWith('/')) pathname = pathname.slice(0, -1);
|
||||
url = normalize(url);
|
||||
pathname = normalize(pathname);
|
||||
|
||||
return url === pathname || (nested && pathname.startsWith(`${url}/`));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user