appview/reporesolver: fix current directory extraction for tree paths #933

path.Dir on extractPathAfterRef returned the parent directory, which broke relative link resolution in markdown when viewing directories. extractCurrentDir now handles blob and tree paths separately: blob paths return the parent dir, tree paths return the directory itself

Signed-off-by: moshyfawn email@moshyfawn.dev

Changed files
+66 -29
appview
docs
+26 -1
appview/reporesolver/resolver.go
··· 63 63 } 64 64 65 65 // get dir/ref 66 - currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath())) 66 + currentDir := extractCurrentDir(r.URL.EscapedPath()) 67 67 ref := chi.URLParam(r, "ref") 68 68 69 69 repoAt := repo.RepoAt() ··· 130 130 } 131 131 132 132 return repoInfo 133 + } 134 + 135 + // extractCurrentDir gets the current directory for markdown link resolution. 136 + // for blob paths, returns the parent dir. for tree paths, returns the path itself. 137 + // 138 + // /@user/repo/blob/main/docs/README.md => docs 139 + // /@user/repo/tree/main/docs => docs 140 + func extractCurrentDir(fullPath string) string { 141 + fullPath = strings.TrimPrefix(fullPath, "/") 142 + 143 + blobPattern := regexp.MustCompile(`blob/[^/]+/(.*)$`) 144 + if matches := blobPattern.FindStringSubmatch(fullPath); len(matches) > 1 { 145 + return path.Dir(matches[1]) 146 + } 147 + 148 + treePattern := regexp.MustCompile(`tree/[^/]+/(.*)$`) 149 + if matches := treePattern.FindStringSubmatch(fullPath); len(matches) > 1 { 150 + dir := strings.TrimSuffix(matches[1], "/") 151 + if dir == "" { 152 + return "." 153 + } 154 + return dir 155 + } 156 + 157 + return "." 133 158 } 134 159 135 160 // extractPathAfterRef gets the actual repository path
+22
appview/reporesolver/resolver_test.go
··· 1 + package reporesolver 2 + 3 + import "testing" 4 + 5 + func TestExtractCurrentDir(t *testing.T) { 6 + tests := []struct { 7 + path string 8 + want string 9 + }{ 10 + {"/@user/repo/blob/main/docs/README.md", "docs"}, 11 + {"/@user/repo/blob/main/README.md", "."}, 12 + {"/@user/repo/tree/main/docs", "docs"}, 13 + {"/@user/repo/tree/main/docs/", "docs"}, 14 + {"/@user/repo/tree/main", "."}, 15 + } 16 + 17 + for _, tt := range tests { 18 + if got := extractCurrentDir(tt.path); got != tt.want { 19 + t.Errorf("extractCurrentDir(%q) = %q, want %q", tt.path, got, tt.want) 20 + } 21 + } 22 + }
+18 -28
docs/template.html
··· 61 61 popover 62 62 class="mobile-toc-popover 63 63 bg-white dark:bg-gray-800 64 - border-r border-gray-200 dark:border-gray-700 64 + border-b border-gray-200 dark:border-gray-700 65 65 h-full overflow-y-auto 66 66 px-6 py-4 fixed inset-x-0 top-0 w-fit max-w-4/5 m-0" 67 67 > 68 - <div class="flex flex-col min-h-full"> 69 - <div class="flex-1"> 70 - <button 71 - type="button" 72 - popovertarget="mobile-toc-popover" 73 - popovertargetaction="toggle" 74 - class="w-full flex gap-2 items-center text-sm font-semibold dark:text-white mb-4"> 75 - ${ x.svg() } 76 - $if(toc-title)$$toc-title$$else$Table of Contents$endif$ 77 - </button> 78 - ${ table-of-contents:toc.html() } 79 - </div> 80 - ${ single-page:mode.html() } 81 - </div> 68 + <button 69 + type="button" 70 + popovertarget="mobile-toc-popover" 71 + popovertargetaction="toggle" 72 + class="w-full flex gap-2 items-center text-sm font-semibold dark:text-white mb-4"> 73 + ${ x.svg() } 74 + $if(toc-title)$$toc-title$$else$Table of Contents$endif$ 75 + </button> 76 + ${ table-of-contents:toc.html() } 77 + ${ single-page:mode.html() } 82 78 </div> 79 + 83 80 84 81 <!-- desktop sidebar toc --> 85 - <nav 86 - id="$idprefix$TOC" 87 - role="doc-toc" 88 - class="hidden md:flex md:flex-col fixed left-0 top-0 w-80 h-screen 89 - bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 90 - p-4 z-50 overflow-y-auto"> 91 - <div class="flex-1"> 92 - $if(toc-title)$ 93 - <h2 id="$idprefix$toc-title" class="text-lg font-semibold mb-4 text-gray-900">$toc-title$</h2> 94 - $endif$ 95 - ${ table-of-contents:toc.html() } 96 - </div> 82 + <nav id="$idprefix$TOC" role="doc-toc" class="hidden md:block fixed left-0 top-0 w-80 h-screen bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 overflow-y-auto p-4 z-50"> 83 + $if(toc-title)$ 84 + <h2 id="$idprefix$toc-title" class="text-lg font-semibold mb-4 text-gray-900">$toc-title$</h2> 85 + $endif$ 86 + ${ table-of-contents:toc.html() } 97 87 ${ single-page:mode.html() } 98 88 </nav> 99 89 $endif$ ··· 128 118 $body$ 129 119 </article> 130 120 </main> 131 - <nav id="sitenav" class="border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800"> 121 + <nav id="sitenav" class="border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 "> 132 122 <div class="max-w-4xl mx-auto px-8 py-4"> 133 123 <div class="flex justify-between gap-4"> 134 124 <span class="flex-1">