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) => (
);
@@ -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