Reset branch to main
This commit is contained in:
@@ -2,8 +2,7 @@ import createModule from '@neslinesli93/qpdf-wasm';
|
||||
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
||||
import { createIcons } from 'lucide';
|
||||
import { state, resetState } from '../state.js';
|
||||
import * as pdfjsLib from 'pdfjs-dist'
|
||||
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
const STANDARD_SIZES = {
|
||||
A4: { width: 595.28, height: 841.89 },
|
||||
@@ -50,14 +49,14 @@ export function convertPoints(points: any, unit: any) {
|
||||
|
||||
// Convert hex color to RGB
|
||||
export function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result
|
||||
? {
|
||||
r: parseInt(result[1], 16) / 255,
|
||||
g: parseInt(result[2], 16) / 255,
|
||||
b: parseInt(result[3], 16) / 255,
|
||||
}
|
||||
: { r: 0, g: 0, b: 0 }
|
||||
r: parseInt(result[1], 16) / 255,
|
||||
g: parseInt(result[2], 16) / 255,
|
||||
b: parseInt(result[3], 16) / 255,
|
||||
}
|
||||
: { r: 0, g: 0, b: 0 };
|
||||
}
|
||||
|
||||
export const formatBytes = (bytes: any, decimals = 1) => {
|
||||
@@ -89,7 +88,10 @@ export const readFileAsArrayBuffer = (file: any) => {
|
||||
});
|
||||
};
|
||||
|
||||
export function parsePageRanges(rangeString: string, totalPages: number): number[] {
|
||||
export function parsePageRanges(
|
||||
rangeString: string,
|
||||
totalPages: number
|
||||
): number[] {
|
||||
if (!rangeString || rangeString.trim() === '') {
|
||||
return Array.from({ length: totalPages }, (_, i) => i);
|
||||
}
|
||||
@@ -128,11 +130,9 @@ export function parsePageRanges(rangeString: string, totalPages: number): number
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return Array.from(indices).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Formats an ISO 8601 date string (e.g., "2008-02-21T17:15:56-08:00")
|
||||
* into a localized, human-readable string.
|
||||
@@ -198,7 +198,7 @@ export function formatStars(num: number) {
|
||||
return (num / 1000).toFixed(1) + 'K';
|
||||
}
|
||||
return num.toLocaleString();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates a filename to a maximum length, adding ellipsis if needed.
|
||||
@@ -207,14 +207,18 @@ export function formatStars(num: number) {
|
||||
* @param maxLength - Maximum length (default: 30)
|
||||
* @returns Truncated filename with ellipsis if needed
|
||||
*/
|
||||
export function truncateFilename(filename: string, maxLength: number = 25): string {
|
||||
export function truncateFilename(
|
||||
filename: string,
|
||||
maxLength: number = 25
|
||||
): string {
|
||||
if (filename.length <= maxLength) {
|
||||
return filename;
|
||||
}
|
||||
|
||||
const lastDotIndex = filename.lastIndexOf('.');
|
||||
const extension = lastDotIndex !== -1 ? filename.substring(lastDotIndex) : '';
|
||||
const nameWithoutExt = lastDotIndex !== -1 ? filename.substring(0, lastDotIndex) : filename;
|
||||
const nameWithoutExt =
|
||||
lastDotIndex !== -1 ? filename.substring(0, lastDotIndex) : filename;
|
||||
|
||||
const availableLength = maxLength - extension.length - 3; // 3 for '...'
|
||||
|
||||
@@ -225,7 +229,10 @@ export function truncateFilename(filename: string, maxLength: number = 25): stri
|
||||
return nameWithoutExt.substring(0, availableLength) + '...' + extension;
|
||||
}
|
||||
|
||||
export function formatShortcutDisplay(shortcut: string, isMac: boolean): string {
|
||||
export function formatShortcutDisplay(
|
||||
shortcut: string,
|
||||
isMac: boolean
|
||||
): string {
|
||||
if (!shortcut) return '';
|
||||
return shortcut
|
||||
.replace('mod', isMac ? '⌘' : 'Ctrl')
|
||||
@@ -233,7 +240,7 @@ export function formatShortcutDisplay(shortcut: string, isMac: boolean): string
|
||||
.replace('alt', isMac ? '⌥' : 'Alt')
|
||||
.replace('shift', 'Shift')
|
||||
.split('+')
|
||||
.map(k => k.charAt(0).toUpperCase() + k.slice(1))
|
||||
.map((k) => k.charAt(0).toUpperCase() + k.slice(1))
|
||||
.join(isMac ? '' : '+');
|
||||
}
|
||||
|
||||
@@ -263,7 +270,7 @@ export function resetAndReloadTool(preResetCallback?: () => void) {
|
||||
export function getPDFDocument(src: any) {
|
||||
let params = src;
|
||||
|
||||
// Handle different input types similar to how getDocument handles them,
|
||||
// Handle different input types similar to how getDocument handles them,
|
||||
// but we ensure we have an object to attach wasmUrl to.
|
||||
if (typeof src === 'string') {
|
||||
params = { url: src };
|
||||
@@ -285,20 +292,171 @@ export function getPDFDocument(src: any) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a sanitized PDF filename.
|
||||
*
|
||||
* The provided filename is processed as follows:
|
||||
* - Removes a trailing `.pdf` file extension (case-insensitive)
|
||||
* - Trims leading and trailing whitespace
|
||||
* - Truncates the name to a maximum of 80 characters
|
||||
*
|
||||
* @param filename The original filename (including extension)
|
||||
* @returns The sanitized filename without the `.pdf` extension, limited to 80 characters
|
||||
* Escape HTML special characters to prevent XSS
|
||||
* @param text - The text to escape
|
||||
* @returns The escaped text
|
||||
*/
|
||||
export function getCleanPdfFilename(filename: string): string {
|
||||
let clean = filename.replace(/\.pdf$/i, '').trim();
|
||||
if (clean.length > 80) {
|
||||
clean = clean.slice(0, 80);
|
||||
}
|
||||
return clean;
|
||||
export function escapeHtml(text: string): string {
|
||||
const map: Record<string, string> = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
};
|
||||
return text.replace(/[&<>"']/g, (m) => map[m]);
|
||||
}
|
||||
|
||||
export function uint8ArrayToBase64(bytes: Uint8Array): string {
|
||||
const CHUNK_SIZE = 0x8000;
|
||||
const chunks: string[] = [];
|
||||
for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
|
||||
const chunk = bytes.subarray(i, Math.min(i + CHUNK_SIZE, bytes.length));
|
||||
chunks.push(String.fromCharCode(...chunk));
|
||||
}
|
||||
return btoa(chunks.join(''));
|
||||
}
|
||||
|
||||
export function sanitizeEmailHtml(html: string): string {
|
||||
if (!html) return html;
|
||||
|
||||
let sanitized = html;
|
||||
|
||||
sanitized = sanitized.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, '');
|
||||
sanitized = sanitized.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
|
||||
sanitized = sanitized.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
|
||||
sanitized = sanitized.replace(/<link[^>]*>/gi, '');
|
||||
sanitized = sanitized.replace(/\s+style=["'][^"']*["']/gi, '');
|
||||
sanitized = sanitized.replace(/\s+class=["'][^"']*["']/gi, '');
|
||||
sanitized = sanitized.replace(/\s+data-[a-z-]+=["'][^"']*["']/gi, '');
|
||||
sanitized = sanitized.replace(
|
||||
/<img[^>]*(?:width=["']1["'][^>]*height=["']1["']|height=["']1["'][^>]*width=["']1["'])[^>]*\/?>/gi,
|
||||
''
|
||||
);
|
||||
sanitized = sanitized.replace(
|
||||
/href=["']https?:\/\/[^"']*safelinks\.protection\.outlook\.com[^"']*url=([^&"']+)[^"']*["']/gi,
|
||||
(match, encodedUrl) => {
|
||||
try {
|
||||
const decodedUrl = decodeURIComponent(encodedUrl);
|
||||
return `href="${decodedUrl}"`;
|
||||
} catch {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
);
|
||||
sanitized = sanitized.replace(/\s+originalsrc=["'][^"']*["']/gi, '');
|
||||
sanitized = sanitized.replace(
|
||||
/href=["']([^"']{500,})["']/gi,
|
||||
(match, url) => {
|
||||
const baseUrl = url.split('?')[0];
|
||||
if (baseUrl && baseUrl.length < 200) {
|
||||
return `href="${baseUrl}"`;
|
||||
}
|
||||
return `href="${url.substring(0, 200)}"`;
|
||||
}
|
||||
);
|
||||
|
||||
sanitized = sanitized.replace(
|
||||
/\s+(cellpadding|cellspacing|bgcolor|border|valign|align|width|height|role|dir|id)=["'][^"']*["']/gi,
|
||||
''
|
||||
);
|
||||
sanitized = sanitized.replace(/<\/?table[^>]*>/gi, '<div>');
|
||||
sanitized = sanitized.replace(/<\/?tbody[^>]*>/gi, '');
|
||||
sanitized = sanitized.replace(/<\/?thead[^>]*>/gi, '');
|
||||
sanitized = sanitized.replace(/<\/?tfoot[^>]*>/gi, '');
|
||||
sanitized = sanitized.replace(/<tr[^>]*>/gi, '<div>');
|
||||
sanitized = sanitized.replace(/<\/tr>/gi, '</div>');
|
||||
sanitized = sanitized.replace(/<td[^>]*>/gi, '<span> ');
|
||||
sanitized = sanitized.replace(/<\/td>/gi, ' </span>');
|
||||
sanitized = sanitized.replace(/<th[^>]*>/gi, '<strong> ');
|
||||
sanitized = sanitized.replace(/<\/th>/gi, ' </strong>');
|
||||
sanitized = sanitized.replace(/<div>\s*<\/div>/gi, '');
|
||||
sanitized = sanitized.replace(/<span>\s*<\/span>/gi, '');
|
||||
sanitized = sanitized.replace(/(<div>)+/gi, '<div>');
|
||||
sanitized = sanitized.replace(/(<\/div>)+/gi, '</div>');
|
||||
sanitized = sanitized.replace(
|
||||
/<a[^>]*href=["']\s*["'][^>]*>([^<]*)<\/a>/gi,
|
||||
'$1'
|
||||
);
|
||||
|
||||
const MAX_HTML_SIZE = 100000;
|
||||
if (sanitized.length > MAX_HTML_SIZE) {
|
||||
const truncateAt = sanitized.lastIndexOf('</div>', MAX_HTML_SIZE);
|
||||
if (truncateAt > MAX_HTML_SIZE / 2) {
|
||||
sanitized = sanitized.substring(0, truncateAt) + '</div></body></html>';
|
||||
} else {
|
||||
sanitized = sanitized.substring(0, MAX_HTML_SIZE) + '...</body></html>';
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a raw RFC 2822 date string into a nicer human-readable format,
|
||||
* while preserving the original timezone and time.
|
||||
* Example input: "Sun, 8 Jan 2017 20:37:44 +0200"
|
||||
* Example output: "Sunday, January 8, 2017 at 8:37 PM (+0200)"
|
||||
*/
|
||||
export function formatRawDate(raw: string): string {
|
||||
try {
|
||||
const match = raw.match(
|
||||
/([A-Za-z]{3}),\s+(\d{1,2})\s+([A-Za-z]{3})\s+(\d{4})\s+(\d{2}):(\d{2})(?::(\d{2}))?\s+([+-]\d{4})/
|
||||
);
|
||||
|
||||
if (match) {
|
||||
const [
|
||||
,
|
||||
dayAbbr,
|
||||
dom,
|
||||
monthAbbr,
|
||||
year,
|
||||
hoursStr,
|
||||
minsStr,
|
||||
secsStr,
|
||||
timezone,
|
||||
] = match;
|
||||
|
||||
const days: Record<string, string> = {
|
||||
Sun: 'Sunday',
|
||||
Mon: 'Monday',
|
||||
Tue: 'Tuesday',
|
||||
Wed: 'Wednesday',
|
||||
Thu: 'Thursday',
|
||||
Fri: 'Friday',
|
||||
Sat: 'Saturday',
|
||||
};
|
||||
const months: Record<string, string> = {
|
||||
Jan: 'January',
|
||||
Feb: 'February',
|
||||
Mar: 'March',
|
||||
Apr: 'April',
|
||||
May: 'May',
|
||||
Jun: 'June',
|
||||
Jul: 'July',
|
||||
Aug: 'August',
|
||||
Sep: 'September',
|
||||
Oct: 'October',
|
||||
Nov: 'November',
|
||||
Dec: 'December',
|
||||
};
|
||||
|
||||
const fullDay = days[dayAbbr] || dayAbbr;
|
||||
const fullMonth = months[monthAbbr] || monthAbbr;
|
||||
|
||||
let hours = parseInt(hoursStr, 10);
|
||||
const ampm = hours >= 12 ? 'PM' : 'AM';
|
||||
hours = hours % 12;
|
||||
hours = hours ? hours : 12;
|
||||
const tzSign = timezone.substring(0, 1);
|
||||
const tzHours = timezone.substring(1, 3);
|
||||
const tzMins = timezone.substring(3, 5);
|
||||
const formattedTz = `UTC${tzSign}${tzHours}:${tzMins}`;
|
||||
|
||||
return `${fullDay}, ${fullMonth} ${dom}, ${year} at ${hours}:${minsStr} ${ampm} (${formattedTz})`;
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback to raw string if parsing fails
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user