main
/**
* Web-path routing for server-rendered fragments.
*
* The HTML fragments link with forge-style web paths (`/demo/repo/tree/main/docs/`,
* `/demo/repo/commit/<sha>`, `/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/<space>/repo/<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, string> },
): 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 }
}