Browse Source

get the docs page to handle markdown and asciidoc

main
Silberengel 4 weeks ago
parent
commit
032930e3ba
  1. 935
      package-lock.json
  2. 1
      package.json
  3. 288
      src/routes/repos/[npub]/[repo]/+page.svelte

935
package-lock.json generated

File diff suppressed because it is too large Load Diff

1
package.json

@ -19,6 +19,7 @@
"@codemirror/view": "^6.39.14", "@codemirror/view": "^6.39.14",
"@sveltejs/kit": "^2.0.0", "@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0",
"asciidoctor": "^3.0.4",
"codemirror": "^6.0.2", "codemirror": "^6.0.2",
"codemirror-asciidoc": "^2.0.1", "codemirror-asciidoc": "^2.0.1",
"highlight.js": "^11.10.0", "highlight.js": "^11.10.0",

288
src/routes/repos/[npub]/[repo]/+page.svelte

@ -72,9 +72,12 @@
if (wasDifferent) { if (wasDifferent) {
checkMaintainerStatus().catch(err => console.warn('Failed to reload maintainer status after login:', err)); checkMaintainerStatus().catch(err => console.warn('Failed to reload maintainer status after login:', err));
loadBookmarkStatus().catch(err => console.warn('Failed to reload bookmark status after login:', err)); loadBookmarkStatus().catch(err => console.warn('Failed to reload bookmark status after login:', err));
// Reload current tab data if needed // Reload all repository data with the new user context
if (activeTab === 'files' && !loading) { if (!loading) {
loadBranches().catch(err => console.warn('Failed to reload branches after login:', err));
loadFiles().catch(err => console.warn('Failed to reload files after login:', err)); loadFiles().catch(err => console.warn('Failed to reload files after login:', err));
loadReadme().catch(err => console.warn('Failed to reload readme after login:', err));
loadTags().catch(err => console.warn('Failed to reload tags after login:', err));
} }
} }
} else { } else {
@ -192,7 +195,9 @@
if (repoNotFound) return; if (repoNotFound) return;
loadingReadme = true; loadingReadme = true;
try { try {
const response = await fetch(`/api/repos/${npub}/${repo}/readme?ref=${currentBranch}`); const response = await fetch(`/api/repos/${npub}/${repo}/readme?ref=${currentBranch}`, {
headers: buildApiHeaders()
});
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
if (data.found) { if (data.found) {
@ -470,7 +475,9 @@
async function loadForkInfo() { async function loadForkInfo() {
try { try {
const response = await fetch(`/api/repos/${npub}/${repo}/fork`); const response = await fetch(`/api/repos/${npub}/${repo}/fork`, {
headers: buildApiHeaders()
});
if (response.ok) { if (response.ok) {
forkInfo = await response.json(); forkInfo = await response.json();
} }
@ -486,7 +493,9 @@
try { try {
// Check if repo exists locally by trying to fetch branches // Check if repo exists locally by trying to fetch branches
// If it returns 404, repo is not cloned // If it returns 404, repo is not cloned
const response = await fetch(`/api/repos/${npub}/${repo}/branches`); const response = await fetch(`/api/repos/${npub}/${repo}/branches`, {
headers: buildApiHeaders()
});
isRepoCloned = response.ok; isRepoCloned = response.ok;
} catch (err) { } catch (err) {
isRepoCloned = false; isRepoCloned = false;
@ -503,7 +512,8 @@
const response = await fetch(`/api/repos/${npub}/${repo}/clone`, { const response = await fetch(`/api/repos/${npub}/${repo}/clone`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
...buildApiHeaders()
} }
}); });
@ -546,7 +556,10 @@
console.log(`[Fork UI] Starting fork of ${truncatedNpub}/${repo}...`); console.log(`[Fork UI] Starting fork of ${truncatedNpub}/${repo}...`);
const response = await fetch(`/api/repos/${npub}/${repo}/fork`, { const response = await fetch(`/api/repos/${npub}/${repo}/fork`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {
'Content-Type': 'application/json',
...buildApiHeaders()
},
body: JSON.stringify({ userPubkey }) body: JSON.stringify({ userPubkey })
}); });
@ -637,13 +650,16 @@
} }
} }
// If no chat relays are defined for the project, use default relays
const relaysToUse = chatRelays.length > 0 ? chatRelays : DEFAULT_NOSTR_RELAYS;
const discussionsService = new DiscussionsService(combinedRelays); const discussionsService = new DiscussionsService(combinedRelays);
const discussionEntries = await discussionsService.getDiscussions( const discussionEntries = await discussionsService.getDiscussions(
repoOwnerPubkey, repoOwnerPubkey,
repo, repo,
announcement.id, announcement.id,
announcement.pubkey, announcement.pubkey,
chatRelays, relaysToUse,
combinedRelays combinedRelays
); );
@ -670,7 +686,9 @@
} }
async function loadDocumentation() { async function loadDocumentation() {
if (loadingDocs || documentationContent !== null) return; if (loadingDocs) return;
// Only skip if we already have rendered HTML (successful load)
if (documentationHtml !== null) return;
loadingDocs = true; loadingDocs = true;
try { try {
@ -678,21 +696,9 @@
const data = $page.data as typeof pageData; const data = $page.data as typeof pageData;
if (data.repoIsPrivate) { if (data.repoIsPrivate) {
// Check access via API // Check access via API
const headers: Record<string, string> = {}; const accessResponse = await fetch(`/api/repos/${npub}/${repo}/access`, {
if (userPubkey) { headers: buildApiHeaders()
try { });
const decoded = nip19.decode(userPubkey);
if (decoded.type === 'npub') {
headers['X-User-Pubkey'] = decoded.data as string;
} else {
headers['X-User-Pubkey'] = userPubkey;
}
} catch {
headers['X-User-Pubkey'] = userPubkey;
}
}
const accessResponse = await fetch(`/api/repos/${npub}/${repo}/access`, { headers });
if (accessResponse.ok) { if (accessResponse.ok) {
const accessData = await accessResponse.json(); const accessData = await accessResponse.json();
if (!accessData.canView) { if (!accessData.canView) {
@ -711,7 +717,9 @@
if (decoded.type === 'npub') { if (decoded.type === 'npub') {
const repoOwnerPubkey = decoded.data as string; const repoOwnerPubkey = decoded.data as string;
const client = new NostrClient(DEFAULT_NOSTR_RELAYS); const client = new NostrClient(DEFAULT_NOSTR_RELAYS);
const events = await client.fetchEvents([
// First, get the repo announcement to find the documentation tag
const announcementEvents = await client.fetchEvents([
{ {
kinds: [KIND.REPO_ANNOUNCEMENT], kinds: [KIND.REPO_ANNOUNCEMENT],
authors: [repoOwnerPubkey], authors: [repoOwnerPubkey],
@ -720,11 +728,72 @@
} }
]); ]);
if (events.length > 0) { if (announcementEvents.length === 0) {
documentationContent = events[0].content || null; loadingDocs = false;
return;
}
const announcement = announcementEvents[0];
// Look for documentation tag in the announcement
const documentationTag = announcement.tags.find(t => t[0] === 'documentation');
// Render as markdown if content exists let docKind: number | null = null;
if (documentationContent) {
if (documentationTag && documentationTag[1]) {
// Parse the a-tag format: kind:pubkey:identifier
const docAddress = documentationTag[1];
const parts = docAddress.split(':');
if (parts.length >= 3) {
docKind = parseInt(parts[0]);
const docPubkey = parts[1];
const docIdentifier = parts.slice(2).join(':'); // In case identifier contains ':'
// Fetch the documentation event
const docEvents = await client.fetchEvents([
{
kinds: [docKind],
authors: [docPubkey],
'#d': [docIdentifier],
limit: 1
}
]);
if (docEvents.length > 0) {
documentationContent = docEvents[0].content || null;
} else {
console.warn('Documentation event not found:', docAddress);
documentationContent = null;
}
} else {
console.warn('Invalid documentation tag format:', docAddress);
documentationContent = null;
}
} else {
// No documentation tag, try to use announcement content as fallback
documentationContent = announcement.content || null;
}
// Render content based on kind: AsciiDoc for 30041 or 30818, Markdown otherwise
if (documentationContent) {
// Check if we should use AsciiDoc parser (kinds 30041 or 30818)
const useAsciiDoc = docKind === 30041 || docKind === 30818;
if (useAsciiDoc) {
// Use AsciiDoc parser
const Asciidoctor = (await import('@asciidoctor/core')).default;
const asciidoctor = Asciidoctor();
const converted = asciidoctor.convert(documentationContent, {
safe: 'safe',
attributes: {
'source-highlighter': 'highlight.js'
}
});
// Convert to string if it's a Document object
documentationHtml = typeof converted === 'string' ? converted : String(converted);
} else {
// Use Markdown parser
const MarkdownIt = (await import('markdown-it')).default; const MarkdownIt = (await import('markdown-it')).default;
const hljsModule = await import('highlight.js'); const hljsModule = await import('highlight.js');
const hljs = hljsModule.default || hljsModule; const hljs = hljsModule.default || hljsModule;
@ -742,10 +811,14 @@
documentationHtml = md.render(documentationContent); documentationHtml = md.render(documentationContent);
} }
} else {
// No content found, clear HTML
documentationHtml = null;
} }
} }
} catch (err) { } catch (err) {
console.error('Error loading documentation:', err); console.error('Error loading documentation:', err);
documentationHtml = null;
} finally { } finally {
loadingDocs = false; loadingDocs = false;
} }
@ -1042,7 +1115,9 @@
if (repoNotFound) return; if (repoNotFound) return;
loadingVerification = true; loadingVerification = true;
try { try {
const response = await fetch(`/api/repos/${npub}/${repo}/verify`); const response = await fetch(`/api/repos/${npub}/${repo}/verify`, {
headers: buildApiHeaders()
});
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
verificationStatus = data; verificationStatus = data;
@ -1112,9 +1187,26 @@
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }
// Helper function to build headers with user pubkey
function buildApiHeaders(): Record<string, string> {
const headers: Record<string, string> = {};
// Use $userStore directly to ensure we get the latest value
const currentUserPubkeyHex = $userStore.userPubkeyHex || userPubkeyHex;
if (currentUserPubkeyHex) {
headers['X-User-Pubkey'] = currentUserPubkeyHex;
// Debug logging (remove in production)
console.debug('[API Headers] Sending X-User-Pubkey:', currentUserPubkeyHex.substring(0, 16) + '...');
} else {
console.debug('[API Headers] No user pubkey available, sending request without X-User-Pubkey header');
}
return headers;
}
async function loadBranches() { async function loadBranches() {
try { try {
const response = await fetch(`/api/repos/${npub}/${repo}/branches`); const response = await fetch(`/api/repos/${npub}/${repo}/branches`, {
headers: buildApiHeaders()
});
if (response.ok) { if (response.ok) {
branches = await response.json(); branches = await response.json();
if (branches.length > 0 && !branches.includes(currentBranch)) { if (branches.length > 0 && !branches.includes(currentBranch)) {
@ -1138,7 +1230,9 @@
error = null; error = null;
try { try {
const url = `/api/repos/${npub}/${repo}/tree?ref=${currentBranch}&path=${encodeURIComponent(path)}`; const url = `/api/repos/${npub}/${repo}/tree?ref=${currentBranch}&path=${encodeURIComponent(path)}`;
const response = await fetch(url); const response = await fetch(url, {
headers: buildApiHeaders()
});
if (!response.ok) { if (!response.ok) {
if (response.status === 404) { if (response.status === 404) {
@ -1251,7 +1345,8 @@
const response = await fetch(`/api/repos/${npub}/${repo}/file`, { const response = await fetch(`/api/repos/${npub}/${repo}/file`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
...buildApiHeaders()
}, },
body: JSON.stringify({ body: JSON.stringify({
path: currentFile, path: currentFile,
@ -1313,7 +1408,10 @@
const response = await fetch(`/api/repos/${npub}/${repo}/file`, { const response = await fetch(`/api/repos/${npub}/${repo}/file`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {
'Content-Type': 'application/json',
...buildApiHeaders()
},
body: JSON.stringify({ body: JSON.stringify({
path: filePath, path: filePath,
content: newFileContent, content: newFileContent,
@ -1361,7 +1459,10 @@
const response = await fetch(`/api/repos/${npub}/${repo}/file`, { const response = await fetch(`/api/repos/${npub}/${repo}/file`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {
'Content-Type': 'application/json',
...buildApiHeaders()
},
body: JSON.stringify({ body: JSON.stringify({
path: filePath, path: filePath,
commitMessage: `Delete ${filePath}`, commitMessage: `Delete ${filePath}`,
@ -1402,7 +1503,10 @@
try { try {
const response = await fetch(`/api/repos/${npub}/${repo}/branches`, { const response = await fetch(`/api/repos/${npub}/${repo}/branches`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {
'Content-Type': 'application/json',
...buildApiHeaders()
},
body: JSON.stringify({ body: JSON.stringify({
branchName: newBranchName, branchName: newBranchName,
fromBranch: newBranchFrom fromBranch: newBranchFrom
@ -1429,7 +1533,9 @@
loadingCommits = true; loadingCommits = true;
error = null; error = null;
try { try {
const response = await fetch(`/api/repos/${npub}/${repo}/commits?branch=${currentBranch}&limit=50`); const response = await fetch(`/api/repos/${npub}/${repo}/commits?branch=${currentBranch}&limit=50`, {
headers: buildApiHeaders()
});
if (response.ok) { if (response.ok) {
commits = await response.json(); commits = await response.json();
} }
@ -1448,7 +1554,9 @@
? commits[commits.findIndex(c => c.hash === commitHash) + 1]?.hash || `${commitHash}^` ? commits[commits.findIndex(c => c.hash === commitHash) + 1]?.hash || `${commitHash}^`
: `${commitHash}^`; : `${commitHash}^`;
const response = await fetch(`/api/repos/${npub}/${repo}/diff?from=${parentHash}&to=${commitHash}`); const response = await fetch(`/api/repos/${npub}/${repo}/diff?from=${parentHash}&to=${commitHash}`, {
headers: buildApiHeaders()
});
if (response.ok) { if (response.ok) {
diffData = await response.json(); diffData = await response.json();
selectedCommit = commitHash; selectedCommit = commitHash;
@ -1464,7 +1572,9 @@
async function loadTags() { async function loadTags() {
if (repoNotFound) return; if (repoNotFound) return;
try { try {
const response = await fetch(`/api/repos/${npub}/${repo}/tags`); const response = await fetch(`/api/repos/${npub}/${repo}/tags`, {
headers: buildApiHeaders()
});
if (response.ok) { if (response.ok) {
tags = await response.json(); tags = await response.json();
} }
@ -1490,7 +1600,10 @@
try { try {
const response = await fetch(`/api/repos/${npub}/${repo}/tags`, { const response = await fetch(`/api/repos/${npub}/${repo}/tags`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: {
'Content-Type': 'application/json',
...buildApiHeaders()
},
body: JSON.stringify({ body: JSON.stringify({
tagName: newTagName, tagName: newTagName,
ref: newTagRef, ref: newTagRef,
@ -1520,7 +1633,9 @@
loadingIssues = true; loadingIssues = true;
error = null; error = null;
try { try {
const response = await fetch(`/api/repos/${npub}/${repo}/issues`); const response = await fetch(`/api/repos/${npub}/${repo}/issues`, {
headers: buildApiHeaders()
});
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
issues = data.map((issue: { id: string; tags: string[][]; content: string; status?: string; pubkey: string; created_at: number }) => ({ issues = data.map((issue: { id: string; tags: string[][]; content: string; status?: string; pubkey: string; created_at: number }) => ({
@ -1594,7 +1709,9 @@
loadingPRs = true; loadingPRs = true;
error = null; error = null;
try { try {
const response = await fetch(`/api/repos/${npub}/${repo}/prs`); const response = await fetch(`/api/repos/${npub}/${repo}/prs`, {
headers: buildApiHeaders()
});
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
prs = data.map((pr: { id: string; tags: string[][]; content: string; status?: string; pubkey: string; created_at: number; commitId?: string }) => ({ prs = data.map((pr: { id: string; tags: string[][]; content: string; status?: string; pubkey: string; created_at: number; commitId?: string }) => ({
@ -2354,6 +2471,26 @@
</div> </div>
{/if} {/if}
{#if activeTab === 'docs'}
<div class="docs-content">
{#if loadingDocs}
<div class="loading">Loading documentation...</div>
{:else if documentationHtml}
<div class="documentation-body">
{@html documentationHtml}
</div>
{:else if documentationContent === null}
<div class="empty-state">
<p>No documentation found for this repository.</p>
</div>
{:else}
<div class="empty-state">
<p>Documentation content is empty.</p>
</div>
{/if}
</div>
{/if}
{#if activeTab === 'discussions'} {#if activeTab === 'discussions'}
<div class="discussions-content"> <div class="discussions-content">
{#if loadingDiscussions} {#if loadingDiscussions}
@ -4091,6 +4228,73 @@
margin: 1rem 0; margin: 1rem 0;
} }
/* Documentation */
.docs-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--card-bg);
}
.documentation-body {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 1.5rem;
min-height: 0;
}
.documentation-body :global(h1),
.documentation-body :global(h2),
.documentation-body :global(h3),
.documentation-body :global(h4),
.documentation-body :global(h5),
.documentation-body :global(h6) {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
color: var(--text-primary);
}
.documentation-body :global(p) {
margin-bottom: 1rem;
line-height: 1.6;
}
.documentation-body :global(code) {
background: var(--bg-secondary);
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.9em;
}
.documentation-body :global(pre) {
background: var(--bg-secondary);
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
border: 1px solid var(--border-light);
margin: 1rem 0;
}
.documentation-body :global(pre code) {
background: none;
padding: 0;
}
.documentation-body :global(.hljs) {
background: var(--bg-secondary);
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
border: 1px solid var(--border-light);
}
.documentation-body :global(pre.hljs) {
margin: 1rem 0;
}
/* Issues and PRs */ /* Issues and PRs */
.issues-sidebar, .prs-sidebar { .issues-sidebar, .prs-sidebar {
width: 300px; width: 300px;

Loading…
Cancel
Save