From 23eb6740d07ac3e36115cd644e776d4e36c9a7b8 Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Sat, 30 May 2026 12:09:02 +0000 Subject: [PATCH] fix spa navigate on tangled.org / tangled.sh, resolve relative files properly on repo root --- src/components/repo.tsx | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------- src/pages/repo/code.tsx | 30 ++++++++++++++++++++++++++++-- 2 file(s) changed, 118 insertion(s)(+), 17 deletion(s)(-) diff --git a/src/components/repo.tsx b/src/components/repo.tsx --- a/src/components/repo.tsx +++ b/src/components/repo.tsx @@ -36,7 +36,7 @@ import { getRepoStarCount } from '../lib/api/stars'; import { getRepoIssueCount } from '../lib/api/issues'; import { getRepoPullCount } from '../lib/api/pulls'; -import { formatRelativeTime, languageColor } from '../lib/repo-utils'; +import { formatRelativeTime, languageColor, encodePath } from '../lib/repo-utils'; import { getTangledAppviewService } from '../lib/settings'; import { Avatar, SkeletonBlock, buttonStyles, cardStyles } from './common'; @@ -676,7 +676,13 @@ ); -export const ReadmeCard: Component<{ filename?: string; markdown: string }> = (props) => ( +export const ReadmeCard: Component<{ + filename?: string; + markdown: string; + repo?: RepoContext; + refName?: string; + path?: string; +}> = (props) => (
@@ -685,7 +691,13 @@
- +
); @@ -900,14 +912,71 @@ ); -export const MarkdownBlock: Component<{ markdown: string; variant?: 'default' | 'readme' | 'code' }> = (props) => { +export const resolveRelativePath = (currentPath: string, relativePath: string): string => { + let normalized = relativePath; + if (normalized.startsWith('/')) { + normalized = normalized.slice(1); + currentPath = ''; + } + + if (normalized.startsWith('./')) { + normalized = normalized.slice(2); + } + + if (!currentPath) { + return normalized; + } + + const currentSegments = currentPath.split('/').filter(Boolean); + const relativeSegments = normalized.split('/'); + + for (const segment of relativeSegments) { + if (segment === '..') { + currentSegments.pop(); + } else if (segment !== '.' && segment !== '') { + currentSegments.push(segment); + } + } + + return currentSegments.join('/'); +}; + +export const MarkdownBlock: Component<{ + markdown: string; + variant?: 'default' | 'readme' | 'code'; + repo?: RepoContext; + refName?: string; + path?: string; +}> = (props) => { const navigate = useNavigate(); - const html = createMemo(() => - DOMPurify.sanitize(marked.parse(props.markdown, { async: false }) as string, { + const html = createMemo(() => { + const options: any = { async: false }; + + if (props.repo && props.refName) { + const repo = props.repo; + const refName = props.refName; + options.walkTokens = (token: any) => { + if (token.type === 'link' || token.type === 'image') { + const href = token.href; + if (href && !href.startsWith('#') && !/^[a-z][a-z0-9+.-]*:/i.test(href)) { + const [pathAndQuery, fragment] = href.split('#'); + const [pathPart, queryPart] = pathAndQuery.split('?'); + + const resolvedPath = resolveRelativePath(props.path ?? '', pathPart); + const typeSegment = token.type === 'image' ? 'raw' : 'tree'; + const newHref = `/${repo.owner.handle}/${repo.slug}/${typeSegment}/${encodeURIComponent(refName)}${resolvedPath ? `/${encodePath(resolvedPath)}` : ''}${queryPart ? `?${queryPart}` : ''}${fragment ? `#${fragment}` : ''}`; + token.href = newHref; + } + } + }; + } + + return DOMPurify.sanitize(marked.parse(props.markdown, options) as unknown as string, { ADD_ATTR: ['class'], - }), - ); + }); + }); + const classes = createMemo(() => props.variant === 'readme' ? 'untangled-readme-prose prose prose-sm sm:prose-base min-w-0 max-w-none dark:prose-invert prose-p:text-gray-800 dark:prose-p:text-gray-200 prose-li:text-gray-800 dark:prose-li:text-gray-200 prose-pre:max-w-full prose-pre:overflow-x-auto prose-pre:rounded-md prose-pre:bg-gray-950 dark:prose-pre:bg-gray-950 prose-code:break-words' @@ -922,18 +991,24 @@ return; } - const href = anchor.getAttribute('href'); - if (!href || !href.startsWith('https://tangled.org/')) { - return; - } - if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) { return; } + const href = anchor.getAttribute('href'); + if (!href) { + return; + } + + if (href.startsWith('/') && !href.startsWith('//')) { + event.preventDefault(); + navigate(href); + return; + } + try { - const url = new URL(href); - if (url.hostname === 'tangled.org') { + const url = new URL(href, window.location.origin); + if (url.hostname === 'tangled.org' || url.hostname === 'tangled.sh') { event.preventDefault(); navigate(url.pathname + url.search + url.hash); } diff --git a/src/pages/repo/code.tsx b/src/pages/repo/code.tsx --- a/src/pages/repo/code.tsx +++ b/src/pages/repo/code.tsx @@ -523,6 +523,18 @@ createEffect(() => { const repo = repoQuery.data; + const treeData = treeQuery.data; + if (repo && treeData) { + const tree = treeData.tree; + const parent = tree?.parent ?? tree?.dotdot; + if (tree && (!tree.files || tree.files.length === 0) && parent === treeData.path) { + navigate(blobHref(repo, treeData.ref, treeData.path), { replace: true }); + } + } + }); + + createEffect(() => { + const repo = repoQuery.data; const langs = languagesQuery.data; if (repo && langs?.languages?.length) { const normalized = normalizeLanguages(langs.languages, langs.totalSize); @@ -837,7 +849,15 @@ - {(readme) => } + {(readme) => ( + + )} ); @@ -1010,7 +1030,13 @@
- +
-- tangled.sh