1- import React , { useEffect , useMemo , useState } from "react"
1+ import React , { useEffect , useState } from "react"
22
33import { Button , CircularProgress , Link , Tooltip } from "@heroui/react"
44import binaryExtensions from "binary-extensions"
@@ -14,6 +14,9 @@ import { formatSize } from "../utils/utils.js"
1414import { tst } from "../utils/overrides.js"
1515
1616import "../style.css"
17+ import "../styles/highlight-theme-light.css"
18+ import "../styles/highlight-theme-dark.css"
19+ import { highlightHTML , useHLJS } from "../utils/HighlightLoader.js"
1720
1821function isBinaryPath ( path : string ) {
1922 return binaryExtensions . includes ( path . replace ( / .* \. / , "" ) )
@@ -22,31 +25,30 @@ function isBinaryPath(path: string) {
2225export function DecryptPaste ( ) {
2326 const [ pasteFile , setPasteFile ] = useState < File | undefined > ( undefined )
2427 const [ pasteContentBuffer , setPasteContentBuffer ] = useState < ArrayBuffer | undefined > ( undefined )
28+ const [ pasteLang , setPasteLang ] = useState < string | undefined > ( undefined )
2529
2630 const [ isFileBinary , setFileBinary ] = useState ( false )
31+ const [ isDecrypted , setDecrypted ] = useState ( false )
2732 const [ forceShowBinary , setForceShowBinary ] = useState ( false )
2833 const showFileContent = pasteFile !== undefined && ( ! isFileBinary || forceShowBinary )
2934
3035 const [ isLoading , setIsLoading ] = useState < boolean > ( false )
3136
3237 const { ErrorModal, showModal, handleFailedResp } = useErrorModal ( )
33- const [ isDark , modeSelection , setModeSelection ] = useDarkModeSelection ( )
38+ const [ _ , modeSelection , setModeSelection ] = useDarkModeSelection ( )
39+ const hljs = useHLJS ( )
3440
35- const pasteStringContent = useMemo < string | undefined > ( ( ) => {
36- return pasteContentBuffer && new TextDecoder ( ) . decode ( pasteContentBuffer )
37- } , [ pasteContentBuffer ] )
41+ const pasteStringContent = pasteContentBuffer && new TextDecoder ( ) . decode ( pasteContentBuffer )
42+
43+ const pasteLineCount = ( pasteStringContent ?. match ( / \n / g ) ?. length || 0 ) + 1
3844
3945 // uncomment the following lines for testing
40- // const url = new URL("http://localhost:8787/d/dHYQ.jpg.txt#uqeULsBTb2I3iC7rD6AaYh4oJ5lMjJA2nYR+H0U8bEA= ")
46+ // const url = new URL("http://localhost:8787/GQbf ")
4147 const url = location
4248
4349 const { name, ext, filename } = parsePath ( url . pathname )
44- const keyString = url . hash . slice ( 1 )
4550
4651 useEffect ( ( ) => {
47- if ( keyString . length === 0 ) {
48- showModal ( "Error" , "No encryption key is given. You should append the key after a “#” character in the URL" )
49- }
5052 const pasteUrl = `${ API_URL } /${ name } `
5153
5254 const fetchPaste = async ( ) => {
@@ -59,42 +61,54 @@ export function DecryptPaste() {
5961 }
6062
6163 const scheme : EncryptionScheme | null = resp . headers . get ( "X-PB-Encryption-Scheme" ) as EncryptionScheme | null
62- if ( scheme === null ) {
63- showModal ( "Error" , "No encryption scheme is given by the server" )
64- return
65- }
66- let key : CryptoKey | undefined
67- try {
68- key = await decodeKey ( scheme , keyString )
69- } catch {
70- showModal ( "Error" , `Failed to parse “${ keyString } ” as ${ scheme } key` )
71- return
72- }
73- if ( key === undefined ) {
74- showModal ( "Error" , `Failed to parse “${ keyString } ” as ${ scheme } key` )
75- return
64+ let filenameFromDisp = resp . headers . has ( "Content-Disposition" )
65+ ? parseFilenameFromContentDisposition ( resp . headers . get ( "Content-Disposition" ) ! ) || undefined
66+ : undefined
67+ if ( filenameFromDisp && scheme !== null ) {
68+ filenameFromDisp = filenameFromDisp . replace ( / .e n c r y p t e d $ / , "" )
7669 }
7770
78- const decrypted = await decrypt ( scheme , key , await resp . bytes ( ) )
79- if ( decrypted === null ) {
80- showModal ( "Error" , "Failed to decrypt content" )
81- } else {
82- const filenameFromDispTrimmed = resp . headers . has ( "Content-Disposition" )
83- ? parseFilenameFromContentDisposition ( resp . headers . get ( "Content-Disposition" ) ! ) ?. replace (
84- / .e n c r y p t e d $ / g,
85- "" ,
86- ) || undefined
87- : undefined
71+ const lang = resp . headers . get ( "X-PB-Highlight-Language" )
72+
73+ const inferredFilename = filename || ( ext && name + ext ) || filenameFromDisp
74+ const respBytes = await resp . bytes ( )
75+ const isBinary = lang === null && inferredFilename !== undefined && isBinaryPath ( inferredFilename )
76+ setPasteLang ( lang || undefined )
77+ setFileBinary ( isBinary )
8878
89- // TODO: highlight with lang
90- const lang = resp . headers . get ( "X-PB-Highlight-Language" )
79+ if ( scheme === null ) {
80+ setPasteFile ( new File ( [ respBytes ] , inferredFilename || name ) )
81+ setPasteContentBuffer ( respBytes )
82+ } else {
83+ const keyString = url . hash . slice ( 1 )
84+ if ( keyString . length === 0 ) {
85+ showModal ( "Error" , "No encryption key is given. You should append the key after a “#” character in the URL" )
86+ }
87+ let key : CryptoKey | undefined
88+ try {
89+ key = await decodeKey ( scheme , keyString )
90+ } catch {
91+ showModal ( "Error" , `Failed to parse “${ keyString } ” as ${ scheme } key` )
92+ return
93+ }
94+ if ( key === undefined ) {
95+ showModal ( "Error" , `Failed to parse “${ keyString } ” as ${ scheme } key` )
96+ return
97+ }
98+
99+ const decrypted = await decrypt ( scheme , key , respBytes )
100+ if ( decrypted === null ) {
101+ showModal ( "Error" , "Failed to decrypt content" )
102+ return
103+ }
91104
92- const inferredFilename = filename || ( ext && name + ext ) || filenameFromDispTrimmed
93105 setPasteFile ( new File ( [ decrypted ] , inferredFilename || name ) )
94106 setPasteContentBuffer ( decrypted )
107+ setPasteLang ( lang || undefined )
95108
96109 const isBinary = lang === null && inferredFilename !== undefined && isBinaryPath ( inferredFilename )
97110 setFileBinary ( isBinary )
111+ setDecrypted ( true )
98112 }
99113 } finally {
100114 setIsLoading ( false )
@@ -108,7 +122,7 @@ export function DecryptPaste() {
108122
109123 const fileIndicator = pasteFile && (
110124 < div className = "text-foreground-600 mb-2 text-small" >
111- { `${ pasteFile ?. name } (${ formatSize ( pasteFile . size ) } )` }
125+ { `${ pasteFile ?. name } (${ formatSize ( pasteFile . size ) } )` + ( pasteLang ? ` ( ${ pasteLang } )` : "" ) }
112126 { forceShowBinary && (
113127 < button className = "ml-2 text-primary-500" onClick = { ( ) => setForceShowBinary ( false ) } >
114128 (Click to hide)
@@ -132,10 +146,7 @@ export function DecryptPaste() {
132146 const buttonClasses = `rounded-full bg-background hover:bg-default-100 ${ tst } `
133147 return (
134148 < main
135- className = {
136- `flex flex-col items-center min-h-screen transition-transform-background bg-background ${ tst } text-foreground w-full p-2` +
137- ( isDark ? " dark" : " light" )
138- }
149+ className = { `flex flex-col items-center min-h-screen transition-transform-background bg-background ${ tst } text-foreground w-full p-2` }
139150 >
140151 < div className = "w-full max-w-[64rem]" >
141152 < div className = "flex flex-row my-4 items-center justify-between" >
@@ -148,7 +159,7 @@ export function DecryptPaste() {
148159 </ Link >
149160 < span className = "mx-2" > { " / " } </ span >
150161 < code > { name } </ code >
151- < span className = "ml-1" > { isLoading ? " (Loading…)" : pasteFile ? " (Decrypted)" : "" } </ span >
162+ < span className = "ml-1" > { isLoading ? " (Loading…)" : isDecrypted ? " (Decrypted)" : "" } </ span >
152163 </ h1 >
153164 { showFileContent && (
154165 < Tooltip content = { `Copy to clipboard` } >
@@ -167,7 +178,7 @@ export function DecryptPaste() {
167178 < DarkModeToggle modeSelection = { modeSelection } setModeSelection = { setModeSelection } />
168179 </ div >
169180 < div className = "my-4" >
170- < div className = { `min-h-[30rem] w-full bg-secondary -50 rounded-lg p-3 relative ${ tst } ` } >
181+ < div className = { `min-h-[30rem] w-full bg-default -50 rounded-lg p-3 relative ${ tst } ` } >
171182 { isLoading ? (
172183 < CircularProgress className = "absolute top-[50%] left-[50%] translate-[-50%]" />
173184 ) : (
@@ -176,8 +187,21 @@ export function DecryptPaste() {
176187 { showFileContent ? (
177188 < >
178189 { fileIndicator }
179- < div className = "font-mono whitespace-pre-wrap" role = "article" >
180- { pasteStringContent ! }
190+ < div className = "font-mono whitespace-pre-wrap relative" role = "article" >
191+ < pre
192+ style = { { paddingLeft : `${ Math . floor ( Math . log10 ( pasteLineCount ) ) + 2 } em` } }
193+ dangerouslySetInnerHTML = { { __html : highlightHTML ( hljs , pasteLang , pasteStringContent ! ) } }
194+ />
195+ < span
196+ className = {
197+ "line-number-rows absolute pointer-events-none text-default-500 top-0 left-0 " +
198+ "border-solid border-default-300 border-r-1"
199+ }
200+ >
201+ { Array . from ( { length : pasteLineCount } , ( _ , idx ) => {
202+ return < span key = { idx } />
203+ } ) }
204+ </ span >
181205 </ div >
182206 </ >
183207 ) : (
0 commit comments