[READ-ONLY] a fast, modern browser for the npm registry
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: anchor navigation in details README (#254)

authored by

Florian Heuberger and committed by
GitHub
4f63c4c7 ebf4a2e3

+48 -2
+9
app/assets/main.css
··· 58 58 -webkit-font-smoothing: antialiased; 59 59 -moz-osx-font-smoothing: grayscale; 60 60 text-rendering: optimizeLegibility; 61 + scroll-behavior: smooth; 62 + scroll-padding-top: 5rem; /* Offset for fixed header - otherwise anchor headers are cutted */ 63 + } 64 + 65 + /* Disable smooth scrolling if user prefers reduced motion */ 66 + @media (prefers-reduced-motion: reduce) { 67 + html { 68 + scroll-behavior: auto; 69 + } 61 70 } 62 71 63 72 /*
+39 -2
server/utils/readme.ts
··· 158 158 // Format: > [!NOTE], > [!TIP], > [!IMPORTANT], > [!WARNING], > [!CAUTION] 159 159 160 160 /** 161 + * Generate a GitHub-style slug from heading text. 162 + * - Convert to lowercase 163 + * - Remove HTML tags 164 + * - Replace spaces with hyphens 165 + * - Remove special characters (keep alphanumeric, hyphens, underscores) 166 + * - Collapse multiple hyphens 167 + */ 168 + function slugify(text: string): string { 169 + return text 170 + .replace(/<[^>]*>/g, '') // Strip HTML tags 171 + .toLowerCase() 172 + .trim() 173 + .replace(/\s+/g, '-') // Spaces to hyphens 174 + .replace(/[^\w\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff-]/g, '') // Keep alphanumeric, CJK, hyphens 175 + .replace(/-+/g, '-') // Collapse multiple hyphens 176 + .replace(/^-|-$/g, '') // Trim leading/trailing hyphens 177 + } 178 + 179 + /** 161 180 * Resolve a relative URL to an absolute URL. 162 181 * If repository info is available, resolve to provider's raw file URLs. 163 182 * Otherwise, fall back to jsdelivr CDN. ··· 165 184 function resolveUrl(url: string, packageName: string, repoInfo?: RepositoryInfo): string { 166 185 if (!url) return url 167 186 if (url.startsWith('#')) { 168 - return url 187 + // Prefix anchor links to match heading IDs (avoids collision with page IDs) 188 + return `#user-content-${url.slice(1)}` 169 189 } 170 190 if (hasProtocol(url, { acceptRelative: true })) { 171 191 try { ··· 240 260 const collectedLinks: PlaygroundLink[] = [] 241 261 const seenUrls = new Set<string>() 242 262 263 + // Track used heading slugs to handle duplicates (GitHub-style: foo, foo-1, foo-2) 264 + const usedSlugs = new Map<string, number>() 265 + 243 266 // Track heading hierarchy to ensure sequential order for accessibility 244 267 // Page h1 = package name, h2 = "Readme" section heading 245 268 // So README starts at h3, and we ensure no levels are skipped ··· 262 285 263 286 lastSemanticLevel = semanticLevel 264 287 const text = this.parser.parseInline(tokens) 265 - return `<h${semanticLevel} data-level="${depth}">${text}</h${semanticLevel}>\n` 288 + 289 + // Generate GitHub-style slug for anchor links 290 + let slug = slugify(text) 291 + if (!slug) slug = 'heading' // Fallback for empty headings 292 + 293 + // Handle duplicate slugs (GitHub-style: foo, foo-1, foo-2) 294 + const count = usedSlugs.get(slug) ?? 0 295 + usedSlugs.set(slug, count + 1) 296 + const uniqueSlug = count === 0 ? slug : `${slug}-${count}` 297 + 298 + // Prefix with 'user-content-' to avoid collisions with page IDs 299 + // (e.g., #install, #dependencies, #versions are used by the package page) 300 + const id = `user-content-${uniqueSlug}` 301 + 302 + return `<h${semanticLevel} id="${id}" data-level="${depth}">${text}</h${semanticLevel}>\n` 266 303 } 267 304 268 305 // Syntax highlighting for code blocks (uses shared highlighter)