/** * Web-path routing for server-rendered fragments. * * The HTML fragments link with forge-style web paths (`/demo/repo/tree/main/docs/`, * `/demo/repo/commit/`, `/demo/repo/commits/main/app/main.py`, * `/demo/repo` for the `..` row) — none of which exist as SPA routes. Map them * back onto in-app state so every link in a fragment keeps working. */ export type WebTarget = | { kind: 'tree'; path: string } | { kind: 'blob'; path: string } | { kind: 'commit'; sha: string } | { kind: 'history'; path: string } | { kind: 'up' } | null export function parentPath(path: string): string { return path.split('/').slice(0, -1).join('/') } /** * Canonical in-app URL for a repo file, folder or any other repo view. * * GitHub-style README links are repo-relative (`../README.md`, `docs/`), but a * `srcdoc` iframe has no URL of its own — it inherits the *parent document's* * base, so the browser resolves them against `/app//repo/` and a * copied or middle-clicked link loses the repo: `CHANGES.md` becomes * `/app/minimax/repo/CHANGES.md`, whose last segment the router reads as the * repo name. Rewriting every in-repo link to this absolute shape is what keeps * "open in new tab" and "copy link address" honest. */ export function repoWebHref( space: string, repo: string, parts: { path?: string; file?: string; rev?: string; tab?: string; extra?: Record }, ): string { const params = new URLSearchParams() if (parts.rev) params.set('rev', parts.rev) if (parts.tab && parts.tab !== 'code') params.set('tab', parts.tab) if (parts.path) params.set('path', parts.path) if (parts.file) params.set('file', parts.file) for (const [key, value] of Object.entries(parts.extra ?? {})) params.set(key, value) const query = params.toString() const base = `/app/${encodeURIComponent(space)}/repo/${encodeURIComponent(repo)}` return query ? `${base}?${query}` : base } /** * Inverse of `repoWebHref`: read back an in-app repo URL produced by it. * * The README sandbox forwards its clicks to the host, and after link rewriting * those hrefs are absolute app URLs rather than repo-relative paths, so the * host needs to turn one back into a route. Returns null for any other URL * (external site, another repo, a non-repo route). */ export function parseRepoAppHref( href: string, space: string, repo: string, ): { path?: string; file?: string; rev?: string; tab?: string } | null { const prefix = `/app/${encodeURIComponent(space)}/repo/${encodeURIComponent(repo)}` const [pathname, search = ''] = href.split('?') if (pathname !== prefix) return null const params = new URLSearchParams(search) const out: { path?: string; file?: string; rev?: string; tab?: string } = {} const path = params.get('path') const file = params.get('file') const rev = params.get('rev') const tab = params.get('tab') if (path) out.path = path if (file) out.file = file if (rev) out.rev = rev if (tab) out.tab = tab return out } export function parseWebHref(href: string, space: string, repo: string, rev: string): WebTarget { const clean = href.split('#')[0].split('?')[0] const prefix = `/${space}/${repo}` if (clean === prefix || clean === `${prefix}/`) return { kind: 'up' } if (!clean.startsWith(`${prefix}/`)) return null let rest = clean.slice(prefix.length + 1) try { rest = decodeURIComponent(rest) } catch { /* leave the raw form when the author did not escape a % */ } const match = /^(tree|blob|commits|commit)\/?([^/]*)\/?(.*)$/.exec(rest) if (!match) return null const [, kind, ref, tail] = match if (kind === 'commit') { // A commit link carries the full object id and no revision segment. return /^[0-9a-f]{7,64}$/i.test(ref) ? { kind: 'commit', sha: ref.toLowerCase() } : null } // Never follow a link pinned to a different revision — it would silently // change what "current revision" means for the rest of the page. if (ref !== rev) return null const path = tail.replace(/\/+$/, '') if (kind === 'tree') return path ? { kind: 'tree', path } : null if (kind === 'blob') return path ? { kind: 'blob', path } : null return { kind: 'history', path } }