Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions packages/chronicle/src/lib/folder-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Node, Folder } from 'fumadocs-core/page-tree';

const NodeType = {
Page: 'page',
Folder: 'folder',
} as const;

export function parentPath(url: string): string {
const parts = url.split('/').filter(Boolean);
parts.pop();
return '/' + parts.join('/');
}

export function getFolderPath(node: Folder): string | null {
if (node.index) return node.index.url;
for (const child of node.children) {
if (child.type === NodeType.Page) return parentPath(child.url);
}
for (const child of node.children) {
if (child.type === NodeType.Folder) {
const childPath = getFolderPath(child as Folder);
if (childPath) return parentPath(childPath);
}
}
return null;
}
85 changes: 85 additions & 0 deletions packages/chronicle/src/lib/source-utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, expect, test } from 'bun:test'
import type { Node, Folder } from 'fumadocs-core/page-tree'
import { parentPath, getFolderPath } from './folder-utils'

function page(url: string): Node {
return { type: 'page', name: 'Page', url } as Node
}

function folder(name: string, children: Node[], indexUrl?: string): Folder {
return {
type: 'folder',
name,
children,
...(indexUrl ? { index: { url: indexUrl } } : {}),
} as Folder
}

describe('parentPath', () => {
test('returns parent of page URL', () => {
expect(parentPath('/docs/guides/install')).toBe('/docs/guides')
})

test('returns root for top-level page', () => {
expect(parentPath('/docs')).toBe('/')
})

test('handles trailing segments', () => {
expect(parentPath('/a/b/c/d')).toBe('/a/b/c')
})

test('handles root', () => {
expect(parentPath('/')).toBe('/')
})
})

describe('getFolderPath', () => {
test('returns index URL when folder has index', () => {
const f = folder('Guides', [page('/docs/guides/install')], '/docs/guides')
expect(getFolderPath(f)).toBe('/docs/guides')
})

test('derives path from direct child page', () => {
const f = folder('Guides', [page('/docs/guides/install')])
expect(getFolderPath(f)).toBe('/docs/guides')
})

test('derives path from subfolder child (not deeply nested)', () => {
const f = folder('Tasking', [
folder('Via Order Desk', [page('/docs/tasking/via_order_desk/package')])
])
expect(getFolderPath(f)).toBe('/docs/tasking')
})

test('handles folder with & in path', () => {
const f = folder('Cart & Order', [page('/docs/cart&order/working_with_cart')])
expect(getFolderPath(f)).toBe('/docs/cart&order')
})

test('handles folder with space in path', () => {
const f = folder('My Folder', [page('/docs/my folder/intro')])
expect(getFolderPath(f)).toBe('/docs/my folder')
})

test('returns null for empty folder', () => {
const f = folder('Empty', [])
expect(getFolderPath(f)).toBeNull()
})

test('prefers direct child page over subfolder', () => {
const f = folder('Mixed', [
page('/docs/mixed/intro'),
folder('Sub', [page('/docs/mixed/sub/deep')])
])
expect(getFolderPath(f)).toBe('/docs/mixed')
})

test('deeply nested only-subfolder chain', () => {
const f = folder('Root', [
folder('Mid', [
folder('Deep', [page('/a/b/c/d/page')])
])
])
expect(getFolderPath(f)).toBe('/a/b')
})
})
29 changes: 9 additions & 20 deletions packages/chronicle/src/lib/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import path from 'node:path';
import { loader } from 'fumadocs-core/source';
import { flattenTree } from 'fumadocs-core/page-tree';
import type { Root, Node, Folder } from 'fumadocs-core/page-tree';

import { parentPath, getFolderPath } from './folder-utils';

const NodeType = {
Page: 'page',
Folder: 'folder',
} as const;
import type { MDXContent } from 'mdx/types';
import type { TableOfContents } from 'fumadocs-core/toc';
import {
Expand Down Expand Up @@ -120,28 +127,10 @@ export function invalidate() {
cachedNavMap = null;
}

function getFolderPath(node: Folder): string | null {
const firstPage = findFirstPage(node);
if (!firstPage) return null;
const parts = firstPage.url.split('/').filter(Boolean);
parts.pop();
return '/' + parts.join('/');
}

function findFirstPage(node: Folder): { url: string } | null {
for (const child of node.children) {
if (child.type === 'page') return child;
if (child.type === 'folder') {
const found = findFirstPage(child);
if (found) return found;
}
}
return node.index ?? null;
}

function getOrder(node: Node, pageOrderMap: Map<string, number>, folderOrderMap: Map<string, number>): number | undefined {
if (node.type === 'page') return pageOrderMap.get(node.url);
if (node.type === 'folder') {
if (node.type === NodeType.Page) return pageOrderMap.get(node.url);
if (node.type === NodeType.Folder) {
const folderPath = getFolderPath(node);
if (folderPath) return folderOrderMap.get(folderPath);
}
Expand Down
Loading