import { getRepo, getRepoLanguages, getRepoForkCount, getRepoByDid } from '../lib/api/repos';
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';
marked.setOptions({
631 unmodified lines
Expand all
</div>
</div>
);
export const ReadmeCard: Component<{ filename?: string; markdown: string }> = (props) => (
export const ReadmeCard: Component<{
filename?: string;
markdown: string;
repo?: RepoContext;
refName?: string;
path?: string;
}> = (props) => (
<div class="mt-4 rounded bg-white dark:bg-gray-800 shadow-sm w-full mx-auto overflow-hidden">
<Show when={props.filename}>
<div class="px-4 py-2 border-b border-gray-200 dark:border-gray-600 flex items-center gap-2">
<FileText class="w-4 h-4 text-gray-600 dark:text-gray-400" />
<span class="font-mono text-sm text-gray-800 dark:text-gray-200">{props.filename}</span>
</div>
</Show>
<section class="px-6 py-6 overflow-auto">
<MarkdownBlock markdown={props.markdown} variant="readme" />
<MarkdownBlock
markdown={props.markdown}
variant="readme"
repo={props.repo}
refName={props.refName}
path={props.path}
/>
</section>
</div>
);
206 unmodified lines
Expand all
</div>
</Show>
);
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'
: props.variant === 'code'
if (!anchor || event.defaultPrevented) {
return;
}
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
return;
}
const href = anchor.getAttribute('href');
if (!href || !href.startsWith('https://tangled.org/')) {
if (!href) {
return;
}
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
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);
}
} catch {
1079 unmodified lines
Expand all