[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: add copy readme as markdown button on package page (#1058)

Co-authored-by: Willow (GHOST) <git@willow.sh>

authored by

Sybren W
Willow (GHOST)
and committed by
GitHub
c6e57983 3d3fb2ad

+89 -20
+40 -7
app/pages/package/[[org]]/[name].vue
··· 62 62 const version = requestedVersion.value 63 63 return version ? `${base}/v/${version}` : base 64 64 }, 65 - { default: () => ({ html: '', playgroundLinks: [], toc: [] }) }, 65 + { default: () => ({ html: '', md: '', playgroundLinks: [], toc: [] }) }, 66 66 ) 67 + 68 + //copy README file as Markdown 69 + const { copied: copiedReadme, copy: copyReadme } = useClipboard({ 70 + source: () => readmeData.value?.md ?? '', 71 + copiedDuring: 2000, 72 + }) 67 73 68 74 // Track active TOC item based on scroll position 69 75 const tocItems = computed(() => readmeData.value?.toc ?? []) ··· 1136 1142 </a> 1137 1143 </h2> 1138 1144 <ClientOnly> 1139 - <ReadmeTocDropdown 1140 - v-if="readmeData?.toc && readmeData.toc.length > 1" 1141 - :toc="readmeData.toc" 1142 - :active-id="activeTocId" 1143 - :scroll-to-heading="scrollToHeading" 1144 - /> 1145 + <div class="flex items-center gap-2"> 1146 + <!-- Copy readme as Markdown button --> 1147 + <TooltipApp 1148 + v-if="readmeData?.md" 1149 + :text="$t('package.readme.copy_as_markdown')" 1150 + position="bottom" 1151 + > 1152 + <button 1153 + type="button" 1154 + @click="copyReadme()" 1155 + class="px-2 py-1.5 font-mono text-xs rounded transition-colors duration-150 inline-flex items-center gap-1.5" 1156 + :class=" 1157 + copiedReadme ? 'text-accent bg-accent/10' : 'text-fg-subtle bg-bg hover:text-fg' 1158 + " 1159 + :aria-label=" 1160 + copiedReadme ? $t('common.copied') : $t('package.readme.copy_as_markdown') 1161 + " 1162 + > 1163 + <span 1164 + :class="copiedReadme ? 'i-carbon:checkmark' : 'i-simple-icons:markdown'" 1165 + class="size-3" 1166 + aria-hidden="true" 1167 + /> 1168 + {{ copiedReadme ? $t('common.copied') : $t('common.copy') }} 1169 + </button> 1170 + </TooltipApp> 1171 + <ReadmeTocDropdown 1172 + v-if="readmeData?.toc && readmeData.toc.length > 1" 1173 + :toc="readmeData.toc" 1174 + :active-id="activeTocId" 1175 + :scroll-to-heading="scrollToHeading" 1176 + /> 1177 + </div> 1145 1178 </ClientOnly> 1146 1179 </div> 1147 1180
+2 -1
i18n/locales/de-DE.json
··· 221 221 "important": "Wichtig", 222 222 "warning": "Warnung", 223 223 "caution": "Vorsicht" 224 - } 224 + }, 225 + "copy_as_markdown": "README als Markdown kopieren" 225 226 }, 226 227 "provenance_section": { 227 228 "title": "Herkunft",
+2 -1
i18n/locales/en.json
··· 221 221 "important": "Important", 222 222 "warning": "Warning", 223 223 "caution": "Caution" 224 - } 224 + }, 225 + "copy_as_markdown": "Copy README as Markdown" 225 226 }, 226 227 "provenance_section": { 227 228 "title": "Provenance",
+2 -1
i18n/locales/es.json
··· 208 208 "title": "Léeme", 209 209 "no_readme": "No hay README disponible.", 210 210 "view_on_github": "Ver en GitHub", 211 - "toc_title": "Índice" 211 + "toc_title": "Índice", 212 + "copy_as_markdown": "Copiar README como Markdown" 212 213 }, 213 214 "keywords_title": "Palabras clave", 214 215 "compatibility": "Compatibilidad",
+2 -1
i18n/locales/fr-FR.json
··· 206 206 "title": "Readme", 207 207 "no_readme": "Aucun README disponible.", 208 208 "view_on_github": "Voir sur GitHub", 209 - "toc_title": "Sommaire" 209 + "toc_title": "Sommaire", 210 + "copy_as_markdown": "Copier le README en markdown" 210 211 }, 211 212 "keywords_title": "Mots-clés", 212 213 "compatibility": "Compatibilité",
+2 -1
i18n/locales/it-IT.json
··· 221 221 "important": "Importante", 222 222 "warning": "Avvertenza", 223 223 "caution": "Cautela" 224 - } 224 + }, 225 + "copy_as_markdown": "Copia README come Markdown" 225 226 }, 226 227 "provenance_section": { 227 228 "title": "Provenienza",
+2 -1
lunaria/files/de-DE.json
··· 221 221 "important": "Wichtig", 222 222 "warning": "Warnung", 223 223 "caution": "Vorsicht" 224 - } 224 + }, 225 + "copy_as_markdown": "README als Markdown kopieren" 225 226 }, 226 227 "provenance_section": { 227 228 "title": "Herkunft",
+2 -1
lunaria/files/en-GB.json
··· 221 221 "important": "Important", 222 222 "warning": "Warning", 223 223 "caution": "Caution" 224 - } 224 + }, 225 + "copy_as_markdown": "Copy README as Markdown" 225 226 }, 226 227 "provenance_section": { 227 228 "title": "Provenance",
+2 -1
lunaria/files/en-US.json
··· 221 221 "important": "Important", 222 222 "warning": "Warning", 223 223 "caution": "Caution" 224 - } 224 + }, 225 + "copy_as_markdown": "Copy README as Markdown" 225 226 }, 226 227 "provenance_section": { 227 228 "title": "Provenance",
+2 -1
lunaria/files/es-419.json
··· 208 208 "title": "Léame", 209 209 "no_readme": "No hay README disponible.", 210 210 "view_on_github": "Ver en GitHub", 211 - "toc_title": "Índice" 211 + "toc_title": "Índice", 212 + "copy_as_markdown": "Copiar README como Markdown" 212 213 }, 213 214 "keywords_title": "Palabras clave", 214 215 "compatibility": "Compatibilidad",
+2 -1
lunaria/files/es-ES.json
··· 208 208 "title": "Léeme", 209 209 "no_readme": "No hay README disponible.", 210 210 "view_on_github": "Ver en GitHub", 211 - "toc_title": "Índice" 211 + "toc_title": "Índice", 212 + "copy_as_markdown": "Copiar README como Markdown" 212 213 }, 213 214 "keywords_title": "Palabras clave", 214 215 "compatibility": "Compatibilidad",
+2 -1
lunaria/files/fr-FR.json
··· 206 206 "title": "Readme", 207 207 "no_readme": "Aucun README disponible.", 208 208 "view_on_github": "Voir sur GitHub", 209 - "toc_title": "Sommaire" 209 + "toc_title": "Sommaire", 210 + "copy_as_markdown": "Copier le README en markdown" 210 211 }, 211 212 "keywords_title": "Mots-clés", 212 213 "compatibility": "Compatibilité",
+2 -1
lunaria/files/it-IT.json
··· 221 221 "important": "Importante", 222 222 "warning": "Avvertenza", 223 223 "caution": "Cautela" 224 - } 224 + }, 225 + "copy_as_markdown": "Copia README come Markdown" 225 226 }, 226 227 "provenance_section": { 227 228 "title": "Provenienza",
+2 -1
server/utils/readme.ts
··· 277 277 packageName: string, 278 278 repoInfo?: RepositoryInfo, 279 279 ): Promise<ReadmeResponse> { 280 - if (!content) return { html: '', playgroundLinks: [], toc: [] } 280 + if (!content) return { html: '', md: '', playgroundLinks: [], toc: [] } 281 281 282 282 const shiki = await getShikiHighlighter() 283 283 const renderer = new marked.Renderer() ··· 455 455 456 456 return { 457 457 html: convertToEmoji(sanitized), 458 + md: content, 458 459 playgroundLinks: collectedLinks, 459 460 toc, 460 461 }
+2
shared/types/readme.ts
··· 30 30 export interface ReadmeResponse { 31 31 /** Rendered HTML content */ 32 32 html: string 33 + /** Original markdown content */ 34 + md: string 33 35 /** Extracted playground/demo links */ 34 36 playgroundLinks: PlaygroundLink[] 35 37 /** Table of contents extracted from headings */
+21
test/unit/server/utils/readme.spec.ts
··· 307 307 }) 308 308 }) 309 309 }) 310 + 311 + describe('Markdown Content Extraction', () => { 312 + describe('Markdown', () => { 313 + it('returns original markdown content unchanged', async () => { 314 + const markdown = `# Title\n\nSome **bold** text and a [link](https://example.com).` 315 + const result = await renderReadmeHtml(markdown, 'test-pkg') 316 + 317 + expect(result.md).toBe(markdown) 318 + }) 319 + }) 320 + describe('HTML', () => { 321 + it('returns sanitized html', async () => { 322 + const markdown = `# Title\n\nSome **bold** text and a [link](https://example.com).` 323 + const result = await renderReadmeHtml(markdown, 'test-pkg') 324 + 325 + expect(result.html).toBe(`<h3 id="user-content-title" data-level="1">Title</h3> 326 + <p>Some <strong>bold</strong> text and a <a href="https://example.com" rel="nofollow noreferrer noopener" target="_blank">link</a>.</p> 327 + `) 328 + }) 329 + }) 330 + })