2025-10-24 11:37:15 +05:30
|
|
|
import createModule from '@neslinesli93/qpdf-wasm';
|
2025-11-24 21:16:23 +05:30
|
|
|
import { showLoader, hideLoader, showAlert } from '../ui.js';
|
2025-10-27 22:01:36 +05:30
|
|
|
import { createIcons } from 'lucide';
|
2025-11-24 21:16:23 +05:30
|
|
|
import { state, resetState } from '../state.js';
|
2026-03-09 17:02:30 +01:00
|
|
|
import * as pdfjsLib from 'pdfjs-dist';
|
2025-10-24 11:37:15 +05:30
|
|
|
|
2025-10-12 11:55:45 +05:30
|
|
|
const STANDARD_SIZES = {
|
2025-10-17 11:37:32 +05:30
|
|
|
A4: { width: 595.28, height: 841.89 },
|
|
|
|
|
Letter: { width: 612, height: 792 },
|
|
|
|
|
Legal: { width: 612, height: 1008 },
|
|
|
|
|
Tabloid: { width: 792, height: 1224 },
|
|
|
|
|
A3: { width: 841.89, height: 1190.55 },
|
|
|
|
|
A5: { width: 419.53, height: 595.28 },
|
2025-10-12 11:55:45 +05:30
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function getStandardPageName(width: any, height: any) {
|
2025-10-17 11:37:32 +05:30
|
|
|
const tolerance = 1; // Allow for minor floating point variations
|
|
|
|
|
for (const [name, size] of Object.entries(STANDARD_SIZES)) {
|
|
|
|
|
if (
|
|
|
|
|
(Math.abs(width - size.width) < tolerance &&
|
|
|
|
|
Math.abs(height - size.height) < tolerance) ||
|
|
|
|
|
(Math.abs(width - size.height) < tolerance &&
|
|
|
|
|
Math.abs(height - size.width) < tolerance)
|
|
|
|
|
) {
|
|
|
|
|
return name;
|
2025-10-12 11:55:45 +05:30
|
|
|
}
|
2025-10-17 11:37:32 +05:30
|
|
|
}
|
|
|
|
|
return 'Custom';
|
2025-10-12 11:55:45 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function convertPoints(points: any, unit: any) {
|
2026-03-09 16:45:32 +01:00
|
|
|
let result: number;
|
2025-10-17 11:37:32 +05:30
|
|
|
switch (unit) {
|
|
|
|
|
case 'in':
|
|
|
|
|
result = points / 72;
|
|
|
|
|
break;
|
|
|
|
|
case 'mm':
|
|
|
|
|
result = (points / 72) * 25.4;
|
|
|
|
|
break;
|
|
|
|
|
case 'px':
|
|
|
|
|
result = points * (96 / 72); // Assuming 96 DPI
|
|
|
|
|
break;
|
|
|
|
|
default: // 'pt'
|
|
|
|
|
result = points;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
return result.toFixed(2);
|
2025-10-12 11:55:45 +05:30
|
|
|
}
|
|
|
|
|
|
2025-11-24 21:16:23 +05:30
|
|
|
// Convert hex color to RGB
|
|
|
|
|
export function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
2026-03-09 17:02:30 +01:00
|
|
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
2025-10-17 11:37:32 +05:30
|
|
|
return result
|
|
|
|
|
? {
|
2026-03-09 17:02:30 +01:00
|
|
|
r: parseInt(result[1], 16) / 255,
|
|
|
|
|
g: parseInt(result[2], 16) / 255,
|
|
|
|
|
b: parseInt(result[3], 16) / 255,
|
|
|
|
|
}
|
|
|
|
|
: { r: 0, g: 0, b: 0 };
|
2025-11-24 21:16:23 +05:30
|
|
|
}
|
2025-10-12 11:55:45 +05:30
|
|
|
|
|
|
|
|
export const formatBytes = (bytes: any, decimals = 1) => {
|
2025-10-17 11:37:32 +05:30
|
|
|
if (bytes === 0) return '0 Bytes';
|
|
|
|
|
const k = 1024;
|
|
|
|
|
const dm = decimals < 0 ? 0 : decimals;
|
|
|
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
|
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
2025-10-12 11:55:45 +05:30
|
|
|
};
|
|
|
|
|
|
2025-12-11 19:34:14 +05:30
|
|
|
export const downloadFile = (blob: Blob, filename: string): void => {
|
2025-10-17 11:37:32 +05:30
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const a = document.createElement('a');
|
|
|
|
|
a.href = url;
|
|
|
|
|
a.download = filename;
|
|
|
|
|
document.body.appendChild(a);
|
|
|
|
|
a.click();
|
|
|
|
|
document.body.removeChild(a);
|
|
|
|
|
URL.revokeObjectURL(url);
|
2025-10-12 11:55:45 +05:30
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const readFileAsArrayBuffer = (file: any) => {
|
2025-10-17 11:37:32 +05:30
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
reader.onload = () => resolve(reader.result);
|
|
|
|
|
reader.onerror = (error) => reject(error);
|
|
|
|
|
reader.readAsArrayBuffer(file);
|
|
|
|
|
});
|
2025-10-12 11:55:45 +05:30
|
|
|
};
|
|
|
|
|
|
2026-03-09 17:02:30 +01:00
|
|
|
export function parsePageRanges(
|
|
|
|
|
rangeString: string,
|
|
|
|
|
totalPages: number
|
|
|
|
|
): number[] {
|
2025-10-17 11:37:32 +05:30
|
|
|
if (!rangeString || rangeString.trim() === '') {
|
|
|
|
|
return Array.from({ length: totalPages }, (_, i) => i);
|
|
|
|
|
}
|
2025-10-12 11:55:45 +05:30
|
|
|
|
2025-12-11 19:34:14 +05:30
|
|
|
const indices = new Set<number>();
|
2025-10-17 11:37:32 +05:30
|
|
|
const parts = rangeString.split(',');
|
2025-10-12 11:55:45 +05:30
|
|
|
|
2025-10-17 11:37:32 +05:30
|
|
|
for (const part of parts) {
|
|
|
|
|
const trimmedPart = part.trim();
|
|
|
|
|
if (!trimmedPart) continue;
|
2025-10-12 11:55:45 +05:30
|
|
|
|
2025-10-17 11:37:32 +05:30
|
|
|
if (trimmedPart.includes('-')) {
|
|
|
|
|
const [start, end] = trimmedPart.split('-').map(Number);
|
|
|
|
|
if (
|
|
|
|
|
isNaN(start) ||
|
|
|
|
|
isNaN(end) ||
|
|
|
|
|
start < 1 ||
|
|
|
|
|
end > totalPages ||
|
|
|
|
|
start > end
|
|
|
|
|
) {
|
|
|
|
|
console.warn(`Invalid range skipped: ${trimmedPart}`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2025-10-12 11:55:45 +05:30
|
|
|
|
2025-10-17 11:37:32 +05:30
|
|
|
for (let i = start; i <= end; i++) {
|
|
|
|
|
indices.add(i - 1);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
const pageNum = Number(trimmedPart);
|
|
|
|
|
|
|
|
|
|
if (isNaN(pageNum) || pageNum < 1 || pageNum > totalPages) {
|
|
|
|
|
console.warn(`Invalid page number skipped: ${trimmedPart}`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
indices.add(pageNum - 1);
|
2025-10-12 11:55:45 +05:30
|
|
|
}
|
2025-10-17 11:37:32 +05:30
|
|
|
}
|
2025-10-12 11:55:45 +05:30
|
|
|
|
2025-10-17 11:37:32 +05:30
|
|
|
return Array.from(indices).sort((a, b) => a - b);
|
2025-10-12 18:23:13 +05:30
|
|
|
}
|
2025-10-21 13:38:54 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Formats an ISO 8601 date string (e.g., "2008-02-21T17:15:56-08:00")
|
|
|
|
|
* into a localized, human-readable string.
|
|
|
|
|
* @param {string} isoDateString - The ISO 8601 date string.
|
|
|
|
|
* @returns {string} A localized date and time string, or the original string if parsing fails.
|
|
|
|
|
*/
|
|
|
|
|
export function formatIsoDate(isoDateString) {
|
|
|
|
|
if (!isoDateString || typeof isoDateString !== 'string') {
|
|
|
|
|
return isoDateString; // Return original value if it's not a valid string
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const date = new Date(isoDateString);
|
|
|
|
|
// Check if the date object is valid
|
|
|
|
|
if (isNaN(date.getTime())) {
|
|
|
|
|
return isoDateString; // Return original string if the date is invalid
|
|
|
|
|
}
|
|
|
|
|
return date.toLocaleString();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Could not parse ISO date:', e);
|
|
|
|
|
return isoDateString; // Return original string on any error
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-24 11:37:15 +05:30
|
|
|
|
|
|
|
|
let qpdfInstance: any = null;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initialize qpdf-wasm singleton.
|
|
|
|
|
* Subsequent calls return the same instance.
|
|
|
|
|
*/
|
|
|
|
|
export async function initializeQpdf() {
|
|
|
|
|
if (qpdfInstance) return qpdfInstance;
|
|
|
|
|
|
|
|
|
|
showLoader('Initializing PDF engine...');
|
|
|
|
|
try {
|
|
|
|
|
qpdfInstance = await createModule({
|
2025-12-13 20:57:32 +05:30
|
|
|
locateFile: () => import.meta.env.BASE_URL + 'qpdf.wasm',
|
2025-10-24 11:37:15 +05:30
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to initialize qpdf-wasm:', error);
|
|
|
|
|
showAlert(
|
|
|
|
|
'Initialization Error',
|
|
|
|
|
'Could not load the PDF engine. Please refresh the page and try again.'
|
|
|
|
|
);
|
|
|
|
|
throw error;
|
|
|
|
|
} finally {
|
|
|
|
|
hideLoader();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return qpdfInstance;
|
2025-10-24 13:42:09 +05:30
|
|
|
}
|
2025-10-27 22:01:36 +05:30
|
|
|
|
|
|
|
|
export function initializeIcons(): void {
|
2025-10-27 22:35:49 +05:30
|
|
|
createIcons({
|
|
|
|
|
attrs: {
|
|
|
|
|
class: 'bento-icon',
|
|
|
|
|
'stroke-width': '1.5',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-11-10 21:54:41 +05:30
|
|
|
|
|
|
|
|
export function formatStars(num: number) {
|
|
|
|
|
if (num >= 1000) {
|
|
|
|
|
return (num / 1000).toFixed(1) + 'K';
|
|
|
|
|
}
|
|
|
|
|
return num.toLocaleString();
|
2026-03-09 17:02:30 +01:00
|
|
|
}
|
2025-11-21 17:10:56 +05:30
|
|
|
|
2025-11-24 21:16:23 +05:30
|
|
|
/**
|
|
|
|
|
* Truncates a filename to a maximum length, adding ellipsis if needed.
|
|
|
|
|
* Preserves the file extension.
|
|
|
|
|
* @param filename - The filename to truncate
|
|
|
|
|
* @param maxLength - Maximum length (default: 30)
|
|
|
|
|
* @returns Truncated filename with ellipsis if needed
|
|
|
|
|
*/
|
2026-03-09 17:02:30 +01:00
|
|
|
export function truncateFilename(
|
|
|
|
|
filename: string,
|
|
|
|
|
maxLength: number = 25
|
|
|
|
|
): string {
|
2025-11-24 21:16:23 +05:30
|
|
|
if (filename.length <= maxLength) {
|
|
|
|
|
return filename;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const lastDotIndex = filename.lastIndexOf('.');
|
|
|
|
|
const extension = lastDotIndex !== -1 ? filename.substring(lastDotIndex) : '';
|
2026-03-09 17:02:30 +01:00
|
|
|
const nameWithoutExt =
|
|
|
|
|
lastDotIndex !== -1 ? filename.substring(0, lastDotIndex) : filename;
|
2025-11-24 21:16:23 +05:30
|
|
|
|
|
|
|
|
const availableLength = maxLength - extension.length - 3; // 3 for '...'
|
|
|
|
|
|
|
|
|
|
if (availableLength <= 0) {
|
|
|
|
|
return filename.substring(0, maxLength - 3) + '...';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nameWithoutExt.substring(0, availableLength) + '...' + extension;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 17:02:30 +01:00
|
|
|
export function formatShortcutDisplay(
|
|
|
|
|
shortcut: string,
|
|
|
|
|
isMac: boolean
|
|
|
|
|
): string {
|
2025-11-24 21:16:23 +05:30
|
|
|
if (!shortcut) return '';
|
|
|
|
|
return shortcut
|
|
|
|
|
.replace('mod', isMac ? '⌘' : 'Ctrl')
|
|
|
|
|
.replace('ctrl', isMac ? '^' : 'Ctrl') // Control key on Mac shows as ^
|
|
|
|
|
.replace('alt', isMac ? '⌥' : 'Alt')
|
|
|
|
|
.replace('shift', 'Shift')
|
|
|
|
|
.split('+')
|
2026-03-09 17:02:30 +01:00
|
|
|
.map((k) => k.charAt(0).toUpperCase() + k.slice(1))
|
2025-11-24 21:16:23 +05:30
|
|
|
.join(isMac ? '' : '+');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function resetAndReloadTool(preResetCallback?: () => void) {
|
|
|
|
|
const toolid = state.activeTool;
|
|
|
|
|
|
|
|
|
|
if (preResetCallback) {
|
|
|
|
|
preResetCallback();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resetState();
|
|
|
|
|
|
|
|
|
|
if (toolid) {
|
|
|
|
|
const element = document.querySelector(
|
|
|
|
|
`[data-tool-id="${toolid}"]`
|
|
|
|
|
) as HTMLElement;
|
|
|
|
|
if (element) element.click();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Wrapper for pdfjsLib.getDocument that adds the required wasmUrl configuration.
|
|
|
|
|
* Use this instead of calling pdfjsLib.getDocument directly.
|
|
|
|
|
* @param src The source to load (url string, typed array, or parameters object)
|
|
|
|
|
* @returns The PDF loading task
|
|
|
|
|
*/
|
|
|
|
|
export function getPDFDocument(src: any) {
|
|
|
|
|
let params = src;
|
|
|
|
|
|
2026-03-09 17:02:30 +01:00
|
|
|
// Handle different input types similar to how getDocument handles them,
|
2025-11-24 21:16:23 +05:30
|
|
|
// but we ensure we have an object to attach wasmUrl to.
|
|
|
|
|
if (typeof src === 'string') {
|
|
|
|
|
params = { url: src };
|
|
|
|
|
} else if (src instanceof Uint8Array || src instanceof ArrayBuffer) {
|
|
|
|
|
params = { data: src };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure params is an object
|
|
|
|
|
if (typeof params !== 'object' || params === null) {
|
|
|
|
|
params = {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add wasmUrl pointing to our public/wasm directory
|
|
|
|
|
// This is required for PDF.js v5+ to load OpenJPEG for certain images
|
|
|
|
|
return pdfjsLib.getDocument({
|
|
|
|
|
...params,
|
2025-12-13 20:57:32 +05:30
|
|
|
wasmUrl: import.meta.env.BASE_URL + 'pdfjs-viewer/wasm/',
|
2025-11-24 21:16:23 +05:30
|
|
|
});
|
|
|
|
|
}
|
2025-12-19 02:08:06 +01:00
|
|
|
|
|
|
|
|
/**
|
2026-03-09 17:02:30 +01:00
|
|
|
* Escape HTML special characters to prevent XSS
|
|
|
|
|
* @param text - The text to escape
|
|
|
|
|
* @returns The escaped text
|
2025-12-19 02:08:06 +01:00
|
|
|
*/
|
2026-03-09 17:02:30 +01:00
|
|
|
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
|
2025-12-19 02:08:06 +01:00
|
|
|
}
|
2026-03-09 17:02:30 +01:00
|
|
|
return raw;
|
2025-12-19 02:08:06 +01:00
|
|
|
}
|