Skip to content

Commit 61b4d2d

Browse files
committed
feat: improve support for highlight and filename
1 parent b44d280 commit 61b4d2d

File tree

15 files changed

+143
-56
lines changed

15 files changed

+143
-56
lines changed

doc/api.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ The `Content-Type` header is set to the mime type inferred from the filename of
1212

1313
The `Content-Disposition` header is set to `inline` by default. But can be overriden by `?a` query string. If the paste is uploaded with filename, or `<filename>` is set in given request URL, `Content-Disposition` is appended with `filename*` indicating the filename. If the paste is encrypted, the filename is appended with `.encrypted` suffix.
1414

15-
If the paste is encrypted, an `X-Encryption-Scheme` header will be set to the encryption scheme.
15+
If the paste is encrypted, an `X-PB-Encryption-Scheme` header will be set to the encryption scheme.
16+
17+
If the paste is uploaded with a `lang` parameter, an `X-PB-Highlight-Language` header will be set to the encryption scheme.
1618

1719
- `?a=`: optional. Set `Content-Disposition` to `attachment` if present.
1820

@@ -190,7 +192,9 @@ Upload your paste. It accept parameters in form-data:
190192

191193
- `p`: optional. The flag of **private mode**. If specified to any value, the name of the paste is as long as 24 characters. No effect if `n` is used.
192194
-
193-
- `encryption-scheme`: optional. The encryption scheme used in the uploaded paste. It will be returned as `X-Encryption-Scheme` header on fetching paste. Note that this is not the encryption scheme that the backend will perform.
195+
- `encryption-scheme`: optional. The encryption scheme used in the uploaded paste. It will be returned as `X-PB-Encryption-Scheme` header on fetching paste. Note that this is not the encryption scheme that the backend will perform.
196+
197+
- `lang`: optional. The language of the uploaded paste for syntax highlighting. Should be a lower-case name of language listed in [highlight.js documentation](https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md). This will be returned as `X-PB-Highlight-Language` header on fetching paste.
194198

195199
`POST` method returns a JSON string by default, if no error occurs, for example:
196200

