diff --git a/package-lock.json b/package-lock.json index e2a08ea..3bb1729 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,13 +17,16 @@ "@sveltejs/vite-plugin-svelte": "^4.0.0", "codemirror": "^6.0.2", "codemirror-asciidoc": "^2.0.1", - "marked": "^17.0.2", + "highlight.js": "^11.10.0", + "markdown-it": "^14.1.0", "nostr-tools": "^2.22.1", "simple-git": "^3.31.1", - "svelte": "^5.0.0" + "svelte": "^5.0.0", + "ws": "^8.19.0" }, "devDependencies": { "@sveltejs/adapter-node": "^5.0.0", + "@types/markdown-it": "^14.1.2", "@types/node": "^20.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", @@ -1817,6 +1820,31 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.33", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", @@ -2155,7 +2183,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -2525,6 +2552,18 @@ "node": ">=6.0.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es6-promise": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", @@ -3088,6 +3127,15 @@ "node": ">= 0.4" } }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3307,6 +3355,15 @@ "node": ">= 0.8.0" } }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", @@ -3345,18 +3402,29 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/marked": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.2.tgz", - "integrity": "sha512-s5HZGFQea7Huv5zZcAGhJLT3qLpAfnY7v7GWkICUr0+Wd5TFEtdlRR2XUL5Gg+RH7u2Df595ifrxR03mBaw7gA==", + "node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", "license": "MIT", - "bin": { - "marked": "bin/marked.js" + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" }, - "engines": { - "node": ">= 20" + "bin": { + "markdown-it": "bin/markdown-it.mjs" } }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3734,6 +3802,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4326,6 +4403,12 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -4460,6 +4543,27 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index b378be8..2757390 100644 --- a/package.json +++ b/package.json @@ -21,13 +21,16 @@ "@sveltejs/vite-plugin-svelte": "^4.0.0", "codemirror": "^6.0.2", "codemirror-asciidoc": "^2.0.1", - "marked": "^17.0.2", + "highlight.js": "^11.10.0", + "markdown-it": "^14.1.0", "nostr-tools": "^2.22.1", "simple-git": "^3.31.1", - "svelte": "^5.0.0" + "svelte": "^5.0.0", + "ws": "^8.19.0" }, "devDependencies": { "@sveltejs/adapter-node": "^5.0.0", + "@types/markdown-it": "^14.1.2", "@types/node": "^20.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", diff --git a/src/lib/services/nostr/nostr-client.ts b/src/lib/services/nostr/nostr-client.ts index 14bda57..dd14deb 100644 --- a/src/lib/services/nostr/nostr-client.ts +++ b/src/lib/services/nostr/nostr-client.ts @@ -3,6 +3,34 @@ */ import type { NostrEvent, NostrFilter } from '../../types/nostr.js'; +import { createRequire } from 'module'; + +// Polyfill WebSocket for Node.js environments (lazy initialization) +let wsPolyfillInitialized = false; +function initializeWebSocketPolyfill() { + if (wsPolyfillInitialized) return; + if (typeof global === 'undefined' || typeof global.WebSocket !== 'undefined') { + wsPolyfillInitialized = true; + return; + } + + try { + // Use createRequire for ES modules compatibility + const requireFunc = createRequire(import.meta.url); + const WebSocketImpl = requireFunc('ws'); + global.WebSocket = WebSocketImpl as any; + wsPolyfillInitialized = true; + } catch { + // ws package not available, will fail at runtime in Node.js + console.warn('WebSocket polyfill not available. Install "ws" package for Node.js support.'); + wsPolyfillInitialized = true; // Mark as initialized to avoid repeated warnings + } +} + +// Initialize on module load if in Node.js +if (typeof process !== 'undefined' && process.versions?.node) { + initializeWebSocketPolyfill(); +} export class NostrClient { private relays: string[] = []; @@ -36,6 +64,9 @@ export class NostrClient { } private async fetchFromRelay(relay: string, filters: NostrFilter[]): Promise { + // Ensure WebSocket polyfill is initialized + initializeWebSocketPolyfill(); + return new Promise((resolve, reject) => { const ws = new WebSocket(relay); const events: NostrEvent[] = []; @@ -131,6 +162,9 @@ export class NostrClient { } private async publishToRelay(relay: string, nostrEvent: NostrEvent): Promise { + // Ensure WebSocket polyfill is initialized + initializeWebSocketPolyfill(); + return new Promise((resolve, reject) => { const ws = new WebSocket(relay); let resolved = false; diff --git a/src/routes/api/git/[...path]/+server.ts b/src/routes/api/git/[...path]/+server.ts index bb9cb64..419c5ef 100644 --- a/src/routes/api/git/[...path]/+server.ts +++ b/src/routes/api/git/[...path]/+server.ts @@ -25,8 +25,11 @@ const ownershipTransferService = new OwnershipTransferService(DEFAULT_NOSTR_RELA const maintainerService = new MaintainerService(DEFAULT_NOSTR_RELAYS); // Path to git-http-backend (common locations) +// Alpine Linux: /usr/lib/git-core/git-http-backend +// Debian/Ubuntu: /usr/lib/git-core/git-http-backend +// macOS: /usr/local/libexec/git-core/git-http-backend or /opt/homebrew/libexec/git-core/git-http-backend const GIT_HTTP_BACKEND_PATHS = [ - '/usr/lib/git-core/git-http-backend', + '/usr/lib/git-core/git-http-backend', // Alpine, Debian, Ubuntu '/usr/libexec/git-core/git-http-backend', '/usr/local/libexec/git-core/git-http-backend', '/opt/homebrew/libexec/git-core/git-http-backend' diff --git a/src/routes/docs/nip34/+page.svelte b/src/routes/docs/nip34/+page.svelte index d586251..6f26b3f 100644 --- a/src/routes/docs/nip34/+page.svelte +++ b/src/routes/docs/nip34/+page.svelte @@ -10,8 +10,24 @@ try { const docContent = $page.data.content; if (docContent) { - const { marked } = await import('marked'); - content = marked.parse(docContent) as string; + const MarkdownIt = (await import('markdown-it')).default; + const hljsModule = await import('highlight.js'); + const hljs = hljsModule.default || hljsModule; + + const md: any = new MarkdownIt({ + highlight: function (str: string, lang: string): string { + if (lang && hljs.getLanguage(lang)) { + try { + return '
' +
+                       hljs.highlight(str, { language: lang }).value +
+                       '
'; + } catch (__) {} + } + return '
' + md.utils.escapeHtml(str) + '
'; + } + }); + + content = md.render(docContent); } else { error = $page.data.error || 'Failed to load NIP-34 documentation'; } diff --git a/src/routes/repos/[npub]/[repo]/+page.svelte b/src/routes/repos/[npub]/[repo]/+page.svelte index 966fc77..3ecc0f4 100644 --- a/src/routes/repos/[npub]/[repo]/+page.svelte +++ b/src/routes/repos/[npub]/[repo]/+page.svelte @@ -101,6 +101,7 @@ let readmeIsMarkdown = $state(false); let loadingReadme = $state(false); let readmeHtml = $state(''); + let highlightedFileContent = $state(''); // Fork let forkInfo = $state<{ isFork: boolean; originalRepo: { npub: string; repo: string } | null } | null>(null); @@ -123,8 +124,24 @@ // Render markdown if needed if (readmeIsMarkdown && readmeContent) { - const { marked } = await import('marked'); - readmeHtml = marked.parse(readmeContent) as string; + const MarkdownIt = (await import('markdown-it')).default; + const hljsModule = await import('highlight.js'); + const hljs = hljsModule.default || hljsModule; + + const md: any = new MarkdownIt({ + highlight: function (str: string, lang: string): string { + if (lang && hljs.getLanguage(lang)) { + try { + return '
' +
+                           hljs.highlight(str, { language: lang }).value +
+                           '
'; + } catch (__) {} + } + return '
' + md.utils.escapeHtml(str) + '
'; + } + }); + + readmeHtml = md.render(readmeContent); } } } @@ -135,6 +152,145 @@ } } + // Map file extensions to highlight.js language names + function getHighlightLanguage(ext: string): string { + const langMap: Record = { + 'js': 'javascript', + 'ts': 'typescript', + 'jsx': 'javascript', + 'tsx': 'typescript', + 'json': 'json', + 'css': 'css', + 'html': 'xml', + 'xml': 'xml', + 'yaml': 'yaml', + 'yml': 'yaml', + 'py': 'python', + 'rb': 'ruby', + 'go': 'go', + 'rs': 'rust', + 'java': 'java', + 'c': 'c', + 'cpp': 'cpp', + 'h': 'c', + 'hpp': 'cpp', + 'sh': 'bash', + 'bash': 'bash', + 'zsh': 'bash', + 'sql': 'sql', + 'php': 'php', + 'swift': 'swift', + 'kt': 'kotlin', + 'scala': 'scala', + 'r': 'r', + 'm': 'objectivec', + 'mm': 'objectivec', + 'vue': 'xml', + 'svelte': 'xml', + 'dockerfile': 'dockerfile', + 'toml': 'toml', + 'ini': 'ini', + 'conf': 'ini', + 'log': 'plaintext', + 'txt': 'plaintext', + 'adoc': 'asciidoc', + 'asciidoc': 'asciidoc', + 'ad': 'asciidoc', + }; + return langMap[ext.toLowerCase()] || 'plaintext'; + } + + async function applySyntaxHighlighting(content: string, ext: string) { + try { + const hljsModule = await import('highlight.js'); + // highlight.js v11+ uses default export + const hljs = hljsModule.default || hljsModule; + const lang = getHighlightLanguage(ext); + + // Register AsciiDoc language if needed (not in highlight.js by default) + if (lang === 'asciidoc' && !hljs.getLanguage('asciidoc')) { + hljs.registerLanguage('asciidoc', function(hljs: any) { + return { + name: 'AsciiDoc', + aliases: ['adoc', 'asciidoc', 'ad'], + contains: [ + // Headers + { + className: 'section', + begin: /^={1,6}\s+/, + relevance: 10 + }, + // Bold + { + className: 'strong', + begin: /\*\*[^*]+\*\*/, + relevance: 0 + }, + // Italic + { + className: 'emphasis', + begin: /_[^_]+_/, + relevance: 0 + }, + // Inline code + { + className: 'code', + begin: /`[^`]+`/, + relevance: 0 + }, + // Code blocks + { + className: 'code', + begin: /^----+$/, + end: /^----+$/, + contains: [{ begin: /./ }] + }, + // Lists + { + className: 'bullet', + begin: /^(\*+|\.+|-+)\s+/, + relevance: 0 + }, + // Links + { + className: 'link', + begin: /link:/, + end: /\[/, + contains: [{ begin: /\[/, end: /\]/ }] + }, + // Comments + { + className: 'comment', + begin: /^\/\/.*$/, + relevance: 0 + }, + // Attributes + { + className: 'attr', + begin: /^:.*:$/, + relevance: 0 + } + ] + }; + }); + } + + // Apply highlighting + if (lang === 'plaintext') { + highlightedFileContent = `
${hljs.highlight(content, { language: 'plaintext' }).value}
`; + } else if (hljs.getLanguage(lang)) { + highlightedFileContent = `
${hljs.highlight(content, { language: lang }).value}
`; + } else { + // Fallback to auto-detection + highlightedFileContent = `
${hljs.highlightAuto(content).value}
`; + } + } catch (err) { + console.error('Error applying syntax highlighting:', err); + // Fallback to plain text + highlightedFileContent = `
${content}
`; + } + } + async function loadForkInfo() { try { const response = await fetch(`/api/repos/${npub}/${repo}/fork`); @@ -387,6 +543,17 @@ } else { fileLanguage = 'text'; } + + // Apply syntax highlighting for read-only view (non-maintainers) + if (fileContent && !isMaintainer) { + await applySyntaxHighlighting(fileContent, ext || ''); + } + + // Apply syntax highlighting to file content if not in editor + if (fileContent && !isMaintainer) { + // For read-only view, apply highlight.js + await applySyntaxHighlighting(fileContent, ext || ''); + } } catch (err) { error = err instanceof Error ? err.message : 'Failed to load file'; console.error('Error loading file:', err); @@ -1211,7 +1378,9 @@ {@html readmeHtml} {:else if readmeContent} -
{readmeContent}
+
+
{readmeContent}
+
{/if} {/if} @@ -1245,7 +1414,11 @@ /> {:else}
-
{editedContent}
+ {#if highlightedFileContent} + {@html highlightedFileContent} + {:else} +
{fileContent}
+ {/if}
{/if} @@ -2236,6 +2409,42 @@ white-space: pre; } + .read-only-editor { + height: 100%; + overflow: auto; + } + + .read-only-editor :global(.hljs) { + padding: 1rem; + background: #1e1e1e; + color: #d4d4d4; + border-radius: 4px; + overflow-x: auto; + margin: 0; + } + + .read-only-editor :global(pre) { + margin: 0; + padding: 0; + } + + .read-only-editor :global(code) { + font-family: 'Courier New', Courier, monospace; + font-size: 14px; + line-height: 1.5; + } + + .readme-content :global(.hljs) { + background: #f5f5f5; + padding: 1rem; + border-radius: 4px; + overflow-x: auto; + } + + .readme-content :global(pre.hljs) { + margin: 1rem 0; + } + /* Issues and PRs */ .issues-sidebar, .prs-sidebar { width: 300px;