[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.

feat: render markdown in code view (#403)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

authored by

Leo
autofix-ci[bot]
and committed by
GitHub
f80f5a57 cbbd0222

+144 -8
+60 -8
app/pages/code/[...path].vue
··· 243 243 return url 244 244 }) 245 245 246 + // Toggle markdown view mode 247 + const markdownViewModes = [ 248 + { 249 + key: 'preview', 250 + label: $t('code.markdown_view_mode.preview'), 251 + icon: 'i-carbon-view', 252 + }, 253 + { 254 + key: 'code', 255 + label: $t('code.markdown_view_mode.code'), 256 + icon: 'i-carbon-code', 257 + }, 258 + ] as const 259 + 260 + const markdownViewMode = ref<(typeof markdownViewModes)[number]['key']>('preview') 261 + 246 262 useHead({ 247 263 link: [{ rel: 'canonical', href: canonicalUrl }], 248 264 }) ··· 359 375 <!-- File viewer --> 360 376 <template v-if="isViewingFile && fileContent"> 361 377 <div 362 - class="sticky top-0 bg-bg border-b border-border px-4 py-2 flex items-center justify-between" 378 + class="sticky z-10 top-0 bg-bg border-b border-border px-4 py-2 flex items-center justify-between" 363 379 > 364 - <div class="flex items-center gap-3 text-sm"> 365 - <span class="text-fg-muted">{{ 366 - $t('code.lines', { count: fileContent.lines }) 367 - }}</span> 368 - <span v-if="currentNode?.size" class="text-fg-subtle">{{ 369 - formatBytes(currentNode.size) 370 - }}</span> 380 + <div class="flex items-center gap-2"> 381 + <div 382 + v-if="fileContent.markdownHtml" 383 + class="flex items-center gap-1 p-0.5 bg-bg-subtle border border-border-subtle rounded-md overflow-x-auto" 384 + role="tablist" 385 + aria-label="Markdown view mode selector" 386 + > 387 + <button 388 + v-for="mode in markdownViewModes" 389 + :key="mode.key" 390 + role="tab" 391 + class="px-2 py-1.5 font-mono text-xs rounded transition-colors duration-150 border border-solid focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 inline-flex items-center gap-1.5" 392 + :class=" 393 + markdownViewMode === mode.key 394 + ? 'bg-bg shadow text-fg border-border' 395 + : 'text-fg-subtle hover:text-fg border-transparent' 396 + " 397 + @click="markdownViewMode = mode.key" 398 + > 399 + <span class="inline-block h-3 w-3" :class="mode.icon" aria-hidden="true" /> 400 + {{ mode.label }} 401 + </button> 402 + </div> 403 + <div class="flex items-center gap-3 text-sm"> 404 + <span class="text-fg-muted">{{ 405 + $t('code.lines', { count: fileContent.lines }) 406 + }}</span> 407 + <span v-if="currentNode?.size" class="text-fg-subtle">{{ 408 + formatBytes(currentNode.size) 409 + }}</span> 410 + </div> 371 411 </div> 372 412 <div class="flex items-center gap-2"> 373 413 <button ··· 389 429 </a> 390 430 </div> 391 431 </div> 432 + <div 433 + v-if="fileContent.markdownHtml" 434 + v-show="markdownViewMode === 'preview'" 435 + class="flex justify-center p-4" 436 + > 437 + <div 438 + class="readme-content prose prose-invert max-w-[70ch]" 439 + v-html="fileContent.markdownHtml.html" 440 + ></div> 441 + </div> 442 + 392 443 <CodeViewer 444 + v-show="!fileContent.markdownHtml || markdownViewMode === 'code'" 393 445 :html="fileContent.html" 394 446 :lines="fileContent.lines" 395 447 :selected-lines="selectedLines"
+4
i18n/locales/ar.json
··· 532 532 "table": { 533 533 "name": "الاسم", 534 534 "size": "الحجم" 535 + }, 536 + "markdown_view_mode": { 537 + "preview": "معاينة", 538 + "code": "الكود" 535 539 } 536 540 }, 537 541 "badges": {
+4
i18n/locales/de-DE.json
··· 532 532 "table": { 533 533 "name": "Name", 534 534 "size": "Größe" 535 + }, 536 + "markdown_view_mode": { 537 + "preview": "Vorschau", 538 + "code": "Code" 535 539 } 536 540 }, 537 541 "badges": {
+4
i18n/locales/en.json
··· 550 550 "table": { 551 551 "name": "Name", 552 552 "size": "Size" 553 + }, 554 + "markdown_view_mode": { 555 + "preview": "preview", 556 + "code": "code" 553 557 } 554 558 }, 555 559 "badges": {
+4
i18n/locales/es.json
··· 535 535 "table": { 536 536 "name": "Nombre", 537 537 "size": "Tamaño" 538 + }, 539 + "markdown_view_mode": { 540 + "preview": "vista previa", 541 + "code": "código" 538 542 } 539 543 }, 540 544 "badges": {
+4
i18n/locales/fr-FR.json
··· 537 537 "table": { 538 538 "name": "Nom", 539 539 "size": "Taille" 540 + }, 541 + "markdown_view_mode": { 542 + "preview": "aperçu", 543 + "code": "code" 540 544 } 541 545 }, 542 546 "badges": {
+4
i18n/locales/it-IT.json
··· 537 537 "table": { 538 538 "name": "Nome", 539 539 "size": "Dimensione" 540 + }, 541 + "markdown_view_mode": { 542 + "preview": "anteprima", 543 + "code": "codice" 540 544 } 541 545 }, 542 546 "filters": {
+4
i18n/locales/ja-JP.json
··· 537 537 "table": { 538 538 "name": "名前", 539 539 "size": "サイズ" 540 + }, 541 + "markdown_view_mode": { 542 + "preview": "プレビュー", 543 + "code": "コード" 540 544 } 541 545 }, 542 546 "badges": {
+4
i18n/locales/zh-CN.json
··· 546 546 "table": { 547 547 "name": "名称", 548 548 "size": "大小" 549 + }, 550 + "markdown_view_mode": { 551 + "preview": "预览", 552 + "code": "代码" 549 553 } 550 554 }, 551 555 "badges": {
+4
lunaria/files/ar.json
··· 532 532 "table": { 533 533 "name": "الاسم", 534 534 "size": "الحجم" 535 + }, 536 + "markdown_view_mode": { 537 + "preview": "معاينة", 538 + "code": "الكود" 535 539 } 536 540 }, 537 541 "badges": {
+4
lunaria/files/de-DE.json
··· 532 532 "table": { 533 533 "name": "Name", 534 534 "size": "Größe" 535 + }, 536 + "markdown_view_mode": { 537 + "preview": "Vorschau", 538 + "code": "Code" 535 539 } 536 540 }, 537 541 "badges": {
+4
lunaria/files/en-US.json
··· 550 550 "table": { 551 551 "name": "Name", 552 552 "size": "Size" 553 + }, 554 + "markdown_view_mode": { 555 + "preview": "preview", 556 + "code": "code" 553 557 } 554 558 }, 555 559 "badges": {
+4
lunaria/files/es-419.json
··· 535 535 "table": { 536 536 "name": "Nombre", 537 537 "size": "Tamaño" 538 + }, 539 + "markdown_view_mode": { 540 + "preview": "vista previa", 541 + "code": "código" 538 542 } 539 543 }, 540 544 "badges": {
+4
lunaria/files/es-ES.json
··· 535 535 "table": { 536 536 "name": "Nombre", 537 537 "size": "Tamaño" 538 + }, 539 + "markdown_view_mode": { 540 + "preview": "vista previa", 541 + "code": "código" 538 542 } 539 543 }, 540 544 "badges": {
+4
lunaria/files/fr-FR.json
··· 537 537 "table": { 538 538 "name": "Nom", 539 539 "size": "Taille" 540 + }, 541 + "markdown_view_mode": { 542 + "preview": "aperçu", 543 + "code": "code" 540 544 } 541 545 }, 542 546 "badges": {
+4
lunaria/files/it-IT.json
··· 537 537 "table": { 538 538 "name": "Nome", 539 539 "size": "Dimensione" 540 + }, 541 + "markdown_view_mode": { 542 + "preview": "anteprima", 543 + "code": "codice" 540 544 } 541 545 }, 542 546 "filters": {
+4
lunaria/files/ja-JP.json
··· 537 537 "table": { 538 538 "name": "名前", 539 539 "size": "サイズ" 540 + }, 541 + "markdown_view_mode": { 542 + "preview": "プレビュー", 543 + "code": "コード" 540 544 } 541 545 }, 542 546 "badges": {
+4
lunaria/files/zh-CN.json
··· 546 546 "table": { 547 547 "name": "名称", 548 548 "size": "大小" 549 + }, 550 + "markdown_view_mode": { 551 + "preview": "预览", 552 + "code": "代码" 549 553 } 550 554 }, 551 555 "badges": {
+14
server/api/registry/file/[...pkg].get.ts
··· 1 1 import * as v from 'valibot' 2 2 import { PackageFileQuerySchema } from '#shared/schemas/package' 3 + import type { ReadmeResponse } from '#shared/types/readme' 3 4 import { 4 5 CACHE_MAX_AGE_ONE_YEAR, 5 6 ERROR_PACKAGE_VERSION_AND_FILE_FAILED, ··· 166 167 resolveRelative, 167 168 }) 168 169 170 + let markdownHtml: ReadmeResponse | undefined 171 + if (language === 'markdown') { 172 + // Best-effort: markdown preview is optional; never block code view 173 + try { 174 + const packageData = await fetchNpmPackage(rawPackageName) 175 + const repoInfo = parseRepositoryInfo(packageData.repository) 176 + markdownHtml = await renderReadmeHtml(content, rawPackageName, repoInfo) 177 + } catch { 178 + markdownHtml = undefined 179 + } 180 + } 181 + 169 182 return { 170 183 package: packageName, 171 184 version, ··· 174 187 content, 175 188 html, 176 189 lines: content.split('\n').length, 190 + markdownHtml, 177 191 } 178 192 } catch (error: unknown) { 179 193 handleApiError(error, {
+2
shared/types/npm-registry.ts
··· 7 7 */ 8 8 9 9 import type { PackumentVersion } from '@npm/types' 10 + import type { ReadmeResponse } from './readme' 10 11 11 12 // Re-export official npm types for packument/manifest 12 13 export type { ··· 325 326 content: string 326 327 html: string 327 328 lines: number 329 + markdownHtml?: ReadmeResponse 328 330 }