fix(explorer): dont rely on data to get slug, compute it in trie
This commit is contained in:
		
							parent
							
								
									9c8fec06d2
								
							
						
					
					
						commit
						d618a4e3f3
					
				| @ -23,7 +23,7 @@ export interface Options { | ||||
| 
 | ||||
| const defaultOptions: Options = { | ||||
|   folderDefaultState: "collapsed", | ||||
|   folderClickBehavior: "collapse", | ||||
|   folderClickBehavior: "link", | ||||
|   useSavedState: true, | ||||
|   mapFn: (node) => { | ||||
|     return node | ||||
|  | ||||
| @ -78,11 +78,11 @@ function createFileNode(currentSlug: FullSlug, node: FileTrieNode): HTMLLIElemen | ||||
|   const clone = template.content.cloneNode(true) as DocumentFragment | ||||
|   const li = clone.querySelector("li") as HTMLLIElement | ||||
|   const a = li.querySelector("a") as HTMLAnchorElement | ||||
|   a.href = resolveRelative(currentSlug, node.data?.slug!) | ||||
|   a.dataset.for = node.data?.slug | ||||
|   a.href = resolveRelative(currentSlug, node.slug) | ||||
|   a.dataset.for = node.slug | ||||
|   a.textContent = node.displayName | ||||
| 
 | ||||
|   if (currentSlug === node.data?.slug) { | ||||
|   if (currentSlug === node.slug) { | ||||
|     a.classList.add("active") | ||||
|   } | ||||
| 
 | ||||
| @ -102,7 +102,7 @@ function createFolderNode( | ||||
|   const folderOuter = li.querySelector(".folder-outer") as HTMLElement | ||||
|   const ul = folderOuter.querySelector("ul") as HTMLUListElement | ||||
| 
 | ||||
|   const folderPath = node.data?.slug! | ||||
|   const folderPath = node.slug | ||||
|   folderContainer.dataset.folderpath = folderPath | ||||
| 
 | ||||
|   if (opts.folderClickBehavior === "link") { | ||||
| @ -110,7 +110,7 @@ function createFolderNode( | ||||
|     const button = titleContainer.querySelector(".folder-button") as HTMLElement | ||||
|     const a = document.createElement("a") | ||||
|     a.href = resolveRelative(currentSlug, folderPath) | ||||
|     a.dataset.for = node.data?.slug | ||||
|     a.dataset.for = folderPath | ||||
|     a.className = "folder-title" | ||||
|     a.textContent = node.displayName | ||||
|     button.replaceWith(a) | ||||
|  | ||||
| @ -11,16 +11,15 @@ describe("FileTrie", () => { | ||||
|   let trie: FileTrieNode<TestData> | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     trie = new FileTrieNode<TestData>("") | ||||
|     trie = new FileTrieNode<TestData>([]) | ||||
|   }) | ||||
| 
 | ||||
|   describe("constructor", () => { | ||||
|     test("should create an empty trie", () => { | ||||
|       assert.deepStrictEqual(trie.children, []) | ||||
|       assert.strictEqual(trie.slugSegment, "") | ||||
|       assert.strictEqual(trie.slug, "") | ||||
|       assert.strictEqual(trie.displayName, "") | ||||
|       assert.strictEqual(trie.data, null) | ||||
|       assert.strictEqual(trie.depth, 0) | ||||
|     }) | ||||
| 
 | ||||
|     test("should set displayName from data title", () => { | ||||
| @ -43,7 +42,7 @@ describe("FileTrie", () => { | ||||
| 
 | ||||
|       trie.add(data) | ||||
|       assert.strictEqual(trie.children.length, 1) | ||||
|       assert.strictEqual(trie.children[0].slugSegment, "test") | ||||
|       assert.strictEqual(trie.children[0].slug, "test") | ||||
|       assert.strictEqual(trie.children[0].data, data) | ||||
|     }) | ||||
| 
 | ||||
| @ -72,20 +71,20 @@ describe("FileTrie", () => { | ||||
|       trie.add(data1) | ||||
|       trie.add(data2) | ||||
|       assert.strictEqual(trie.children.length, 2) | ||||
|       assert.strictEqual(trie.children[0].slugSegment, "folder") | ||||
|       assert.strictEqual(trie.children[0].slug, "folder/index") | ||||
|       assert.strictEqual(trie.children[0].children.length, 1) | ||||
|       assert.strictEqual(trie.children[0].children[0].slugSegment, "test") | ||||
|       assert.strictEqual(trie.children[0].children[0].slug, "folder/test") | ||||
|       assert.strictEqual(trie.children[0].children[0].data, data1) | ||||
| 
 | ||||
|       assert.strictEqual(trie.children[1].slugSegment, "a") | ||||
|       assert.strictEqual(trie.children[1].slug, "a/index") | ||||
|       assert.strictEqual(trie.children[1].children.length, 1) | ||||
|       assert.strictEqual(trie.children[1].data, null) | ||||
| 
 | ||||
|       assert.strictEqual(trie.children[1].children[0].slugSegment, "b") | ||||
|       assert.strictEqual(trie.children[1].children[0].slug, "a/b/index") | ||||
|       assert.strictEqual(trie.children[1].children[0].children.length, 1) | ||||
|       assert.strictEqual(trie.children[1].children[0].data, null) | ||||
| 
 | ||||
|       assert.strictEqual(trie.children[1].children[0].children[0].slugSegment, "c") | ||||
|       assert.strictEqual(trie.children[1].children[0].children[0].slug, "a/b/c/index") | ||||
|       assert.strictEqual(trie.children[1].children[0].children[0].data, data2) | ||||
|       assert.strictEqual(trie.children[1].children[0].children[0].children.length, 0) | ||||
|     }) | ||||
| @ -99,9 +98,9 @@ describe("FileTrie", () => { | ||||
|       trie.add(data1) | ||||
|       trie.add(data2) | ||||
| 
 | ||||
|       trie.filter((node) => node.slugSegment !== "test1") | ||||
|       trie.filter((node) => node.slug !== "test1") | ||||
|       assert.strictEqual(trie.children.length, 1) | ||||
|       assert.strictEqual(trie.children[0].slugSegment, "test2") | ||||
|       assert.strictEqual(trie.children[0].slug, "test2") | ||||
|     }) | ||||
|   }) | ||||
| 
 | ||||
| @ -115,7 +114,7 @@ describe("FileTrie", () => { | ||||
| 
 | ||||
|       trie.map((node) => { | ||||
|         if (node.data) { | ||||
|           node.displayName = "Modified" | ||||
|           node.data.title = "Modified" | ||||
|         } | ||||
|       }) | ||||
| 
 | ||||
| @ -136,7 +135,7 @@ describe("FileTrie", () => { | ||||
|       assert.deepStrictEqual( | ||||
|         entries.map(([path, node]) => [path, node.data]), | ||||
|         [ | ||||
|           ["", trie.data], | ||||
|           ["index", trie.data], | ||||
|           ["test1", data1], | ||||
|           ["a/index", null], | ||||
|           ["a/b/index", null], | ||||
| @ -166,7 +165,12 @@ describe("FileTrie", () => { | ||||
|       trie.add(data3) | ||||
|       const paths = trie.getFolderPaths() | ||||
| 
 | ||||
|       assert.deepStrictEqual(paths, ["folder/index", "folder/subfolder/index", "abc/index"]) | ||||
|       assert.deepStrictEqual(paths, [ | ||||
|         "index", | ||||
|         "folder/index", | ||||
|         "folder/subfolder/index", | ||||
|         "abc/index", | ||||
|       ]) | ||||
|     }) | ||||
|   }) | ||||
| 
 | ||||
| @ -180,9 +184,9 @@ describe("FileTrie", () => { | ||||
|       trie.add(data1) | ||||
|       trie.add(data2) | ||||
| 
 | ||||
|       trie.sort((a, b) => a.slugSegment.localeCompare(b.slugSegment)) | ||||
|       trie.sort((a, b) => a.slug.localeCompare(b.slug)) | ||||
|       assert.deepStrictEqual( | ||||
|         trie.children.map((n) => n.slugSegment), | ||||
|         trie.children.map((n) => n.slug), | ||||
|         ["a", "b", "c"], | ||||
|       ) | ||||
|     }) | ||||
|  | ||||
| @ -7,55 +7,64 @@ interface FileTrieData { | ||||
| } | ||||
| 
 | ||||
| export class FileTrieNode<T extends FileTrieData = ContentDetails> { | ||||
|   children: Array<FileTrieNode<T>> | ||||
|   slugSegment: string | ||||
|   displayName: string | ||||
|   data: T | null | ||||
|   depth: number | ||||
|   isFolder: boolean | ||||
|   children: Array<FileTrieNode<T>> | ||||
| 
 | ||||
|   constructor(segment: string, data?: T, depth: number = 0) { | ||||
|   private slugSegments: string[] | ||||
|   data: T | null | ||||
| 
 | ||||
|   constructor(segments: string[], data?: T) { | ||||
|     this.children = [] | ||||
|     this.slugSegment = segment | ||||
|     this.displayName = data?.title ?? segment | ||||
|     this.slugSegments = segments | ||||
|     this.data = data ?? null | ||||
|     this.depth = depth | ||||
|     this.isFolder = segment === "index" | ||||
|     this.isFolder = false | ||||
|   } | ||||
| 
 | ||||
|   get displayName(): string { | ||||
|     return this.data?.title ?? this.slugSegment ?? "" | ||||
|   } | ||||
| 
 | ||||
|   get slug(): FullSlug { | ||||
|     const path = joinSegments(...this.slugSegments) as FullSlug | ||||
|     if (this.isFolder) { | ||||
|       return joinSegments(path, "index") as FullSlug | ||||
|     } | ||||
| 
 | ||||
|     return path | ||||
|   } | ||||
| 
 | ||||
|   get slugSegment(): string { | ||||
|     return this.slugSegments[this.slugSegments.length - 1] | ||||
|   } | ||||
| 
 | ||||
|   private makeChild(path: string[], file?: T) { | ||||
|     const fullPath = [...this.slugSegments, path[0]] | ||||
|     const child = new FileTrieNode<T>(fullPath, file) | ||||
|     this.children.push(child) | ||||
|     return child | ||||
|   } | ||||
| 
 | ||||
|   private insert(path: string[], file: T) { | ||||
|     if (path.length === 0) return | ||||
|     if (path.length === 0) { | ||||
|       throw new Error("path is empty") | ||||
|     } | ||||
| 
 | ||||
|     const nextSegment = path[0] | ||||
| 
 | ||||
|     // base case, insert here
 | ||||
|     // if we are inserting, we are a folder
 | ||||
|     this.isFolder = true | ||||
|     const segment = path[0] | ||||
|     if (path.length === 1) { | ||||
|       if (nextSegment === "index") { | ||||
|         // index case (we are the root and we just found index.md)
 | ||||
|       // base case, we are at the end of the path
 | ||||
|       if (segment === "index") { | ||||
|         this.data ??= file | ||||
|         const title = file.title | ||||
|         if (title !== "index") { | ||||
|           this.displayName = title | ||||
|         } | ||||
|       } else { | ||||
|         // direct child
 | ||||
|         this.children.push(new FileTrieNode(nextSegment, file, this.depth + 1)) | ||||
|         this.isFolder = true | ||||
|         this.makeChild(path, file) | ||||
|       } | ||||
| 
 | ||||
|       return | ||||
|     } else if (path.length > 1) { | ||||
|       // recursive case, we are not at the end of the path
 | ||||
|       const child = | ||||
|         this.children.find((c) => c.slugSegment === segment) ?? this.makeChild(path, undefined) | ||||
|       child.insert(path.slice(1), file) | ||||
|     } | ||||
| 
 | ||||
|     // find the right child to insert into, creating it if it doesn't exist
 | ||||
|     path = path.splice(1) | ||||
|     let child = this.children.find((c) => c.slugSegment === nextSegment) | ||||
|     if (!child) { | ||||
|       child = new FileTrieNode<T>(nextSegment, undefined, this.depth + 1) | ||||
|       this.children.push(child) | ||||
|       child.isFolder = true | ||||
|     } | ||||
| 
 | ||||
|     child.insert(path, file) | ||||
|   } | ||||
| 
 | ||||
|   // Add new file to trie
 | ||||
| @ -88,7 +97,7 @@ export class FileTrieNode<T extends FileTrieData = ContentDetails> { | ||||
|   } | ||||
| 
 | ||||
|   static fromEntries<T extends FileTrieData>(entries: [FullSlug, T][]) { | ||||
|     const trie = new FileTrieNode<T>("") | ||||
|     const trie = new FileTrieNode<T>([]) | ||||
|     entries.forEach(([, entry]) => trie.add(entry)) | ||||
|     return trie | ||||
|   } | ||||
| @ -98,22 +107,12 @@ export class FileTrieNode<T extends FileTrieData = ContentDetails> { | ||||
|    * in the a flat array including the full path and the node | ||||
|    */ | ||||
|   entries(): [FullSlug, FileTrieNode<T>][] { | ||||
|     const traverse = ( | ||||
|       node: FileTrieNode<T>, | ||||
|       currentPath: string, | ||||
|     ): [FullSlug, FileTrieNode<T>][] => { | ||||
|       const segments = [currentPath, node.slugSegment] | ||||
|       const fullPath = joinSegments(...segments) as FullSlug | ||||
| 
 | ||||
|       const indexQualifiedPath = | ||||
|         node.isFolder && node.depth > 0 ? (joinSegments(fullPath, "index") as FullSlug) : fullPath | ||||
| 
 | ||||
|       const result: [FullSlug, FileTrieNode<T>][] = [[indexQualifiedPath, node]] | ||||
| 
 | ||||
|       return result.concat(...node.children.map((child) => traverse(child, fullPath))) | ||||
|     const traverse = (node: FileTrieNode<T>): [FullSlug, FileTrieNode<T>][] => { | ||||
|       const result: [FullSlug, FileTrieNode<T>][] = [[node.slug, node]] | ||||
|       return result.concat(...node.children.map(traverse)) | ||||
|     } | ||||
| 
 | ||||
|     return traverse(this, "") | ||||
|     return traverse(this) | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user