1- import { useEffect , useState , useRef , useCallback } from 'react'
1+ import React , { useEffect , useState , useRef , useCallback } from 'react'
22import { useParams , useNavigate , Link } from 'react-router-dom'
33import usePages from '../store/usePages'
44import useTags from '../store/useTags'
@@ -10,6 +10,7 @@ import TableOfContents from '../components/Viewer/TableOfContents'
1010import Comments from '../components/Comments'
1111import ConfirmDialog from '../components/ConfirmDialog'
1212import AclManager from '../components/AclManager'
13+ import { stripBrTags } from '../lib/markdown'
1314import api from '../api/client'
1415
1516export default function PageView ( ) {
@@ -141,6 +142,31 @@ export default function PageView() {
141142 setMenuOpen ( false )
142143 }
143144
145+ const handleCopyMarkdown = async ( ) => {
146+ try {
147+ await navigator . clipboard . writeText ( stripBrTags ( page . content_md ?? '' ) )
148+ setToast ( 'Markdown copied' )
149+ } catch {
150+ setToast ( 'Copy failed — clipboard requires HTTPS or localhost' )
151+ }
152+ setMenuOpen ( false )
153+ }
154+
155+ const handleDownloadMarkdown = ( ) => {
156+ const blob = new Blob ( [ stripBrTags ( page . content_md ?? '' ) ] , { type : 'text/markdown;charset=utf-8' } )
157+ const url = URL . createObjectURL ( blob )
158+ const a = document . createElement ( 'a' )
159+ a . href = url
160+ a . download = `${ page . slug } .md`
161+ document . body . appendChild ( a )
162+ a . click ( )
163+ document . body . removeChild ( a )
164+ // Defer revoke: some browsers start the download asynchronously after
165+ // click(), and revoking the blob URL too early can cancel it.
166+ setTimeout ( ( ) => URL . revokeObjectURL ( url ) , 0 )
167+ setMenuOpen ( false )
168+ }
169+
144170 const handleMakePrivate = async ( ) => {
145171 try {
146172 await api . put ( `/pages/${ slug } ` , { is_public : false } )
@@ -281,6 +307,27 @@ export default function PageView() {
281307 </ >
282308 ) }
283309 < div className = "border-t border-border my-1" />
310+ < button
311+ onClick = { handleCopyMarkdown }
312+ className = "w-full text-left px-3 py-2 text-sm text-text hover:bg-surface-hover flex items-center gap-2"
313+ >
314+ < svg className = "w-4 h-4 text-text-secondary" fill = "none" stroke = "currentColor" strokeWidth = "2" strokeLinecap = "round" strokeLinejoin = "round" viewBox = "0 0 24 24" >
315+ < rect x = "9" y = "9" width = "11" height = "11" rx = "2" />
316+ < path d = "M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
317+ </ svg >
318+ Copy Markdown
319+ </ button >
320+ < button
321+ onClick = { handleDownloadMarkdown }
322+ className = "w-full text-left px-3 py-2 text-sm text-text hover:bg-surface-hover flex items-center gap-2"
323+ >
324+ < svg className = "w-4 h-4 text-text-secondary" fill = "none" stroke = "currentColor" strokeWidth = "2" strokeLinecap = "round" strokeLinejoin = "round" viewBox = "0 0 24 24" >
325+ < path d = "M12 3v12" />
326+ < path d = "M7 10l5 5 5-5" />
327+ < path d = "M5 21h14" />
328+ </ svg >
329+ Download .md
330+ </ button >
284331 < button
285332 onClick = { ( ) => {
286333 setMenuOpen ( false )
0 commit comments