frontend/components/CodeInput.tsx renamed to frontend/components/CodeEditor.tsx

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,19 @@ interface CodeInputProps extends React.HTMLProps<HTMLDivElement> {
2727

2828
interface TabSetting {
2929
char: "tab" | "space"
30-
width: 2 | 4
30+
width: 2 | 4 | 8
3131
}
3232

33-
function formatTabSetting(s: TabSetting) {
34-
return `${s.char} ${s.width}`
33+
function formatTabSetting(s: TabSetting, forHuman: boolean) {
34+
if (forHuman) {
35+
if (s.char === "tab") {
36+
return `Tab: ${s.width}`
37+
} else {
38+
return `Spaces: ${s.width}`
39+
}
40+
} else {
41+
return `${s.char} ${s.width}`
42+
}
3543
}
3644

3745
function parseTabSetting(s: string): TabSetting | undefined {
@@ -46,8 +54,10 @@ function parseTabSetting(s: string): TabSetting | undefined {
4654
const tabSettings: TabSetting[] = [
4755
{ char: "tab", width: 2 },
4856
{ char: "tab", width: 4 },
57+
{ char: "tab", width: 8 },
4958
{ char: "space", width: 2 },
5059
{ char: "space", width: 4 },
60+
{ char: "space", width: 8 },
5161
]
5262

5363
function handleNewLines(str: string): string {
@@ -57,7 +67,7 @@ function handleNewLines(str: string): string {
5767
return str
5868
}
5969

60-
export function CodeInput({
70+
export function CodeEditor({
6171
content,
6272
setContent,
6373
lang,
@@ -73,7 +83,7 @@ export function CodeInput({
7383
const refTextarea = useRef<HTMLTextAreaElement | null>(null)
7484

7585
const [heightPx, setHeightPx] = useState<number>(0)
76-
const prism = usePrism()
86+
const hljs = usePrism()
7787
const [tabSetting, setTabSettings] = useState<TabSetting>({ char: "space", width: 2 })
7888

7989
function syncScroll() {
@@ -122,8 +132,8 @@ export function CodeInput({
122132
}, [])
123133

124134
function highlightedHTML() {
125-
if (prism && lang && prism.listLanguages().includes(lang) && lang !== "plaintext") {
126-
const highlighted = prism.highlight(handleNewLines(content), { language: lang })
135+
if (hljs && lang && hljs.listLanguages().includes(lang) && lang !== "plaintext") {
136+
const highlighted = hljs.highlight(handleNewLines(content), { language: lang })
127137
return highlighted.value
128138
} else {
129139
return escapeHtml(content)
@@ -132,36 +142,40 @@ export function CodeInput({
132142

133143
return (
134144
<div className={className} {...rest}>
135-
<div className={"mb-2 gap-4 flex flex-row" + " "}>
136-
<Input type={"text"} label={"File name"} size={"sm"} key={filename} onValueChange={setFilename} />
145+
<div className={"mb-2 gap-2 flex flex-row" + " "}>
146+
<Input type={"text"} label={"File name"} size={"sm"} value={filename || ""} onValueChange={setFilename} />
137147
<Autocomplete
138148
className={"max-w-[10em]"}
139149
label={"Language"}
140150
size={"sm"}
141-
defaultItems={prism ? prism.listLanguages().map((lang) => ({ key: lang })) : []}
142-
selectedKey={lang}
151+
defaultItems={hljs ? hljs.listLanguages().map((lang) => ({ key: lang })) : []}
152+
// we must not use undefined here to avoid conversion from uncontrolled component to controlled component
153+
selectedKey={hljs && lang && hljs.listLanguages().includes(lang) ? lang : ""}
143154
onSelectionChange={(key) => {
144-
setLang((key as string | null) || undefined)
155+
setLang((key as string) || undefined) // when key is empty string, convert back to undefined
145156
}}
146157
>
147158
{(language) => <AutocompleteItem key={language.key}>{language.key}</AutocompleteItem>}
148159
</Autocomplete>
149160
<Select
150161
size={"sm"}
151-
label={"Tabs"}
162+
label={"Indent With"}
152163
className={"max-w-[10em]"}
153-
selectedKeys={[formatTabSetting(tabSetting)]}
164+
selectedKeys={[formatTabSetting(tabSetting, false)]}
154165
onSelectionChange={(s) => {
155166
setTabSettings(parseTabSetting(s.currentKey as string)!)
156167
}}
157168
>
158169
{tabSettings.map((s) => (
159-
<SelectItem key={formatTabSetting(s)}>{formatTabSetting(s)}</SelectItem>
170+
<SelectItem key={formatTabSetting(s, false)}>{formatTabSetting(s, true)}</SelectItem>
160171
))}
161172
</Select>
162173
</div>
163174
<div className={`w-full bg-default-100 ${tst} rounded-xl p-2`}>
164-
<div className={"relative w-full"}>
175+
<div
176+
className={"relative w-full"}
177+
style={{ tabSize: tabSetting.char === "tab" ? tabSetting.width : undefined }}
178+
>
165179
<pre
166180
className={"w-full font-mono overflow-auto text-foreground top-0 left-0 absolute"}
167181
ref={refHighlighting}

frontend/components/ErrorModal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export function useErrorModal() {
2424
}
2525

2626
function handleError(defaultTitle: string, error: Error) {
27+
console.error(error)
2728
if (error instanceof ErrorWithTitle) {
2829
showModal(error.title, error.message)
2930
} else {

frontend/components/PasteEditor.tsx renamed to frontend/components/PasteInputPanel.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React, { useRef, useState, DragEvent } from "react"
33
import { formatSize } from "../utils/utils.js"
44
import { XIcon } from "./icons.js"
55
import { cardOverrides, tst } from "../utils/overrides.js"
6-
import { CodeInput } from "./CodeInput.js"
6+
import { CodeEditor } from "./CodeEditor.js"
77

88
export type EditKind = "edit" | "file"
99

@@ -21,7 +21,7 @@ interface PasteEditorProps extends CardProps {
2121
onStateChange: (state: PasteEditState) => void
2222
}
2323

24-
export function PasteEditor({ isPasteLoading, state, onStateChange, ...rest }: PasteEditorProps) {
24+
export function PasteInputPanel({ isPasteLoading, state, onStateChange, ...rest }: PasteEditorProps) {
2525
const fileInput = useRef<HTMLInputElement>(null)
2626
const [isDragged, setDragged] = useState<boolean>(false)
2727

@@ -62,7 +62,7 @@ export function PasteEditor({ isPasteLoading, state, onStateChange, ...rest }: P
6262
>
6363
{/*Possibly a bug of chrome, but Tab sometimes has a transient unexpected scrollbar when resizing*/}
6464
<Tab key={"edit"} title="Edit" className={"overflow-hidden"}>
65-
<CodeInput
65+
<CodeEditor
6666
content={state.editContent}
6767
setContent={(k) => onStateChange({ ...state, editContent: k })}
6868
lang={state.editHighlightLang}

frontend/pages/DecryptPaste.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@ export function DecryptPaste() {
5858
return
5959
}
6060

61-
const scheme: EncryptionScheme = (resp.headers.get("X-Encryption-Scheme") as EncryptionScheme) || "AES-GCM"
61+
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+
}
6266
let key: CryptoKey | undefined
6367
try {
6468
key = await decodeKey(scheme, keyString)
@@ -82,10 +86,14 @@ export function DecryptPaste() {
8286
) || undefined
8387
: undefined
8488

85-
const inferredFilename = filename || (ext && name + ext) || filenameFromDispTrimmed || name
86-
setPasteFile(new File([decrypted], inferredFilename))
89+
// TODO: highlight with lang
90+
const lang = resp.headers.get("X-PB-Highlight-Language")
91+
92+
const inferredFilename = filename || (ext && name + ext) || filenameFromDispTrimmed
93+
setPasteFile(new File([decrypted], inferredFilename || name))
8794
setPasteContentBuffer(decrypted)
88-
const isBinary = isBinaryPath(inferredFilename)
95+
96+
const isBinary = lang === null && inferredFilename !== undefined && isBinaryPath(inferredFilename)
8997
setFileBinary(isBinary)
9098
}
9199
} finally {

frontend/pages/PasteBin.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { DarkModeToggle, useDarkModeSelection } from "../components/DarkModeTogg
99
import { useErrorModal } from "../components/ErrorModal.js"
1010
import { PanelSettingsPanel, PasteSetting } from "../components/PasteSettingPanel.js"
1111
import { UploadedPanel } from "../components/UploadedPanel.js"
12-
import { PasteEditor, PasteEditState } from "../components/PasteEditor.js"
12+
import { PasteInputPanel, PasteEditState } from "../components/PasteInputPanel.js"
1313

1414
import {
1515
verifyExpiration,
@@ -29,6 +29,7 @@ export function PasteBin() {
2929
editKind: "edit",
3030
editContent: "",
3131
file: null,
32+
editHighlightLang: "plaintext",
3233
})
3334

3435
const [pasteSetting, setPasteSetting] = useState<PasteSetting>({
@@ -55,6 +56,7 @@ export function PasteBin() {
5556
useEffect(() => {
5657
// TODO: do not fetch paste for a large file paste
5758
const pathname = location.pathname
59+
// const pathname = new URL("http://localhost:8787/ds2W:ShNkSKdf5rZypdcJEcAdFmw3").pathname
5860
const { name, password, filename, ext } = parsePath(pathname)
5961

6062
if (password !== undefined && pasteSetting.manageUrl === "") {
@@ -77,18 +79,22 @@ export function PasteBin() {
7779
}
7880
const contentType = resp.headers.get("Content-Type")
7981
const contentDisp = resp.headers.get("Content-Disposition")
82+
const contentLang = resp.headers.get("X-PB-Highlight-Language")
8083

81-
if (contentType && contentType.startsWith("text/")) {
84+
let pasteFilename = filename
85+
if (pasteFilename === undefined && contentDisp !== null) {
86+
pasteFilename = parseFilenameFromContentDisposition(contentDisp)
87+
}
88+
89+
if (contentLang || (contentType && contentType.startsWith("text/"))) {
8290
setEditorState({
8391
editKind: "edit",
8492
editContent: await resp.text(),
8593
file: null,
94+
editHighlightLang: contentLang || undefined,
95+
editFilename: pasteFilename,
8696
})
8797
} else {
88-
let pasteFilename = filename
89-
if (pasteFilename === undefined && contentDisp !== null) {
90-
pasteFilename = parseFilenameFromContentDisposition(contentDisp)
91-
}
9298
setEditorState({
9399
editKind: "file",
94100
editContent: "",
@@ -221,7 +227,7 @@ export function PasteBin() {
221227
>
222228
<div className="grow w-full max-w-[64rem]">
223229
{info}
224-
<PasteEditor
230+
<PasteInputPanel
225231
isPasteLoading={isInitPasteLoading}
226232
state={editorState}
227233
onStateChange={setEditorState}

frontend/test/decrypt.spec.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ describe("decrypt page", async () => {
1818
const encrypted = await encrypt(scheme, key, content)
1919
const server = setupServer(
2020
http.get(`${APIUrl}/abcd`, () => {
21-
return HttpResponse.arrayBuffer(encrypted.buffer)
21+
return HttpResponse.arrayBuffer(encrypted.buffer, {
22+
headers: { "X-PB-Encryption-Scheme": "AES-GCM" },
23+
})
2224
}),
2325
)
2426

frontend/utils/uploader.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { PasteSetting } from "../components/PasteSettingPanel.js"
2-
import type { PasteEditState } from "../components/PasteEditor.js"
2+
import type { PasteEditState } from "../components/PasteInputPanel.js"
33
import { APIUrl, ErrorWithTitle } from "./utils.js"
44
import type { PasteResponse } from "../../shared/interfaces.js"
55
import { encodeKey, encrypt, EncryptionScheme, genKey } from "./encryption.js"
@@ -22,7 +22,7 @@ export async function uploadPaste(
2222
onEncryptionKeyChange: (k: string | undefined) => void, // we only generate key on upload, so need a callback of key generation
2323
onProgress?: (progress: number | undefined) => void,
2424
): Promise<PasteResponse> {
25-
async function constructContent(): Promise<string | File> {
25+
async function constructContent(): Promise<File> {
2626
if (editorState.editKind === "file") {
2727
if (editorState.file === null) {
2828
throw new ErrorWithTitle("Error on Preparing Upload", "No file selected")
@@ -43,9 +43,9 @@ export async function uploadPaste(
4343
if (pasteSetting.doEncrypt) {
4444
const { key, ciphertext } = await genAndEncrypt(encryptionScheme, editorState.editContent)
4545
onEncryptionKeyChange(key)
46-
return new File([ciphertext], "")
46+
return new File([ciphertext], editorState.editFilename || "")
4747
} else {
48-
return editorState.editContent
48+
return new File([editorState.editContent], editorState.editFilename || "")
4949
}
5050
}
5151
}
@@ -57,14 +57,15 @@ export async function uploadPaste(
5757
password: pasteSetting.password.length ? pasteSetting.password : undefined,
5858
expire: pasteSetting.expiration,
5959
name: pasteSetting.uploadKind === "custom" ? pasteSetting.name : undefined,
60+
highlightLanguage: editorState.editHighlightLang,
6061
encryptionScheme: pasteSetting.doEncrypt ? encryptionScheme : undefined,
6162
manageUrl: pasteSetting.manageUrl,
6263
}
6364

64-
const contentLength = typeof options.content === "string" ? options.content.length : options.content.size
65+
const contentLength = options.content.size
6566

6667
try {
67-
if (contentLength < 5 * 1024 * 1024 || typeof options.content === "string") {
68+
if (contentLength < 5 * 1024 * 1024) {
6869
return await uploadNormal(APIUrl, options)
6970
} else {
7071
if (onProgress) onProgress(0)

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"build:frontend": "vite build frontend --outDir ../dist/frontend --emptyOutDir",
1111
"build:frontend:dev": "vite build frontend --mode development --outDir ../dist/frontend --emptyOutDir",
1212
"dev:frontend": "vite serve frontend --mode development",
13+
"preview:frontend": "vite preview --outDir dist/frontend",
1314
"dev": "wrangler dev --var DEPLOY_URL:http://localhost:8787 --port 8787",
1415
"gentypes": "wrangler types --strict-vars false",
1516
"build": "wrangler deploy --dry-run --outdir=dist",

shared/interfaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type MetaResponse = {
1717
sizeBytes: number
1818
location: PasteLocation
1919
filename?: string
20+
highlightLanguage?: string
2021
encryptionScheme?: string
2122
}
2223

0 commit comments

Comments
 (0)