Skip to content

Commit 2585bc4

Browse files
committed
feat: add support for tangled!
1 parent 37ecd46 commit 2585bc4

5 files changed

Lines changed: 135 additions & 0 deletions

File tree

app/composables/useRepoMeta.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,56 @@ const sourcehutAdapter: ProviderAdapter = {
466466
},
467467
}
468468

469+
const tangledAdapter: ProviderAdapter = {
470+
id: 'tangled',
471+
472+
parse(url) {
473+
const host = url.hostname.toLowerCase()
474+
if (
475+
host !== 'tangled.sh' &&
476+
host !== 'www.tangled.sh' &&
477+
host !== 'tangled.org' &&
478+
host !== 'www.tangled.org'
479+
) {
480+
return null
481+
}
482+
483+
const parts = url.pathname.split('/').filter(Boolean)
484+
if (parts.length < 2) return null
485+
486+
// Tangled uses owner/repo format (owner is a domain-like identifier)
487+
const owner = decodeURIComponent(parts[0] ?? '').trim()
488+
const repo = decodeURIComponent(parts[1] ?? '')
489+
.trim()
490+
.replace(/\.git$/i, '')
491+
492+
if (!owner || !repo) return null
493+
494+
return { provider: 'tangled', owner, repo }
495+
},
496+
497+
links(ref) {
498+
const base = `https://tangled.sh/${ref.owner}/${ref.repo}`
499+
return {
500+
repo: base,
501+
stars: base, // Tangled shows stars on the repo page
502+
forks: `${base}/fork`,
503+
}
504+
},
505+
506+
async fetchMeta(_ref, links) {
507+
// Tangled doesn't have a public API for repo stats yet
508+
// Just return basic info without fetching
509+
return {
510+
provider: 'tangled',
511+
url: links.repo,
512+
stars: 0,
513+
forks: 0,
514+
links,
515+
}
516+
},
517+
}
518+
469519
// Order matters: more specific adapters should come before generic ones
470520
const providers: readonly ProviderAdapter[] = [
471521
githubAdapter,
@@ -474,6 +524,7 @@ const providers: readonly ProviderAdapter[] = [
474524
codebergAdapter,
475525
giteeAdapter,
476526
sourcehutAdapter,
527+
tangledAdapter,
477528
giteaAdapter, // Generic Gitea adapter last as fallback for self-hosted instances
478529
] as const
479530

app/pages/[...package].vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ const PROVIDER_ICONS: Record<string, string> = {
164164
gitea: 'i-simple-icons-gitea',
165165
gitee: 'i-simple-icons-gitee',
166166
sourcehut: 'i-simple-icons-sourcehut',
167+
tangled: 'i-custom-tangled',
167168
}
168169
169170
const repoProviderIcon = computed(() => {

shared/utils/git-providers.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export type ProviderId =
88
| 'codeberg'
99
| 'sourcehut'
1010
| 'gitee'
11+
| 'tangled'
1112

1213
export interface RepoRef {
1314
provider: ProviderId
@@ -148,6 +149,27 @@ const providers: ProviderConfig[] = [
148149
getRawBaseUrl: (ref, branch = 'HEAD') =>
149150
`https://git.sr.ht/${ref.owner}/${ref.repo}/blob/${branch}`,
150151
},
152+
{
153+
id: 'tangled',
154+
matchHost: host =>
155+
host === 'tangled.sh' ||
156+
host === 'www.tangled.sh' ||
157+
host === 'tangled.org' ||
158+
host === 'www.tangled.org',
159+
parsePath: parts => {
160+
if (parts.length < 2) return null
161+
// Tangled uses owner/repo format (owner is a domain-like identifier, e.g., nonbinary.computer)
162+
const owner = decodeURIComponent(parts[0] ?? '').trim()
163+
const repo = decodeURIComponent(parts[1] ?? '')
164+
.trim()
165+
.replace(/\.git$/i, '')
166+
if (!owner || !repo) return null
167+
return { owner, repo }
168+
},
169+
getRawBaseUrl: (ref, branch = 'main') =>
170+
`https://tangled.sh/${ref.owner}/${ref.repo}/raw/branch/${branch}`,
171+
blobToRaw: url => url.replace('/blob/', '/raw/branch/'),
172+
},
151173
{
152174
id: 'gitea',
153175
matchHost: host => {
@@ -169,6 +191,8 @@ const providers: ProviderConfig[] = [
169191
'gitee.com',
170192
'sr.ht',
171193
'git.sr.ht',
194+
'tangled.sh',
195+
'tangled.org',
172196
...GITLAB_HOSTS,
173197
]
174198
if (skipHosts.some(h => host === h || host.endsWith(`.${h}`))) return false

test/unit/readme-url-resolution.spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,4 +165,55 @@ describe('parseRepositoryInfo', () => {
165165
})
166166
})
167167
})
168+
169+
describe('Tangled support', () => {
170+
it('parses Tangled URL with tangled.org domain', () => {
171+
const result = parseRepositoryInfo({
172+
url: 'https://tangled.org/nonbinary.computer/weaver',
173+
})
174+
expect(result).toMatchObject({
175+
provider: 'tangled',
176+
owner: 'nonbinary.computer',
177+
repo: 'weaver',
178+
rawBaseUrl: 'https://tangled.sh/nonbinary.computer/weaver/raw/branch/main',
179+
})
180+
})
181+
182+
it('parses Tangled URL with tangled.sh domain', () => {
183+
const result = parseRepositoryInfo({
184+
url: 'https://tangled.sh/pds.ls/pdsls',
185+
})
186+
expect(result).toMatchObject({
187+
provider: 'tangled',
188+
owner: 'pds.ls',
189+
repo: 'pdsls',
190+
rawBaseUrl: 'https://tangled.sh/pds.ls/pdsls/raw/branch/main',
191+
})
192+
})
193+
194+
it('parses Tangled URL with .git suffix', () => {
195+
const result = parseRepositoryInfo({
196+
type: 'git',
197+
url: 'https://tangled.org/owner/repo.git',
198+
})
199+
expect(result).toMatchObject({
200+
provider: 'tangled',
201+
owner: 'owner',
202+
repo: 'repo',
203+
})
204+
})
205+
206+
it('parses Tangled URL with directory (monorepo)', () => {
207+
const result = parseRepositoryInfo({
208+
url: 'https://tangled.org/tangled.org/core',
209+
directory: 'packages/web',
210+
})
211+
expect(result).toMatchObject({
212+
provider: 'tangled',
213+
owner: 'tangled.org',
214+
repo: 'core',
215+
directory: 'packages/web',
216+
})
217+
})
218+
})
168219
})

