Skip to content
Source
Sign inSign up

textPaste.ts

82 lines2.9 KB

1export const PASTED_TEXT_ATTACHMENT_THRESHOLD_BYTES = 32 * 1024;
2​
3const textEncoder = new TextEncoder();
4​
5export type PastedTextDisposition = "attachment" | "inline";
6​
7/** Non-Latin layouts (Cyrillic, Greek) report the typed letter; fall back to the physical key. */
8function isVKey(event: { readonly key: string; readonly code?: string }): boolean {
9 const key = event.key.toLowerCase();
10 return /^[a-z]$/.test(key) ? key === "v" : event.code === "KeyV";
12​
13export function isPasteAsTextShortcut(
14 event: Pick<KeyboardEvent, "key" | "metaKey" | "ctrlKey" | "altKey" | "shiftKey"> & {
15 readonly code?: string;
16 },
17 macPlatform: boolean,
18): boolean {
19 return (
20 isVKey(event) &&
21 event.shiftKey &&
22 !event.altKey &&
23 (macPlatform ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey)
24 );
26​
27/**
28 * The threshold is byte-based: character counts substantially understate the
29 * context cost of some Unicode-heavy clipboard contents.
30 */
31export function pastedTextDisposition(input: {
32 readonly text: string;
33 readonly canAttach: boolean;
34 readonly bypassAutoAttachment?: boolean;
35 readonly wouldExceedInputLimit?: boolean;
36}): PastedTextDisposition {
37 if (input.bypassAutoAttachment || !input.canAttach || input.text.length === 0) {
38 return "inline";
39 }
40 return input.wouldExceedInputLimit ||
41 input.text.length >= PASTED_TEXT_ATTACHMENT_THRESHOLD_BYTES ||
42 textEncoder.encode(input.text).byteLength >= PASTED_TEXT_ATTACHMENT_THRESHOLD_BYTES
43 ? "attachment"
44 : "inline";
46​
47export function nextPastedTextFileName(existingNames: ReadonlyArray<string>): string {
48 const names = new Set(existingNames.map((name) => name.toLowerCase()));
49 if (!names.has("pasted-text.txt")) {
50 return "pasted-text.txt";
51 }
52 for (let sequence = 2; ; sequence += 1) {
53 const candidate = `pasted-text-${sequence}.txt`;
54 if (!names.has(candidate)) {
55 return candidate;
56 }
57 }
59​
60export function replaceTextSelection(input: {
61 readonly value: string;
62 readonly selection: { readonly start: number; readonly end: number };
63 readonly text: string;
64}): { readonly value: string; readonly cursor: number } {
65 const start = Math.max(0, Math.min(input.value.length, input.selection.start));
66 const end = Math.max(start, Math.min(input.value.length, input.selection.end));
67 return {
68 value: `${input.value.slice(0, start)}${input.text}${input.value.slice(end)}`,
69 cursor: start + input.text.length,
70 };
72​
73export function wouldTextPasteExceedLimit(input: {
74 readonly valueLength: number;
75 readonly selection: { readonly start: number; readonly end: number };
76 readonly textLength: number;
77 readonly maxLength: number;
78}): boolean {
79 const start = Math.max(0, Math.min(input.valueLength, input.selection.start));
80 const end = Math.max(start, Math.min(input.valueLength, input.selection.end));
81 return input.valueLength - (end - start) + input.textLength > input.maxLength;