Something went wrong. Try again.
[READ-ONLY]
a fast, modern browser for the npm registry forked from npmx.dev/npmx.dev
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335import { isBuiltin } from 'node:module'// File extension to language mappingconst EXTENSION_MAP: Record<string, string> = { // JavaScript/TypeScript js: 'javascript', mjs: 'javascript', cjs: 'javascript', ts: 'typescript', mts: 'typescript', cts: 'typescript', jsx: 'jsx', tsx: 'tsx',
// Web html: 'html', htm: 'html', css: 'css', scss: 'scss', sass: 'scss', less: 'less', vue: 'vue', svelte: 'svelte', astro: 'astro', gjs: 'glimmer-js', gts: 'glimmer-ts',
// Data formats json: 'json', jsonc: 'jsonc', json5: 'jsonc', yaml: 'yaml', yml: 'yaml', toml: 'toml', xml: 'xml', svg: 'xml',
// Shell sh: 'bash', bash: 'bash', zsh: 'bash', fish: 'bash',
// Docs md: 'markdown', mdx: 'markdown', markdown: 'markdown',
// Other languages py: 'python', rs: 'rust', go: 'go', sql: 'sql', graphql: 'graphql', gql: 'graphql', diff: 'diff', patch: 'diff',}
// Special filenames that have specific languagesconst FILENAME_MAP: Record<string, string> = { '.gitignore': 'bash', '.npmignore': 'bash', '.editorconfig': 'toml', '.prettierrc': 'json', '.eslintrc': 'json', 'tsconfig.json': 'jsonc', 'jsconfig.json': 'jsonc', 'package.json': 'json', 'package-lock.json': 'json', 'pnpm-lock.yaml': 'yaml', 'yarn.lock': 'yaml', 'Makefile': 'bash', 'Dockerfile': 'bash', 'LICENSE': 'text', 'CHANGELOG': 'markdown', 'CHANGELOG.md': 'markdown', 'README': 'markdown', 'README.md': 'markdown', 'README.markdown': 'markdown',}
/** * Determine the language for syntax highlighting based on file path */export function getLanguageFromPath(filePath: string): string { const filename = filePath.split('/').pop() || ''
// Check for exact filename match first if (FILENAME_MAP[filename]) { return FILENAME_MAP[filename] }
// Then check extension const ext = filename.split('.').pop()?.toLowerCase() || '' return EXTENSION_MAP[ext] || 'text'}
/** * Check if a module specifier is an npm package (not a relative/absolute path or Node built-in) */function isNpmPackage(specifier: string): boolean { // Remove quotes const pkg = specifier.replace(/^['"]|['"]$/g, '').trim() // Relative or absolute paths if (pkg.startsWith('.') || pkg.startsWith('/')) return false // Node built-ins with node: prefix if (pkg.startsWith('node:')) return false // Node built-ins without prefix if (isBuiltin(pkg)) return false // Empty if (!pkg) return false return true}
/** * Extract the package name from a module specifier (handles scoped packages and subpaths) */function getPackageName(specifier: string): string { const pkg = specifier.replace(/^['"]|['"]$/g, '').trim() // Scoped package: @scope/name or @scope/name/subpath if (pkg.startsWith('@')) { const parts = pkg.split('/') if (parts[0] && parts[1]) { return `${parts[0]}/${parts[1]}` } } // Regular package: name or name/subpath const firstSlash = pkg.indexOf('/') if (firstSlash > 0) { return pkg.substring(0, firstSlash) } return pkg}
/** * Resolved dependency info for linking imports to specific versions */export interface ResolvedDependency { version: string}
/** * Map of package name to resolved version for import linking */export type DependencyVersions = Record<string, ResolvedDependency>
/** * Function to resolve relative imports to URLs */export type RelativeImportResolver = (specifier: string) => string | null
interface LinkifyOptions { dependencies?: DependencyVersions resolveRelative?: RelativeImportResolver}
/** * Make import/export module specifiers clickable links to package code browser. * Handles: * - import ... from 'package' * - export ... from 'package' * - import 'package' (side-effect imports) * - require('package') * - import('package') - dynamic imports * - Relative imports (./foo, ../bar) when resolver is provided * * @param html - The HTML to process * @param options - Dependencies map and optional relative import resolver */function linkifyImports(html: string, options?: LinkifyOptions): string { const { dependencies, resolveRelative } = options ?? {}
const getHref = (moduleSpecifier: string): string | null => { const cleanSpec = moduleSpecifier.replace(/^['"]|['"]$/g, '').trim()
// Try relative import resolution first if (cleanSpec.startsWith('.') && resolveRelative) { return resolveRelative(moduleSpecifier) }
// Not a relative import - check if it's an npm package if (!isNpmPackage(moduleSpecifier)) { return null }
const packageName = getPackageName(moduleSpecifier) const dep = dependencies?.[packageName] if (dep) { // Link to code browser with resolved version return `/package-code/${packageName}/v/${dep.version}` } // Fall back to package page if not a known dependency return `/package/${packageName}` }
// Match: from keyword span followed by string span containing module specifier // Pattern: <span style="...">from</span><span style="..."> 'module'</span> let result = html.replace( /(<span[^>]*>from<\/span>)(<span[^>]*>) (['"][^'"]+['"])<\/span>/g, (match, fromSpan, stringSpanOpen, moduleSpecifier) => { const href = getHref(moduleSpecifier) if (!href) return match return `${fromSpan}${stringSpanOpen} <a href="${href}" class="import-link">${moduleSpecifier}</a></span>` }, )
// Match: side-effect imports like `import 'package'` // Pattern: <span>import</span><span> 'module'</span> // But NOT: import ... from, import(, or import { result = result.replace( /(<span[^>]*>import<\/span>)(<span[^>]*>) (['"][^'"]+['"])<\/span>/g, (match, importSpan, stringSpanOpen, moduleSpecifier) => { const href = getHref(moduleSpecifier) if (!href) return match return `${importSpan}${stringSpanOpen} <a href="${href}" class="import-link">${moduleSpecifier}</a></span>` }, )
// Match: require( or import( followed by string // Pattern: <span> require</span><span>(</span><span>'module'</span> // or: <span>import</span><span>(</span><span>'module'</span> // Note: require often has a leading space in the span from Shiki result = result.replace( /(<span[^>]*>)(\s*)(require|import)(<\/span>)(<span[^>]*>\(<\/span>)(<span[^>]*>)(['"][^'"]+['"])<\/span>/g, ( match, spanOpen, whitespace, keyword, spanClose, parenSpan, stringSpanOpen, moduleSpecifier, ) => { const href = getHref(moduleSpecifier) if (!href) return match return `${spanOpen}${whitespace}${keyword}${spanClose}${parenSpan}${stringSpanOpen}<a href="${href}" class="import-link">${moduleSpecifier}</a></span>` }, )
return result}
// Languages that support import/export statementsconst IMPORT_LANGUAGES = new Set([ 'javascript', 'typescript', 'jsx', 'tsx', 'vue', 'svelte', 'astro',])
export interface HighlightOptions { /** Map of dependency names to resolved versions for import linking */ dependencies?: DependencyVersions /** Resolver function for relative imports (./foo, ../bar) */ resolveRelative?: RelativeImportResolver}
/** * Highlight code using Shiki with line-by-line output for line highlighting. * Each line is wrapped in a span.line for individual line highlighting. */export async function highlightCode( code: string, language: string, options?: HighlightOptions,): Promise<string> { const shiki = await getShikiHighlighter() const loadedLangs = shiki.getLoadedLanguages()
// Use Shiki if language is loaded if (loadedLangs.includes(language as never)) { try { let html = shiki.codeToHtml(code, { lang: language, themes: { light: 'github-light', dark: 'github-dark' }, defaultColor: 'dark', })
// Shiki doesn't encode > in text content (e.g., arrow functions) html = escapeRawGt(html)
// Make import statements clickable for JS/TS languages if (IMPORT_LANGUAGES.has(language)) { html = linkifyImports(html, { dependencies: options?.dependencies, resolveRelative: options?.resolveRelative, }) }
// Check if Shiki already outputs .line spans (newer versions do) if (html.includes('<span class="line">')) { // Shiki already wraps lines, but they're separated by newlines // We need to remove the newlines since display:block handles line breaks // Replace newlines between </span> and <span class="line"> with nothing return html.replace(/<\/span>\n<span class="line">/g, '</span><span class="line">') }
// Older Shiki without .line spans - wrap manually const codeMatch = html.match(/<code[^>]*>([\s\S]*)<\/code>/) if (codeMatch?.[1]) { const codeContent = codeMatch[1] const lines = codeContent.split('\n') const wrappedLines = lines .map((line: string, i: number) => { if (i === lines.length - 1 && line === '') return null return `<span class="line">${line}</span>` }) .filter((line: string | null): line is string => line !== null) .join('')
return html.replace(codeMatch[1], wrappedLines) }
return html } catch { // Fall back to plain } }
// Plain code for unknown languages - also wrap lines const lines = code.split('\n') const wrappedLines = lines .map(line => { const escaped = line.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') return `<span class="line">${escaped}</span>` }) .join('') // No newlines - display:block handles it
return `<pre class="shiki github-dark"><code>${wrappedLines}</code></pre>`}