uno.config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,19 @@ import {
77
} from 'unocss'
88
import type { Theme } from '@unocss/preset-wind4/theme'
99

10+
const customIcons = {
11+
tangled:
12+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 25"><path fill="currentColor" d="M16.346 24.11c-.785-.007-1.384-.235-2.034-.632-.932-.49-1.643-1.314-2.152-2.222-.808 1.003-1.888 1.611-3.097 1.955-.515.15-1.416.301-2.911-.24-2.154-.724-3.723-2.965-3.545-5.25-.033-.946.313-1.875.802-2.674-1.305-.7-2.37-1.876-2.777-3.318-.248-.79-.237-1.64-.146-2.452.327-1.916 1.764-3.582 3.615-4.182.738-1.683 2.35-2.938 4.176-3.193a5.54 5.54 0 0 1 3.528.7C13.351.89 16.043.383 18.1 1.436c1.568.75 2.69 2.312 2.962 4.015 1.492.598 2.749 1.817 3.242 3.365.33.958.34 2.013.127 2.997-.382 1.536-1.465 2.842-2.868 3.557.003.273.901 2.243.751 3.73-.033 1.858-1.211 3.62-2.846 4.475-.954.557-2.085.546-3.12.535m-4.47-5.35c1.322-.148 2.19-1.3 2.862-2.339.319-.473.562-1.002.803-1.506.314.287.58.828 1.075.957.522.163 1.133.03 1.453-.443.611-1.14.31-2.517-.046-3.699-.22-.679-.507-1.375-1.054-1.856.116-.823-.372-1.66-1.065-2.09-.592.47-1.491.468-2.061-.037-1.093 1.115-2.095 1.078-3.063.195-.217-.199-.632 1.212-2.088.413-.837.7-1.485 1.375-2.06 2.346-.559 1.046-1.143 1.976-1.194 3.113-.024.664.495 1.36 1.198 1.306.703.063 1.182-.63 1.714-.917.08.928.169 1.924.482 2.829.36 1.171 1.627 1.916 2.825 1.745zm.687-3.498c-.644-.394-.334-1.25-.36-1.871.064-.75.115-1.538.453-2.221.356-.487 1.226-.3 1.265.326-.026.628-.314 1.254-.28 1.905-.075.544.054 1.155-.186 1.653-.198.275-.6.355-.892.208m-2.81-.358c-.605-.329-.413-1.156-.508-1.73.08-.666.014-1.51.571-1.978.545-.38 1.287.271 1.03.869-.276.755-.096 1.58-.09 2.346a.712.712 0 0 1-1.002.493"/></svg>',
13+
}
14+
1015
export default defineConfig({
1116
presets: [
1217
presetWind4(),
1318
presetIcons({
1419
scale: 1.2,
20+
collections: {
21+
custom: customIcons,
22+
},
1523
}),
1624
],
1725
transformers: [transformerDirectives(), transformerVariantGroup()],

0 commit comments

Comments
 (0)