polen 0.10.0-next.4 → 0.10.0-next.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/build/api/config/load.js +5 -5
- package/build/api/config/load.js.map +1 -1
- package/build/api/config-resolver/resolve.js +2 -2
- package/build/api/config-resolver/resolve.js.map +1 -1
- package/build/api/content/$$.d.ts +5 -0
- package/build/api/content/$$.d.ts.map +1 -0
- package/build/api/content/$$.js +5 -0
- package/build/api/content/$$.js.map +1 -0
- package/build/api/content/$.d.ts +2 -0
- package/build/api/content/$.d.ts.map +1 -0
- package/build/api/content/$.js +2 -0
- package/build/api/content/$.js.map +1 -0
- package/build/api/content/metadata.d.ts +10 -0
- package/build/api/content/metadata.d.ts.map +1 -0
- package/build/api/content/metadata.js +9 -0
- package/build/api/content/metadata.js.map +1 -0
- package/build/api/content/page.d.ts +11 -0
- package/build/api/content/page.d.ts.map +1 -0
- package/build/api/content/page.js +2 -0
- package/build/api/content/page.js.map +1 -0
- package/build/api/content/scan.d.ts +19 -0
- package/build/api/content/scan.d.ts.map +1 -0
- package/build/api/content/scan.js +57 -0
- package/build/api/content/scan.js.map +1 -0
- package/build/{lib/file-router/sidebar/types.d.ts → api/content/sidebar.d.ts} +8 -1
- package/build/api/content/sidebar.d.ts.map +1 -0
- package/build/api/content/sidebar.js +90 -0
- package/build/api/content/sidebar.js.map +1 -0
- package/build/api/schema/data-sources/schema-directory/schema-directory.js +1 -1
- package/build/api/schema/data-sources/schema-directory/schema-directory.js.map +1 -1
- package/build/api/vite/plugins/branding/index.js +4 -4
- package/build/api/vite/plugins/branding/index.js.map +1 -1
- package/build/api/vite/plugins/core.js +4 -4
- package/build/api/vite/plugins/core.js.map +1 -1
- package/build/api/vite/plugins/pages.d.ts +6 -8
- package/build/api/vite/plugins/pages.d.ts.map +1 -1
- package/build/api/vite/plugins/pages.js +99 -155
- package/build/api/vite/plugins/pages.js.map +1 -1
- package/build/api/vite/plugins/serve.js +5 -5
- package/build/api/vite/plugins/serve.js.map +1 -1
- package/build/cli/_/self-contained-mode.js +5 -5
- package/build/cli/_/self-contained-mode.js.map +1 -1
- package/build/exports/components.d.ts +2 -0
- package/build/exports/components.d.ts.map +1 -0
- package/build/exports/components.js +2 -0
- package/build/exports/components.js.map +1 -0
- package/build/lib/demos/config-schema.d.ts +14 -14
- package/build/lib/file-router/file-router.d.ts +0 -2
- package/build/lib/file-router/file-router.d.ts.map +1 -1
- package/build/lib/file-router/file-router.js +0 -2
- package/build/lib/file-router/file-router.js.map +1 -1
- package/build/lib/file-router/route.d.ts +2 -0
- package/build/lib/file-router/route.d.ts.map +1 -1
- package/build/lib/file-router/route.js.map +1 -1
- package/build/lib/file-router/scan.d.ts.map +1 -1
- package/build/lib/file-router/scan.js +16 -12
- package/build/lib/file-router/scan.js.map +1 -1
- package/build/singletons/debug.d.ts +1 -1
- package/build/singletons/debug.d.ts.map +1 -1
- package/build/singletons/debug.js +1 -1
- package/build/singletons/debug.js.map +1 -1
- package/build/template/components/content/$$.d.ts +2 -0
- package/build/template/components/content/$$.d.ts.map +1 -0
- package/build/template/components/content/$$.js +2 -0
- package/build/template/components/content/$$.js.map +1 -0
- package/build/template/components/sidebar/Sidebar.d.ts +2 -2
- package/build/template/components/sidebar/Sidebar.d.ts.map +1 -1
- package/build/template/components/sidebar/SidebarItem.d.ts +3 -3
- package/build/template/components/sidebar/SidebarItem.d.ts.map +1 -1
- package/build/template/components/sidebar/SidebarItem.jsx +1 -1
- package/build/template/components/sidebar/SidebarItem.jsx.map +1 -1
- package/package.json +10 -3
- package/src/api/config/load.ts +5 -5
- package/src/api/config-resolver/resolve.ts +2 -2
- package/src/api/content/$$.ts +4 -0
- package/src/api/content/$.test.ts +72 -0
- package/src/api/content/$.ts +1 -0
- package/src/api/content/metadata.ts +11 -0
- package/src/api/content/page.ts +12 -0
- package/src/api/content/scan.ts +82 -0
- package/src/api/content/sidebar.ts +136 -0
- package/src/api/schema/data-sources/schema-directory/schema-directory.ts +1 -1
- package/src/api/vite/plugins/branding/index.ts +4 -4
- package/src/api/vite/plugins/core.ts +4 -4
- package/src/api/vite/plugins/pages.ts +117 -171
- package/src/api/vite/plugins/serve.ts +5 -5
- package/src/cli/_/self-contained-mode.ts +5 -5
- package/src/exports/components.ts +1 -0
- package/src/lib/deployment/$$.ts +1 -1
- package/src/lib/deployment/$.test.ts +3 -3
- package/src/lib/deployment/$.ts +1 -1
- package/src/lib/file-router/file-router.ts +0 -2
- package/src/lib/file-router/linter.test.ts +2 -0
- package/src/lib/file-router/route.ts +2 -0
- package/src/lib/file-router/scan.ts +19 -13
- package/src/lib/task/$.test.ts +3 -3
- package/src/singletons/debug.ts +1 -1
- package/src/template/components/content/$$.ts +1 -0
- package/src/template/components/sidebar/Sidebar.tsx +2 -2
- package/src/template/components/sidebar/SidebarItem.tsx +8 -8
- package/build/lib/file-router/scan-tree.d.ts +0 -20
- package/build/lib/file-router/scan-tree.d.ts.map +0 -1
- package/build/lib/file-router/scan-tree.js +0 -158
- package/build/lib/file-router/scan-tree.js.map +0 -1
- package/build/lib/file-router/sidebar/index.d.ts +0 -3
- package/build/lib/file-router/sidebar/index.d.ts.map +0 -1
- package/build/lib/file-router/sidebar/index.js +0 -4
- package/build/lib/file-router/sidebar/index.js.map +0 -1
- package/build/lib/file-router/sidebar/sidebar-tree.d.ts +0 -9
- package/build/lib/file-router/sidebar/sidebar-tree.d.ts.map +0 -1
- package/build/lib/file-router/sidebar/sidebar-tree.js +0 -85
- package/build/lib/file-router/sidebar/sidebar-tree.js.map +0 -1
- package/build/lib/file-router/sidebar/types.d.ts.map +0 -1
- package/build/lib/file-router/sidebar/types.js +0 -2
- package/build/lib/file-router/sidebar/types.js.map +0 -1
- package/build/lib/tree/index.d.ts +0 -3
- package/build/lib/tree/index.d.ts.map +0 -1
- package/build/lib/tree/index.js +0 -2
- package/build/lib/tree/index.js.map +0 -1
- package/build/lib/tree/tree.d.ts +0 -62
- package/build/lib/tree/tree.d.ts.map +0 -1
- package/build/lib/tree/tree.js +0 -134
- package/build/lib/tree/tree.js.map +0 -1
- package/src/lib/file-router/scan-tree.test.ts +0 -189
- package/src/lib/file-router/scan-tree.ts +0 -205
- package/src/lib/file-router/sidebar/index.ts +0 -3
- package/src/lib/file-router/sidebar/sidebar-tree.test.ts +0 -123
- package/src/lib/file-router/sidebar/sidebar-tree.ts +0 -110
- package/src/lib/file-router/sidebar/types.ts +0 -19
- package/src/lib/tree/index.ts +0 -2
- package/src/lib/tree/tree.test.ts +0 -117
- package/src/lib/tree/tree.ts +0 -183
@@ -1,110 +0,0 @@
|
|
1
|
-
import { Tree } from '#lib/tree/index'
|
2
|
-
import { Str } from '@wollybeard/kit'
|
3
|
-
import * as FileRouter from '../file-router.ts'
|
4
|
-
import type { RouteTreeNode } from '../scan-tree.ts'
|
5
|
-
import type { ItemLink, ItemSection, Sidebar } from './types.ts'
|
6
|
-
|
7
|
-
export * from './types.ts'
|
8
|
-
|
9
|
-
/**
|
10
|
-
* Build sidebar from tree structure
|
11
|
-
*/
|
12
|
-
export const buildFromTree = (routeTree: RouteTreeNode, basePath: FileRouter.Path): Sidebar => {
|
13
|
-
const links: ItemLink[] = []
|
14
|
-
const sections: ItemSection[] = []
|
15
|
-
|
16
|
-
// Process only the children of the root node
|
17
|
-
for (const child of routeTree.children) {
|
18
|
-
processNode(child, basePath, [], links, sections)
|
19
|
-
}
|
20
|
-
|
21
|
-
const items = [...links, ...sections]
|
22
|
-
|
23
|
-
return {
|
24
|
-
items,
|
25
|
-
}
|
26
|
-
}
|
27
|
-
|
28
|
-
const processNode = (
|
29
|
-
node: RouteTreeNode,
|
30
|
-
basePath: FileRouter.Path,
|
31
|
-
parentPath: string[],
|
32
|
-
links: ItemLink[],
|
33
|
-
sections: ItemSection[],
|
34
|
-
): void => {
|
35
|
-
const currentPath = [...parentPath, node.value.name]
|
36
|
-
|
37
|
-
if (node.value.type === 'directory') {
|
38
|
-
// This is a directory - create a section
|
39
|
-
const sectionPath = [...basePath, ...currentPath]
|
40
|
-
const sectionPathExp = FileRouter.pathToExpression(sectionPath)
|
41
|
-
const sectionTitle = Str.titlizeSlug(node.value.name)
|
42
|
-
|
43
|
-
const section: ItemSection = {
|
44
|
-
type: `ItemSection`,
|
45
|
-
title: sectionTitle,
|
46
|
-
pathExp: sectionPathExp.startsWith('/') ? sectionPathExp.slice(1) : sectionPathExp,
|
47
|
-
isLinkToo: false,
|
48
|
-
links: [],
|
49
|
-
}
|
50
|
-
|
51
|
-
// Check if this directory has an index file
|
52
|
-
const indexChild = node.children.find(child => child.value.type === 'file' && child.value.name === 'index')
|
53
|
-
if (indexChild) {
|
54
|
-
section.isLinkToo = true
|
55
|
-
}
|
56
|
-
|
57
|
-
// Process all non-index children as links for this section
|
58
|
-
for (const child of node.children) {
|
59
|
-
if (child.value.type === 'file' && child.value.name !== 'index' && child.value.route) {
|
60
|
-
// Pass the parent path of the route, not the section path
|
61
|
-
const routeParentPath = child.value.route.logical.path.slice(0, -1)
|
62
|
-
section.links.push(routeToItemLink(child.value.route, routeParentPath))
|
63
|
-
} else if (child.value.type === 'directory') {
|
64
|
-
// Recursively process subdirectories
|
65
|
-
// Note: This creates nested sections which the original implementation doesn't support
|
66
|
-
// For now, we'll just add the files from subdirectories to the parent section
|
67
|
-
collectFilesFromDirectory(child, child.value.route?.logical.path || [], section.links)
|
68
|
-
}
|
69
|
-
}
|
70
|
-
|
71
|
-
sections.push(section)
|
72
|
-
} else if (node.value.type === 'file' && node.value.route) {
|
73
|
-
// This is a top-level file - add as nav
|
74
|
-
if (node.value.name !== 'index') {
|
75
|
-
links.push(routeToItemLink(node.value.route, basePath))
|
76
|
-
}
|
77
|
-
}
|
78
|
-
}
|
79
|
-
|
80
|
-
const collectFilesFromDirectory = (
|
81
|
-
node: RouteTreeNode,
|
82
|
-
basePath: FileRouter.Path,
|
83
|
-
links: ItemLink[],
|
84
|
-
): void => {
|
85
|
-
Tree.visit(node, (n) => {
|
86
|
-
if (n.value.type === 'file' && n.value.route && n.value.name !== 'index') {
|
87
|
-
// Use the route's parent path for relative title generation
|
88
|
-
const routeParentPath = n.value.route.logical.path.slice(0, -1)
|
89
|
-
links.push(routeToItemLink(n.value.route, routeParentPath))
|
90
|
-
}
|
91
|
-
})
|
92
|
-
}
|
93
|
-
|
94
|
-
const routeToItemLink = (route: FileRouter.Route, basePath: FileRouter.Path): ItemLink => {
|
95
|
-
const pagePathExp = FileRouter.routeToPathExpression(route)
|
96
|
-
const pageRelative = FileRouter.makeRelativeUnsafe(route, basePath)
|
97
|
-
const pageRelativePathExp = FileRouter.routeToPathExpression(pageRelative)
|
98
|
-
|
99
|
-
// Remove leading slash for title generation
|
100
|
-
const titlePath = pageRelativePathExp.startsWith('/') ? pageRelativePathExp.slice(1) : pageRelativePathExp
|
101
|
-
|
102
|
-
// Use only the last segment for the title
|
103
|
-
const titleSegment = pageRelative.logical.path[pageRelative.logical.path.length - 1] || titlePath
|
104
|
-
|
105
|
-
return {
|
106
|
-
type: `ItemLink`,
|
107
|
-
pathExp: pagePathExp.startsWith('/') ? pagePathExp.slice(1) : pagePathExp,
|
108
|
-
title: Str.titlizeSlug(titleSegment),
|
109
|
-
}
|
110
|
-
}
|
@@ -1,19 +0,0 @@
|
|
1
|
-
export interface Sidebar {
|
2
|
-
items: Item[]
|
3
|
-
}
|
4
|
-
|
5
|
-
export type Item = ItemLink | ItemSection
|
6
|
-
|
7
|
-
export interface ItemLink {
|
8
|
-
type: `ItemLink`
|
9
|
-
title: string
|
10
|
-
pathExp: string
|
11
|
-
}
|
12
|
-
|
13
|
-
export interface ItemSection {
|
14
|
-
type: `ItemSection`
|
15
|
-
title: string
|
16
|
-
pathExp: string
|
17
|
-
isLinkToo: boolean
|
18
|
-
links: ItemLink[]
|
19
|
-
}
|
package/src/lib/tree/index.ts
DELETED
@@ -1,117 +0,0 @@
|
|
1
|
-
import { describe, expect, test } from 'vitest'
|
2
|
-
import * as Tree from './tree.ts'
|
3
|
-
|
4
|
-
describe('Tree', () => {
|
5
|
-
const sampleTree = Tree.node('root', [
|
6
|
-
Tree.node('a', [
|
7
|
-
Tree.node('a1'),
|
8
|
-
Tree.node('a2'),
|
9
|
-
]),
|
10
|
-
Tree.node('b', [
|
11
|
-
Tree.node('b1'),
|
12
|
-
]),
|
13
|
-
Tree.node('c'),
|
14
|
-
])
|
15
|
-
|
16
|
-
test('node creates a tree node', () => {
|
17
|
-
const leaf = Tree.node('leaf')
|
18
|
-
expect(leaf).toEqual({ value: 'leaf', children: [] })
|
19
|
-
|
20
|
-
const parent = Tree.node('parent', [leaf])
|
21
|
-
expect(parent).toEqual({ value: 'parent', children: [leaf] })
|
22
|
-
})
|
23
|
-
|
24
|
-
test('map transforms node values', () => {
|
25
|
-
const upperTree = Tree.map(sampleTree, value => value.toUpperCase())
|
26
|
-
|
27
|
-
expect(upperTree.value).toBe('ROOT')
|
28
|
-
expect(upperTree.children[0]!.value).toBe('A')
|
29
|
-
expect(upperTree.children[0]!.children[0]!.value).toBe('A1')
|
30
|
-
})
|
31
|
-
|
32
|
-
test('map provides depth and path', () => {
|
33
|
-
const depths: number[] = []
|
34
|
-
const paths: string[][] = []
|
35
|
-
|
36
|
-
Tree.map(sampleTree, (value, depth, path) => {
|
37
|
-
depths.push(depth)
|
38
|
-
paths.push(path)
|
39
|
-
return value
|
40
|
-
})
|
41
|
-
|
42
|
-
expect(depths).toEqual([0, 1, 2, 2, 1, 2, 1])
|
43
|
-
expect(paths[0]).toEqual([])
|
44
|
-
expect(paths[1]).toEqual(['root'])
|
45
|
-
expect(paths[2]).toEqual(['root', 'a'])
|
46
|
-
})
|
47
|
-
|
48
|
-
test('visit traverses all nodes', () => {
|
49
|
-
const visited: string[] = []
|
50
|
-
Tree.visit(sampleTree, node => visited.push(node.value))
|
51
|
-
|
52
|
-
expect(visited).toEqual(['root', 'a', 'a1', 'a2', 'b', 'b1', 'c'])
|
53
|
-
})
|
54
|
-
|
55
|
-
test('find locates node', () => {
|
56
|
-
const found = Tree.find(sampleTree, value => value === 'b1')
|
57
|
-
expect(found?.value).toBe('b1')
|
58
|
-
|
59
|
-
const notFound = Tree.find(sampleTree, value => value === 'x')
|
60
|
-
expect(notFound).toBeUndefined()
|
61
|
-
})
|
62
|
-
|
63
|
-
test('filter removes non-matching nodes', () => {
|
64
|
-
const filtered = Tree.filter(sampleTree, value => !value.includes('2'))
|
65
|
-
|
66
|
-
expect(filtered).toBeDefined()
|
67
|
-
expect(Tree.flatten(filtered!)).toEqual(['root', 'a', 'a1', 'b', 'b1', 'c'])
|
68
|
-
})
|
69
|
-
|
70
|
-
test('sort orders children', () => {
|
71
|
-
const sorted = Tree.sort(sampleTree, (a, b) => b.localeCompare(a))
|
72
|
-
|
73
|
-
expect(sorted.children.map(c => c.value)).toEqual(['c', 'b', 'a'])
|
74
|
-
expect(sorted.children[2]!.children.map(c => c.value)).toEqual(['a2', 'a1'])
|
75
|
-
})
|
76
|
-
|
77
|
-
test('flatten returns all values', () => {
|
78
|
-
const flat = Tree.flatten(sampleTree)
|
79
|
-
expect(flat).toEqual(['root', 'a', 'a1', 'a2', 'b', 'b1', 'c'])
|
80
|
-
})
|
81
|
-
|
82
|
-
test('depth calculates tree depth', () => {
|
83
|
-
expect(Tree.depth(Tree.node('single'))).toBe(0)
|
84
|
-
expect(Tree.depth(sampleTree)).toBe(2)
|
85
|
-
})
|
86
|
-
|
87
|
-
test('count counts all nodes', () => {
|
88
|
-
expect(Tree.count(Tree.node('single'))).toBe(1)
|
89
|
-
expect(Tree.count(sampleTree)).toBe(7)
|
90
|
-
})
|
91
|
-
|
92
|
-
test('isLeaf identifies leaf nodes', () => {
|
93
|
-
expect(Tree.isLeaf(sampleTree)).toBe(false)
|
94
|
-
expect(Tree.isLeaf(sampleTree.children[0]!)).toBe(false)
|
95
|
-
expect(Tree.isLeaf(sampleTree.children[0]!.children[0]!)).toBe(true)
|
96
|
-
})
|
97
|
-
|
98
|
-
test('leaves gets all leaf nodes', () => {
|
99
|
-
const leafNodes = Tree.leaves(sampleTree)
|
100
|
-
expect(leafNodes.map(n => n.value)).toEqual(['a1', 'a2', 'b1', 'c'])
|
101
|
-
})
|
102
|
-
|
103
|
-
test('fromList builds tree from flat list', () => {
|
104
|
-
const items = [
|
105
|
-
{ id: '1', name: 'root' },
|
106
|
-
{ id: '2', parentId: '1', name: 'child1' },
|
107
|
-
{ id: '3', parentId: '1', name: 'child2' },
|
108
|
-
{ id: '4', parentId: '2', name: 'grandchild' },
|
109
|
-
]
|
110
|
-
|
111
|
-
const trees = Tree.fromList(items, undefined)
|
112
|
-
expect(trees).toHaveLength(1)
|
113
|
-
expect(trees[0]!.value.name).toBe('root')
|
114
|
-
expect(trees[0]!.children).toHaveLength(2)
|
115
|
-
expect(trees[0]!.children[0]!.children[0]!.value.name).toBe('grandchild')
|
116
|
-
})
|
117
|
-
})
|
package/src/lib/tree/tree.ts
DELETED
@@ -1,183 +0,0 @@
|
|
1
|
-
/**
|
2
|
-
* Generic tree data structure and utilities
|
3
|
-
*/
|
4
|
-
|
5
|
-
export interface TreeNode<T> {
|
6
|
-
value: T
|
7
|
-
children: TreeNode<T>[]
|
8
|
-
}
|
9
|
-
|
10
|
-
export type TreeVisitor<T, R = void> = (node: TreeNode<T>, depth: number, path: T[]) => R
|
11
|
-
|
12
|
-
export type TreeMapper<T, U> = (value: T, depth: number, path: T[]) => U
|
13
|
-
|
14
|
-
export type TreePredicate<T> = (value: T, depth: number, path: T[]) => boolean
|
15
|
-
|
16
|
-
/**
|
17
|
-
* Create a new tree node
|
18
|
-
*/
|
19
|
-
export const node = <T>(value: T, children: TreeNode<T>[] = []): TreeNode<T> => ({
|
20
|
-
value,
|
21
|
-
children,
|
22
|
-
})
|
23
|
-
|
24
|
-
/**
|
25
|
-
* Map over a tree, transforming each node's value
|
26
|
-
*/
|
27
|
-
export const map = <T, U>(
|
28
|
-
tree: TreeNode<T>,
|
29
|
-
mapper: TreeMapper<T, U>,
|
30
|
-
depth = 0,
|
31
|
-
path: T[] = [],
|
32
|
-
): TreeNode<U> => {
|
33
|
-
const newPath = [...path, tree.value]
|
34
|
-
return {
|
35
|
-
value: mapper(tree.value, depth, path),
|
36
|
-
children: tree.children.map(child => map(child, mapper, depth + 1, newPath)),
|
37
|
-
}
|
38
|
-
}
|
39
|
-
|
40
|
-
/**
|
41
|
-
* Visit each node in the tree (depth-first)
|
42
|
-
*/
|
43
|
-
export const visit = <T>(
|
44
|
-
tree: TreeNode<T>,
|
45
|
-
visitor: TreeVisitor<T>,
|
46
|
-
depth = 0,
|
47
|
-
path: T[] = [],
|
48
|
-
): void => {
|
49
|
-
visitor(tree, depth, path)
|
50
|
-
const newPath = [...path, tree.value]
|
51
|
-
tree.children.forEach(child => visit(child, visitor, depth + 1, newPath))
|
52
|
-
}
|
53
|
-
|
54
|
-
/**
|
55
|
-
* Find a node in the tree
|
56
|
-
*/
|
57
|
-
export const find = <T>(
|
58
|
-
tree: TreeNode<T>,
|
59
|
-
predicate: TreePredicate<T>,
|
60
|
-
depth = 0,
|
61
|
-
path: T[] = [],
|
62
|
-
): TreeNode<T> | undefined => {
|
63
|
-
if (predicate(tree.value, depth, path)) {
|
64
|
-
return tree
|
65
|
-
}
|
66
|
-
const newPath = [...path, tree.value]
|
67
|
-
for (const child of tree.children) {
|
68
|
-
const found = find(child, predicate, depth + 1, newPath)
|
69
|
-
if (found) return found
|
70
|
-
}
|
71
|
-
return undefined
|
72
|
-
}
|
73
|
-
|
74
|
-
/**
|
75
|
-
* Filter tree nodes (keeps structure, removes non-matching nodes)
|
76
|
-
*/
|
77
|
-
export const filter = <T>(
|
78
|
-
tree: TreeNode<T>,
|
79
|
-
predicate: TreePredicate<T>,
|
80
|
-
depth = 0,
|
81
|
-
path: T[] = [],
|
82
|
-
): TreeNode<T> | undefined => {
|
83
|
-
const newPath = [...path, tree.value]
|
84
|
-
const filteredChildren = tree.children
|
85
|
-
.map(child => filter(child, predicate, depth + 1, newPath))
|
86
|
-
.filter((child): child is TreeNode<T> => child !== undefined)
|
87
|
-
|
88
|
-
// Keep node if it matches or has matching children
|
89
|
-
if (predicate(tree.value, depth, path) || filteredChildren.length > 0) {
|
90
|
-
return {
|
91
|
-
value: tree.value,
|
92
|
-
children: filteredChildren,
|
93
|
-
}
|
94
|
-
}
|
95
|
-
|
96
|
-
return undefined
|
97
|
-
}
|
98
|
-
|
99
|
-
/**
|
100
|
-
* Sort a tree's children at each level
|
101
|
-
*/
|
102
|
-
export const sort = <T>(
|
103
|
-
tree: TreeNode<T>,
|
104
|
-
compareFn: (a: T, b: T) => number,
|
105
|
-
): TreeNode<T> => ({
|
106
|
-
value: tree.value,
|
107
|
-
children: tree.children
|
108
|
-
.map(child => sort(child, compareFn))
|
109
|
-
.sort((a, b) => compareFn(a.value, b.value)),
|
110
|
-
})
|
111
|
-
|
112
|
-
/**
|
113
|
-
* Flatten a tree into an array (depth-first)
|
114
|
-
*/
|
115
|
-
export const flatten = <T>(tree: TreeNode<T>): T[] => {
|
116
|
-
const result: T[] = [tree.value]
|
117
|
-
tree.children.forEach(child => {
|
118
|
-
result.push(...flatten(child))
|
119
|
-
})
|
120
|
-
return result
|
121
|
-
}
|
122
|
-
|
123
|
-
/**
|
124
|
-
* Get the depth of the tree
|
125
|
-
*/
|
126
|
-
export const depth = <T>(tree: TreeNode<T>): number => {
|
127
|
-
if (tree.children.length === 0) return 0
|
128
|
-
return 1 + Math.max(...tree.children.map(depth))
|
129
|
-
}
|
130
|
-
|
131
|
-
/**
|
132
|
-
* Count total nodes in the tree
|
133
|
-
*/
|
134
|
-
export const count = <T>(tree: TreeNode<T>): number => {
|
135
|
-
return 1 + tree.children.reduce((sum, child) => sum + count(child), 0)
|
136
|
-
}
|
137
|
-
|
138
|
-
/**
|
139
|
-
* Check if a node is a leaf (has no children)
|
140
|
-
*/
|
141
|
-
export const isLeaf = <T>(node: TreeNode<T>): boolean => {
|
142
|
-
return node.children.length === 0
|
143
|
-
}
|
144
|
-
|
145
|
-
/**
|
146
|
-
* Get all leaf nodes
|
147
|
-
*/
|
148
|
-
export const leaves = <T>(tree: TreeNode<T>): TreeNode<T>[] => {
|
149
|
-
if (isLeaf(tree)) return [tree]
|
150
|
-
return tree.children.flatMap(leaves)
|
151
|
-
}
|
152
|
-
|
153
|
-
/**
|
154
|
-
* Build a tree from a flat list with parent references
|
155
|
-
*/
|
156
|
-
export const fromList = <T extends { id: string; parentId?: string }>(
|
157
|
-
items: T[],
|
158
|
-
rootId?: string,
|
159
|
-
): TreeNode<T>[] => {
|
160
|
-
const itemMap = new Map(items.map(item => [item.id, item]))
|
161
|
-
const roots: TreeNode<T>[] = []
|
162
|
-
const nodeMap = new Map<string, TreeNode<T>>()
|
163
|
-
|
164
|
-
// Create all nodes
|
165
|
-
items.forEach(item => {
|
166
|
-
nodeMap.set(item.id, node(item))
|
167
|
-
})
|
168
|
-
|
169
|
-
// Build hierarchy
|
170
|
-
items.forEach(item => {
|
171
|
-
const itemNode = nodeMap.get(item.id)!
|
172
|
-
if (item.parentId === rootId) {
|
173
|
-
roots.push(itemNode)
|
174
|
-
} else if (item.parentId) {
|
175
|
-
const parent = nodeMap.get(item.parentId)
|
176
|
-
if (parent) {
|
177
|
-
parent.children.push(itemNode)
|
178
|
-
}
|
179
|
-
}
|
180
|
-
})
|
181
|
-
|
182
|
-
return roots
|
183
|
-
}
|