Compare commits

..
5 Commits
Author SHA1 Message Date
abdullahalam123 3cbbc42434 Release v1.1.0 2025-10-22 15:20:12 +05:30
abdullahalam123 06c36a590b chore: update config files, icons and readme
- Update FUNDING.yml formatting
- Remove debug log in sanitize-pdf.ts
- Adjust test expectation for tool count
- Standardize dependabot.yml quotes
- Update tool icons in config
- Expand and reorganize README features list
- Fix markdown formatting in README
2025-10-22 15:19:44 +05:30
abdullahalam123 de3c2fecc2 fix(sanitize-pdf): improve link removal and add named destinations cleanup
- Refactor link annotation removal logic to handle more action types (URI, Launch, GoTo, GoToR)
- Add cleanup of named destinations in catalog and names dictionary
- Improve error handling and logging throughout the sanitization process
2025-10-22 15:03:24 +05:30
abdullahalam123 b7de4bf1ed fix(sanitize-pdf): remove javascript actions, external links and font files
- Remove JavaScript actions from annotations and form fields
- Remove external links (URI, Launch, GoTo) from annotations
- Remove embedded font files while preserving font descriptors
- Add detailed error logging for each operation
2025-10-22 14:39:16 +05:30
abdullahalam123 e0e5d50240 feat(pdf-tools): add sanitize-pdf tool for comprehensive document cleaning
Implement a new PDF sanitization tool that allows users to remove various potentially sensitive elements from PDFs including metadata, annotations, JavaScript, embedded files, and more. The tool provides configurable options through checkboxes to selectively remove different types of content while preserving the core document structure.

Extract reusable utility functions from existing tools (remove-metadata, remove-annotations, flatten) to support the new sanitization feature. The tool handles edge cases gracefully and provides feedback when no changes are made.
2025-10-22 13:15:22 +05:30
20 changed files with 361 additions and 2501 deletions

No files matched your search

-22
View File
@@ -31,7 +31,6 @@
</head> </head>
<body class="antialiased"> <body class="antialiased">
<canvas id="qtcanvas" style="display: none;"></canvas>
<nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30"> <nav class="bg-gray-800 border-b border-gray-700 sticky top-0 z-30">
<div class="container mx-auto px-4"> <div class="container mx-auto px-4">
<div class="flex justify-between items-center h-16"> <div class="flex justify-between items-center h-16">
@@ -938,28 +937,7 @@
</div> </div>
</footer> </footer>
<script>
window.sofficeLoaded = false;
var Module = {
canvas: document.getElementById('qtcanvas'),
uno_scripts: ["zeta.js", "office_thread.js"],
locateFile(path, prefix) {
if (path.endsWith('.wasm') || path.endsWith('.data') || path.endsWith('.js') || path.endsWith('.metadata')) {
return `/public/static/${path}`;
}
return path;
},
onRuntimeInitialized() {
window.sofficeLoaded = true;
console.log("✅ LibreOffice WASM cargado y listo.");
},
arguments: ['--headless']
};
</script>
<script src="/public/static/soffice.js"></script>
<script type="module" src="src/js/main.ts"></script> <script type="module" src="src/js/main.ts"></script>
<script type="module" src="src/js/mobileMenu.ts"></script> <script type="module" src="src/js/mobileMenu.ts"></script>
</body> </body>
</html> </html>
</html>
+1 -40
View File
@@ -18,9 +18,7 @@
"javascript-obfuscator": "^4.1.1", "javascript-obfuscator": "^4.1.1",
"jspdf": "^3.0.3", "jspdf": "^3.0.3",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"katex": "^0.16.25", "lucide": "^0.546.0",
"lucide": "^0.545.0",
"marked": "^16.4.1",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pdfjs-dist": "^5.4.296", "pdfjs-dist": "^5.4.296",
"pdfkit": "^0.17.2", "pdfkit": "^0.17.2",
@@ -8271,31 +8269,6 @@
"setimmediate": "^1.0.5" "setimmediate": "^1.0.5"
} }
}, },
"node_modules/katex": {
"version": "0.16.25",
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.25.tgz",
"integrity": "sha512-woHRUZ/iF23GBP1dkDQMh1QBad9dmr8/PAwNA54VrSOVYgI12MAcE14TqnDdQOdzyEonGzMepYnqBMYdsoAr8Q==",
"funding": [
"https://opencollective.com/katex",
"https://github.com/sponsors/katex"
],
"license": "MIT",
"dependencies": {
"commander": "^8.3.0"
},
"bin": {
"katex": "cli.js"
}
},
"node_modules/katex/node_modules/commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -8773,18 +8746,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/marked": {
"version": "16.4.1",
"resolved": "https://registry.npmjs.org/marked/-/marked-16.4.1.tgz",
"integrity": "sha512-ntROs7RaN3EvWfy3EZi14H4YxmT6A5YvywfhO+0pm+cH/dnSQRmdAmoFIc3B9aiwTehyk7pESH4ofyBY+V5hZg==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+1 -3
View File
@@ -48,9 +48,7 @@
"javascript-obfuscator": "^4.1.1", "javascript-obfuscator": "^4.1.1",
"jspdf": "^3.0.3", "jspdf": "^3.0.3",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"katex": "^0.16.25", "lucide": "^0.546.0",
"lucide": "^0.545.0",
"marked": "^16.4.1",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"pdfjs-dist": "^5.4.296", "pdfjs-dist": "^5.4.296",
"pdfkit": "^0.17.2", "pdfkit": "^0.17.2",
+1 -1
View File
@@ -6,6 +6,7 @@ export const singlePdfLoadTools = [
'pdf-to-jpg', 'pdf-to-jpg',
'pdf-to-png', 'pdf-to-png',
'pdf-to-webp', 'pdf-to-webp',
'compress',
'pdf-to-greyscale', 'pdf-to-greyscale',
'edit-metadata', 'edit-metadata',
'remove-metadata', 'remove-metadata',
@@ -62,5 +63,4 @@ export const multiFileTools = [
'alternate-merge', 'alternate-merge',
'linearize', 'linearize',
'reverse-pages', 'reverse-pages',
'compress'
]; ];
+2 -12
View File
@@ -202,19 +202,9 @@ export const categories = [
icon: 'file-pen', icon: 'file-pen',
subtitle: 'Convert a plain text file into a PDF.', subtitle: 'Convert a plain text file into a PDF.',
}, },
{ // { id: 'md-to-pdf', name: 'Markdown to PDF', icon: 'file-text', subtitle: 'Convert a Markdown file into a PDF.' },
id: 'md-to-pdf',
name: 'Markdown to PDF',
icon: 'file-text',
subtitle: 'Convert Markdown files to PDF with live preview.',
},
// { id: 'scan-to-pdf', name: 'Scan to PDF', icon: 'camera', subtitle: 'Use your camera to create a scanned PDF.' }, // { id: 'scan-to-pdf', name: 'Scan to PDF', icon: 'camera', subtitle: 'Use your camera to create a scanned PDF.' },
{ // { id: 'word-to-pdf', name: 'Word to PDF', icon: 'file-text', subtitle: 'Convert .docx documents to PDF.' },
id: 'word-to-pdf',
name: 'Word to PDF',
icon: 'file-text',
subtitle: 'Convert .docx documents to PDF.',
},
], ],
}, },
{ {
+6 -7
View File
@@ -440,7 +440,11 @@ async function handleSinglePdfUpload(toolId, file) {
} }
async function handleMultiFileUpload(toolId) { async function handleMultiFileUpload(toolId) {
if (toolId === 'merge' || toolId === 'alternate-merge' || toolId === 'reverse-pages' || toolId === 'compress') { if (
toolId === 'merge' ||
toolId === 'alternate-merge' ||
toolId === 'reverse-pages'
) {
const pdfFilesUnloaded: File[] = []; const pdfFilesUnloaded: File[] = [];
state.files.forEach((file) => { state.files.forEach((file) => {
@@ -492,12 +496,7 @@ async function handleMultiFileUpload(toolId) {
processBtn.onclick = func; processBtn.onclick = func;
} }
} }
if (toolId === 'compress') {
const optionsDiv = document.getElementById('compress-options');
if (optionsDiv) {
optionsDiv.classList.remove('hidden');
}
}
if (toolId === 'merge') { if (toolId === 'merge') {
toolLogic.merge.setup(); toolLogic.merge.setup();
} else if (toolId === 'alternate-merge') { } else if (toolId === 'alternate-merge') {
+1 -2
View File
@@ -21,8 +21,7 @@ export function setupToolInterface(toolId: any) {
const fileInput = document.getElementById('file-input'); const fileInput = document.getElementById('file-input');
const processBtn = document.getElementById('process-btn'); const processBtn = document.getElementById('process-btn');
// Set up process button for tools that don't require file input OR for specific tools if (!fileInput && processBtn) {
if (processBtn) {
const logic = toolLogic[toolId]; const logic = toolLogic[toolId];
if (logic) { if (logic) {
const func = typeof logic.process === 'function' ? logic.process : logic; const func = typeof logic.process === 'function' ? logic.process : logic;
+52 -73
View File
@@ -6,7 +6,7 @@ import {
} from '../utils/helpers.js'; } from '../utils/helpers.js';
import { state } from '../state.js'; import { state } from '../state.js';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import JSZip from 'jszip';
import { PDFDocument, PDFName, PDFDict, PDFStream, PDFNumber } from 'pdf-lib'; import { PDFDocument, PDFName, PDFDict, PDFStream, PDFNumber } from 'pdf-lib';
function dataUrlToBytes(dataUrl: any) { function dataUrlToBytes(dataUrl: any) {
@@ -270,89 +270,68 @@ export async function compress() {
const smartSettings = { ...settings[level].smart, removeMetadata: true }; const smartSettings = { ...settings[level].smart, removeMetadata: true };
const legacySettings = settings[level].legacy; const legacySettings = settings[level].legacy;
const pdfFiles = state.files.filter((f) => f.type === 'application/pdf');
if (!pdfFiles.length) {
showAlert('Error', 'No PDF files found to compress.');
return;
}
showLoader(`Compressing ${pdfFiles.length} PDF(s)...`);
const zip = new JSZip();
let totalOriginal = 0;
let totalCompressed = 0;
try { try {
for (let i = 0; i < pdfFiles.length; i++) { const originalFile = state.files[0];
const originalFile = pdfFiles[i]; const arrayBuffer = await readFileAsArrayBuffer(originalFile);
const arrayBuffer = await readFileAsArrayBuffer(originalFile);
let resultBytes; let resultBytes;
let usedMethod; let usedMethod;
if (algorithm === 'vector') { if (algorithm === 'vector') {
showLoader('Running Vector (Smart) compression...'); showLoader('Running Vector (Smart) compression...');
resultBytes = await performSmartCompression(arrayBuffer, smartSettings); resultBytes = await performSmartCompression(arrayBuffer, smartSettings);
usedMethod = 'Vector'; usedMethod = 'Vector';
} else if (algorithm === 'photon') { } else if (algorithm === 'photon') {
showLoader('Running Photon (Rasterize) compression...'); showLoader('Running Photon (Rasterize) compression...');
resultBytes = await performLegacyCompression(arrayBuffer, legacySettings); resultBytes = await performLegacyCompression(arrayBuffer, legacySettings);
usedMethod = 'Photon'; usedMethod = 'Photon';
} else {
showLoader('Running Automatic (Vector first)...');
const vectorResultBytes = await performSmartCompression(
arrayBuffer,
smartSettings
);
if (vectorResultBytes.length < originalFile.size) {
resultBytes = vectorResultBytes;
usedMethod = 'Vector (Automatic)';
} else { } else {
showLoader('Running Automatic (Vector first)...'); showAlert('Vector failed to reduce size. Trying Photon...', 'info');
const vectorResultBytes = await performSmartCompression( showLoader('Running Automatic (Photon fallback)...');
resultBytes = await performLegacyCompression(
arrayBuffer, arrayBuffer,
smartSettings legacySettings
); );
usedMethod = 'Photon (Automatic)';
if (vectorResultBytes.length < originalFile.size) {
resultBytes = vectorResultBytes;
usedMethod = 'Vector (Automatic)';
} else {
showAlert('Vector failed to reduce size. Trying Photon...', 'info');
showLoader('Running Automatic (Photon fallback)...');
resultBytes = await performLegacyCompression(
arrayBuffer,
legacySettings
);
usedMethod = 'Photon (Automatic)';
}
} }
const originalSize = formatBytes(originalFile.size);
const compressedSize = formatBytes(resultBytes.length);
const savings = originalFile.size - resultBytes.length;
const savingsPercent =
savings > 0 ? ((savings / originalFile.size) * 100).toFixed(1) : 0;
totalOriginal += originalFile.size;
totalCompressed += resultBytes.length;
if (savings > 0) {
showAlert(
'Compression Complete',
`Method: **${usedMethod}**. ` +
`File size reduced from ${originalSize} to ${compressedSize} (Saved ${savingsPercent}%).`
);
} else {
showAlert(
'Compression Finished',
`Method: **${usedMethod}**. ` +
`Could not reduce file size. Original: ${originalSize}, New: ${compressedSize}.`,
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
'warning'
);
}
zip.file(`compressed-${originalFile.name}`, resultBytes);
} }
const zipBlob = await zip.generateAsync({ type: 'blob' });
downloadFile(zipBlob, 'compressed_pdfs.zip');
const totalSavings = totalOriginal - totalCompressed; const originalSize = formatBytes(originalFile.size);
const totalPercent = ((totalSavings / totalOriginal) * 100).toFixed(1); const compressedSize = formatBytes(resultBytes.length);
const savings = originalFile.size - resultBytes.length;
const savingsPercent =
savings > 0 ? ((savings / originalFile.size) * 100).toFixed(1) : 0;
showAlert( if (savings > 0) {
'All Files Compressed', showAlert(
`Total saved: ${totalPercent}% (${formatBytes(totalOriginal)} → ${formatBytes(totalCompressed)})` 'Compression Complete',
`Method: **${usedMethod}**. ` +
`File size reduced from ${originalSize} to ${compressedSize} (Saved ${savingsPercent}%).`
);
} else {
showAlert(
'Compression Finished',
`Method: **${usedMethod}**. ` +
`Could not reduce file size. Original: ${originalSize}, New: ${compressedSize}.`,
// @ts-expect-error TS(2554) FIXME: Expected 2 arguments, but got 3.
'warning'
);
}
downloadFile(
new Blob([resultBytes], { type: 'application/pdf' }),
'compressed-final.pdf'
); );
} catch (e) { } catch (e) {
showAlert( showAlert(
+4 -4
View File
@@ -30,7 +30,7 @@ import { txtToPdf } from './txt-to-pdf.js';
import { invertColors } from './invert-colors.js'; import { invertColors } from './invert-colors.js';
// import { viewMetadata } from './view-metadata.js'; // import { viewMetadata } from './view-metadata.js';
import { reversePages } from './reverse-pages.js'; import { reversePages } from './reverse-pages.js';
import { mdToPdf, setupMarkdownTool } from './md-to-pdf.js'; // import { mdToPdf } from './md-to-pdf.js';
import { svgToPdf } from './svg-to-pdf.js'; import { svgToPdf } from './svg-to-pdf.js';
import { bmpToPdf } from './bmp-to-pdf.js'; import { bmpToPdf } from './bmp-to-pdf.js';
import { heicToPdf } from './heic-to-pdf.js'; import { heicToPdf } from './heic-to-pdf.js';
@@ -47,7 +47,7 @@ import { changeBackgroundColor } from './change-background-color.js';
import { changeTextColor, setupTextColorTool } from './change-text-color.js'; import { changeTextColor, setupTextColorTool } from './change-text-color.js';
import { setupCompareTool } from './compare-pdfs.js'; import { setupCompareTool } from './compare-pdfs.js';
import { setupOcrTool } from './ocr-pdf.js'; import { setupOcrTool } from './ocr-pdf.js';
import { setupWordToPdfTool } from './word-to-pdf.js'; import { wordToPdf } from './word-to-pdf.js';
import { applyAndSaveSignatures, setupSignTool } from './sign-pdf.js'; import { applyAndSaveSignatures, setupSignTool } from './sign-pdf.js';
import { import {
removeAnnotations, removeAnnotations,
@@ -97,7 +97,7 @@ export const toolLogic = {
'txt-to-pdf': txtToPdf, 'txt-to-pdf': txtToPdf,
'invert-colors': invertColors, 'invert-colors': invertColors,
'reverse-pages': reversePages, 'reverse-pages': reversePages,
'md-to-pdf': { process: mdToPdf, setup: setupMarkdownTool }, // 'md-to-pdf': mdToPdf,
'svg-to-pdf': svgToPdf, 'svg-to-pdf': svgToPdf,
'bmp-to-pdf': bmpToPdf, 'bmp-to-pdf': bmpToPdf,
'heic-to-pdf': heicToPdf, 'heic-to-pdf': heicToPdf,
@@ -114,7 +114,7 @@ export const toolLogic = {
'change-text-color': { process: changeTextColor, setup: setupTextColorTool }, 'change-text-color': { process: changeTextColor, setup: setupTextColorTool },
'compare-pdfs': { setup: setupCompareTool }, 'compare-pdfs': { setup: setupCompareTool },
'ocr-pdf': { setup: setupOcrTool }, 'ocr-pdf': { setup: setupOcrTool },
'word-to-pdf': { process: setupWordToPdfTool, setup: setupWordToPdfTool }, 'word-to-pdf': wordToPdf,
'sign-pdf': { process: applyAndSaveSignatures, setup: setupSignTool }, 'sign-pdf': { process: applyAndSaveSignatures, setup: setupSignTool },
'remove-annotations': { 'remove-annotations': {
process: removeAnnotations, process: removeAnnotations,
+95 -483
View File
@@ -1,483 +1,95 @@
/** // // note: this is a work in progress
* Markdown to PDF Converter // import { showLoader, hideLoader, showAlert } from '../ui.js';
* Purpose: Convert Markdown content to PDF with support for multiple markdown flavors // import { downloadFile } from '../utils/helpers.js';
* Features: // import html2canvas from 'html2canvas';
* - GitHub Flavored Markdown support
* - CommonMark support // export async function mdToPdf() {
* - Pandoc Markdown support // // @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
* - Real-time preview // if (
* - File upload support // typeof window.jspdf === 'undefined' ||
* @version 1.0 // typeof window.html2canvas === 'undefined'
* @package BentoPDF // ) {
*/ // showAlert(
// 'Libraries Not Ready',
import { showLoader, hideLoader, showAlert } from '../ui.js'; // 'PDF generation libraries are loading. Please try again.'
import { downloadFile } from '../utils/helpers.js'; // );
import { marked } from 'marked'; // return;
import html2canvas from 'html2canvas'; // }
import { jsPDF } from 'jspdf'; // // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
import katex from 'katex'; // const markdownContent = document.getElementById('md-input').value.trim();
import 'katex/dist/katex.min.css'; // if (!markdownContent) {
// showAlert('Input Required', 'Please enter some Markdown text.');
// Configure marked for different markdown flavors // return;
const configureMarked = (flavor: string) => { // }
switch (flavor) { // showLoader('Generating High-Quality PDF...');
case 'github':
return marked.setOptions({ // try {
gfm: true, // // @ts-expect-error TS(2304) FIXME: Cannot find name 'marked'.
breaks: true, // const htmlContent = marked.parse(markdownContent);
pedantic: false // // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
}); // const pageFormat = document.getElementById('page-format').value;
case 'commonmark': // // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
return marked.setOptions({ // const orientation = document.getElementById('orientation').value;
gfm: false, // // @ts-expect-error TS(2339) FIXME: Property 'value' does not exist on type 'HTMLEleme... Remove this comment to see the full error message
breaks: false, // const marginSize = document.getElementById('margin-size').value;
pedantic: true
}); // const tempContainer = document.createElement('div');
case 'pandoc': // tempContainer.style.cssText =
return marked.setOptions({ // 'position: absolute; top: -9999px; left: -9999px; width: 800px; padding: 40px; background: white; color: black;';
gfm: true, // const styleSheet = document.createElement('style');
breaks: true, // styleSheet.textContent = `
pedantic: false // body { font-family: Helvetica, Arial, sans-serif; line-height: 1.6; font-size: 12px; }
}); // h1, h2, h3 { margin: 20px 0 10px 0; font-weight: 600; border-bottom: 1px solid #eaecef; padding-bottom: .3em; }
default: // h1 { font-size: 2em; } h2 { font-size: 1.5em; }
return marked.setOptions({ // p, blockquote, ul, ol, pre, table { margin: 0 0 16px 0; }
gfm: true, // blockquote { padding: 0 1em; color: #6a737d; border-left: .25em solid #dfe2e5; }
breaks: true, // pre { padding: 16px; overflow: auto; font-size: 85%; line-height: 1.45; background-color: #f6f8fa; border-radius: 6px; }
pedantic: false // code { font-family: 'Courier New', monospace; background-color: rgba(27,31,35,.05); border-radius: 3px; padding: .2em .4em; }
}); // table { width: 100%; border-collapse: collapse; } th, td { padding: 6px 13px; border: 1px solid #dfe2e5; }
} // img { max-width: 100%; }
}; // `;
// tempContainer.appendChild(styleSheet);
// Function to preprocess markdown content to handle math expressions // tempContainer.innerHTML += htmlContent;
function preprocessMathContent(content: string): string { // document.body.appendChild(tempContainer);
// Handle inline math: $...$
content = content.replace(/\$([^$]+)\$/g, '<span class="math-inline">$1</span>'); // const canvas = await html2canvas(tempContainer, {
// scale: 2,
// Handle block math: $$...$$ // useCORS: true,
content = content.replace(/\$\$([\s\S]*?)\$\$/g, '<div class="math-block">$1</div>'); // });
// document.body.removeChild(tempContainer);
return content;
} // // @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
// const { jsPDF } = window.jspdf;
// Function to render KaTeX mathematical expressions using local installation // const pdf = new jsPDF({ orientation, unit: 'mm', format: pageFormat });
function renderMathExpressions(container: HTMLElement) { // const pageFormats = { a4: [210, 297], letter: [216, 279] };
try { // const format = pageFormats[pageFormat];
// Render math expressions using local KaTeX // const [pageWidth, pageHeight] =
const mathElements = container.querySelectorAll('.math-inline, .math-block'); // orientation === 'landscape' ? [format[1], format[0]] : format;
mathElements.forEach((element) => { // const margins = { narrow: 10, normal: 20, wide: 30 };
try { // const margin = margins[marginSize];
const mathText = element.textContent || ''; // const contentWidth = pageWidth - margin * 2;
if (mathText.trim()) { // const contentHeight = pageHeight - margin * 2;
const rendered = katex.renderToString(mathText, { // const imgData = canvas.toDataURL('image/png');
throwOnError: false, // const imgHeight = (canvas.height * contentWidth) / canvas.width;
displayMode: element.classList.contains('math-block'),
strict: false // let heightLeft = imgHeight;
}); // let position = margin;
element.innerHTML = rendered; // pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight);
element.classList.add('katex-rendered'); // heightLeft -= contentHeight;
}
} catch (error) { // while (heightLeft > 0) {
console.warn('Failed to render math expression:', error); // position = position - pageHeight;
// Keep the original text as fallback // pdf.addPage();
} // pdf.addImage(imgData, 'PNG', margin, position, contentWidth, imgHeight);
}); // heightLeft -= contentHeight;
} catch (error) { // }
console.warn('Failed to render math expressions:', error);
} // const pdfBlob = pdf.output('blob');
} // downloadFile(pdfBlob, 'markdown-document.pdf');
// } catch (error) {
export async function mdToPdf() { // console.error('MD to PDF conversion error:', error);
const markdownContent = (document.getElementById('md-input') as HTMLTextAreaElement)?.value?.trim(); // showAlert('Conversion Error', 'Failed to generate PDF.');
// } finally {
if (!markdownContent) { // hideLoader();
showAlert('Content Required', 'Please enter your Markdown content in the text area or upload a .md file to get started.'); // }
return; // }
}
showLoader('Preparing your document for conversion...');
try {
const flavor = (document.getElementById('markdown-flavor') as HTMLSelectElement)?.value || 'github';
const pageFormat = (document.getElementById('page-format') as HTMLSelectElement)?.value || 'a4';
const orientation = (document.getElementById('orientation') as HTMLSelectElement)?.value || 'portrait';
const marginSize = (document.getElementById('margin-size') as HTMLSelectElement)?.value || 'normal';
const imageQuality = (document.getElementById('image-quality') as HTMLSelectElement)?.value || 'high';
// Configure marked based on flavor
configureMarked(flavor);
// Preprocess content to handle math expressions
const processedContent = preprocessMathContent(markdownContent);
// Parse markdown to HTML
const htmlContent = await marked.parse(processedContent);
// Create temporary container for rendering
const tempContainer = document.createElement('div');
tempContainer.style.cssText = `
position: absolute;
top: -9999px;
left: -9999px;
width: 800px;
padding: 40px;
background: white;
color: black;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
// Add comprehensive styling for markdown elements
const styleSheet = document.createElement('style');
styleSheet.textContent = `
/* KaTeX Math Rendering Styles */
.math-inline, .math-block {
font-size: 1.1em;
line-height: 1.2;
}
.math-block {
text-align: center;
margin: 16px 0;
padding: 8px 0;
}
/* Fallback for math rendering when KaTeX is not loaded */
.math-inline:not(.katex-rendered),
.math-block:not(.katex-rendered) {
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
padding: 2px 6px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
}
.math-block:not(.katex-rendered) {
display: block;
text-align: center;
margin: 16px 0;
padding: 8px;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
font-size: 14px;
color: #24292e;
}
h1, h2, h3, h4, h5, h6 {
margin: 24px 0 16px 0;
font-weight: 600;
line-height: 1.25;
border-bottom: 1px solid #eaecef;
padding-bottom: 0.3em;
}
h1 { font-size: 2em; }
h2 { font-size: 1.5em; }
h3 { font-size: 1.25em; }
h4 { font-size: 1em; }
h5 { font-size: 0.875em; }
h6 { font-size: 0.85em; color: #6a737d; }
p, blockquote, ul, ol, pre, table {
margin: 0 0 16px 0;
}
blockquote {
padding: 0 1em;
color: #6a737d;
border-left: 0.25em solid #dfe2e5;
margin: 0 0 16px 0;
}
pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f6f8fa;
border-radius: 6px;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
}
code {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
background-color: rgba(27,31,35,.05);
border-radius: 3px;
padding: 0.2em 0.4em;
font-size: 85%;
}
pre code {
background-color: transparent;
padding: 0;
border-radius: 0;
}
table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
margin: 0 0 16px 0;
display: table;
border: 1px solid #dfe2e5;
}
th, td {
padding: 8px 12px;
border: 1px solid #dfe2e5;
text-align: left;
vertical-align: top;
display: table-cell;
}
th {
font-weight: 600;
background-color: #f6f8fa;
border-bottom: 2px solid #dfe2e5;
}
tr {
display: table-row;
}
thead {
display: table-header-group;
}
tbody {
display: table-row-group;
}
img {
max-width: 100%;
height: auto;
box-sizing: border-box;
}
ul, ol {
padding-left: 2em;
margin: 0 0 16px 0;
}
li {
margin: 0.25em 0;
}
hr {
height: 2px;
padding: 0;
margin: 24px 0;
background-color: #e1e4e8;
border: 0;
border-top: 2px solid #e1e4e8;
display: block;
width: 100%;
}
a {
color: #0366d6;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
strong {
font-weight: 600;
}
em {
font-style: italic;
}
`;
tempContainer.appendChild(styleSheet);
tempContainer.innerHTML += htmlContent;
document.body.appendChild(tempContainer);
// Render KaTeX mathematical expressions using local installation
renderMathExpressions(tempContainer);
// Configure quality settings based on user selection
const renderSettings = {
high: { scale: 2, imageTimeout: 10000 },
medium: { scale: 1.5, imageTimeout: 5000 },
low: { scale: 1, imageTimeout: 3000 }
};
const settings = renderSettings[imageQuality as keyof typeof renderSettings] || renderSettings.high;
// Update progress
showLoader('Processing your Markdown content...');
// Generate canvas from HTML with optimized settings
const canvas = await html2canvas(tempContainer, {
scale: settings.scale,
useCORS: true,
allowTaint: true,
backgroundColor: '#ffffff',
logging: false, // Disable logging for better performance
width: 800,
height: tempContainer.scrollHeight,
scrollX: 0,
scrollY: 0,
foreignObjectRendering: false, // Better compatibility
removeContainer: false,
imageTimeout: settings.imageTimeout,
onclone: (clonedDoc) => {
// Optimize cloned document
const clonedContainer = clonedDoc.querySelector('div');
if (clonedContainer) {
// Remove any problematic elements
const images = clonedContainer.querySelectorAll('img');
images.forEach(img => {
if (img.src.startsWith('data:') && img.src.length > 100000) {
// Skip very large inline images
img.style.display = 'none';
}
});
}
}
});
document.body.removeChild(tempContainer);
// Update progress
showLoader('Generating your PDF document...');
// Create PDF
const pdf = new jsPDF({
orientation: orientation as 'portrait' | 'landscape',
unit: 'mm',
format: pageFormat as 'a4' | 'letter' | 'legal'
});
const pageFormats = {
a4: [210, 297],
letter: [216, 279],
legal: [216, 356]
};
const format = pageFormats[pageFormat as keyof typeof pageFormats] || pageFormats.a4;
const [pageWidth, pageHeight] = orientation === 'landscape' ? [format[1], format[0]] : format;
const margins = {
narrow: 10,
normal: 20,
wide: 30
};
const margin = margins[marginSize as keyof typeof margins] || margins.normal;
const contentWidth = pageWidth - margin * 2;
const contentHeight = pageHeight - margin * 2;
// Configure image quality based on user selection
const imageQualitySettings = {
high: 0.95, // 95% quality for high
medium: 0.85, // 85% quality for medium
low: 0.7 // 70% quality for low
};
const quality = imageQualitySettings[imageQuality as keyof typeof imageQualitySettings] || 0.95;
const imgData = canvas.toDataURL('image/jpeg', quality);
const imgHeight = (canvas.height * contentWidth) / canvas.width;
let heightLeft = imgHeight;
let position = margin;
pdf.addImage(imgData, 'JPEG', margin, position, contentWidth, imgHeight);
heightLeft -= contentHeight;
// Add additional pages if content is longer than one page
while (heightLeft > 0) {
position = position - pageHeight;
pdf.addPage();
pdf.addImage(imgData, 'JPEG', margin, position, contentWidth, imgHeight);
heightLeft -= contentHeight;
}
// Generate PDF with compression
const pdfBlob = pdf.output('blob');
// Show file size information
const fileSizeMB = (pdfBlob.size / (1024 * 1024)).toFixed(2);
downloadFile(pdfBlob, 'markdown-document.pdf');
// Show success message
hideLoader();
showAlert('PDF Conversion Complete', 'Your PDF has been generated and downloaded successfully.', () => {
// Navigate back to the markdown tool
const markdownTool = document.querySelector('[data-tool-id="md-to-pdf"]') as HTMLElement;
if (markdownTool) {
markdownTool.click();
}
});
if (parseFloat(fileSizeMB) > 15) {
console.warn(`Large PDF generated: ${fileSizeMB} MB. Consider using "Low Quality" setting for smaller files.`);
}
} catch (error) {
console.error('MD to PDF conversion error:', error);
hideLoader();
showAlert('Conversion Failed', 'We encountered an issue while converting your Markdown to PDF.\n\nPlease check your content and try again. If the problem persists, try using a different Markdown flavor or reducing the content length.');
}
}
export function setupMarkdownTool() {
const mdInput = document.getElementById('md-input') as HTMLTextAreaElement;
const preview = document.getElementById('markdown-preview') as HTMLDivElement;
const fileInput = document.getElementById('file-input') as HTMLInputElement;
if (mdInput && preview) {
// Real-time preview functionality
const updatePreview = async () => {
const content = mdInput.value;
const flavor = (document.getElementById('markdown-flavor') as HTMLSelectElement)?.value || 'github';
if (content.trim()) {
configureMarked(flavor);
const processedContent = preprocessMathContent(content);
const htmlContent = await marked.parse(processedContent);
preview.innerHTML = htmlContent;
// Render math expressions in preview
renderMathExpressions(preview);
} else {
preview.innerHTML = '<p class="text-gray-500 italic">Preview will appear here...</p>';
}
};
// Update preview on input change
mdInput.addEventListener('input', updatePreview);
// Update preview when flavor changes
const flavorSelect = document.getElementById('markdown-flavor') as HTMLSelectElement;
if (flavorSelect) {
flavorSelect.addEventListener('change', updatePreview);
}
}
// File upload functionality
if (fileInput) {
fileInput.addEventListener('change', (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file && file.type === 'text/markdown' || file?.name.endsWith('.md')) {
const reader = new FileReader();
reader.onload = (e) => {
const content = e.target?.result as string;
if (content && mdInput) {
mdInput.value = content;
// Trigger preview update
mdInput.dispatchEvent(new Event('input'));
}
};
reader.readAsText(file);
} else {
showAlert('Invalid File', 'Please select a valid Markdown (.md) file to upload.');
}
});
}
}
+123 -120
View File
@@ -1,137 +1,140 @@
import { ZetaHelperMain } from '../../lib/zetajs/zetaHelper'; // NOTE: This is a work in progress and does not work correctly as of yet
import { showLoader, hideLoader, showAlert } from '../ui.js';
import { readFileAsArrayBuffer } from '../utils/helpers.js';
import { state } from '../state.js';
async function waitForSofficeLoaded(timeout = 10000): Promise<void> { export async function wordToPdf() {
return new Promise((resolve, reject) => { const file = state.files[0];
const start = Date.now(); if (!file) {
function check() { showAlert('No File', 'Please upload a .docx file first.');
if ((window as any).sofficeLoaded) {
resolve();
} else if (Date.now() - start > timeout) {
reject('Timeout waiting for soffice.wasm to load.');
} else {
requestAnimationFrame(check);
}
}
check();
});
}
async function waitForElements(ids: string[], timeout = 5000): Promise<Record<string, HTMLElement>> {
return new Promise((resolve, reject) => {
const start = Date.now();
function check() {
const elements: Record<string, HTMLElement> = {};
let allFound = true;
for (const id of ids) {
const el = document.getElementById(id);
if (!el) allFound = false;
else elements[id] = el;
}
if (allFound) resolve(elements);
else if (Date.now() - start > timeout) reject('Timeout waiting for DOM elements: ' + ids.join(', '));
else requestAnimationFrame(check);
}
check();
});
}
export async function setupWordToPdfTool() {
let elements: Record<string, HTMLElement>;
try {
await waitForSofficeLoaded();
elements = await waitForElements([
'qtcanvas',
'file-input',
'download',
'frame',
'word-to-pdf-output'
]);
} catch (err) {
console.error(err);
return; return;
} }
const { showLoader('Preparing preview...');
qtcanvas: canvas,
'file-input': input,
download: downloadCheckbox,
frame: iframe,
'word-to-pdf-output': wordToPdfOutput
} = elements;
const zHM = new ZetaHelperMain('/static/office_thread.js', { try {
threadJsType: 'module', const mammothOptions = {
wasmPkg: 'url:/static/' // @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
}); convertImage: mammoth.images.inline((element: any) => {
return element.read('base64').then((imageBuffer: any) => {
return {
src: `data:${element.contentType};base64,${imageBuffer}`,
};
});
}),
};
const arrayBuffer = await readFileAsArrayBuffer(file);
// @ts-expect-error TS(2304) FIXME: Cannot find name 'mammoth'.
const { value: html } = await mammoth.convertToHtml(
{ arrayBuffer },
mammothOptions
);
// Assign thrPort.onmessage immediately // Get references to our modal elements from index.html
zHM.thrPort.onmessage = (e: MessageEvent) => { const previewModal = document.getElementById('preview-modal');
const data = e.data; const previewContent = document.getElementById('preview-content');
switch (data.cmd) { const downloadBtn = document.getElementById('preview-download-btn');
case 'converted': const closeBtn = document.getElementById('preview-close-btn');
try { (window as any).FS.unlink(data.from); } catch {}
const fileData = (window as any).FS.readFile(data.to);
const blob = new Blob([fileData], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
// Show PDF in iframe const styledHtml = `
(iframe as HTMLIFrameElement).src = url; <style>
wordToPdfOutput.classList.remove('hidden'); #preview-content { font-family: 'Times New Roman', Times, serif; font-size: 12pt; line-height: 1.5; color: black; }
#preview-content table { border-collapse: collapse; width: 100%; }
#preview-content td, #preview-content th { border: 1px solid #dddddd; text-align: left; padding: 8px; }
#preview-content img { max-width: 100%; height: auto; }
#preview-content a { color: #0000ee; text-decoration: underline; }
</style>
${html}
`;
previewContent.innerHTML = styledHtml;
// Download if checkbox is checked const marginDiv = document.createElement('div');
if ((downloadCheckbox as HTMLInputElement).checked) { marginDiv.style.height = '100px';
const a = document.createElement('a'); previewContent.appendChild(marginDiv);
a.href = url;
a.download = `${data.name || 'output'}.pdf`;
a.click();
}
try { (window as any).FS.unlink(data.to); } catch {} const images = previewContent.querySelectorAll('img');
(input as HTMLInputElement).disabled = false; const imagePromises = Array.from(images).map((img) => {
break; return new Promise((resolve) => {
// @ts-expect-error TS(2794) FIXME: Expected 1 arguments, but got 0. Did you forget to... Remove this comment to see the full error message
if (img.complete) resolve();
else img.onload = resolve;
});
});
await Promise.all(imagePromises);
case 'start': previewModal.classList.remove('hidden');
// Internal message, can ignore hideLoader();
break;
default: const downloadHandler = async () => {
console.warn('Unknown command from WASM:', data.cmd); showLoader('Generating High-Quality PDF...');
}
};
// Wait for ZetaHelper to initialize // @ts-expect-error TS(2339) FIXME: Property 'jspdf' does not exist on type 'Window & ... Remove this comment to see the full error message
await new Promise<void>(resolve => zHM.start(resolve)); const { jsPDF } = window.jspdf;
const doc = new jsPDF({
orientation: 'p',
unit: 'pt',
format: 'letter',
});
// Handle file selection await doc.html(previewContent, {
(input as HTMLInputElement).onchange = async () => { callback: function (doc: any) {
const inputEl = input as HTMLInputElement; const links = previewContent.querySelectorAll('a');
if (!inputEl.files || inputEl.files.length === 0) { const pageHeight = doc.internal.pageSize.getHeight();
console.error('No file selected.'); const containerRect = previewContent.getBoundingClientRect(); // Get container's position
return;
}
inputEl.disabled = true; links.forEach((link) => {
const file = inputEl.files[0]; if (!link.href) return;
let name = file.name;
let from = '/tmp/input';
const n = name.lastIndexOf('.');
if (n > 0) {
from += name.substring(n);
name = name.substring(0, n);
}
const arrayBuffer = await file.arrayBuffer(); const linkRect = link.getBoundingClientRect();
if (!(window as any).FS) {
console.error('FS not initialized yet.');
inputEl.disabled = false;
return;
}
(window as any).FS.writeFile(from, new Uint8Array(arrayBuffer)); // Calculate position relative to the preview container's top-left
zHM.thrPort.postMessage({ cmd: 'convert', name, from, to: '/tmp/output' }); const relativeX = linkRect.left - containerRect.left;
}; const relativeY = linkRect.top - containerRect.top;
const pageNum = Math.floor(relativeY / pageHeight) + 1;
const yOnPage = relativeY % pageHeight;
doc.setPage(pageNum);
try {
doc.link(
relativeX + 45,
yOnPage + 45,
linkRect.width,
linkRect.height,
{ url: link.href }
);
} catch (e) {
console.warn('Could not add link:', link.href, e);
}
});
const outputFileName = `${file.name.replace(/\.[^/.]+$/, '')}.pdf`;
doc.save(outputFileName);
hideLoader();
},
autoPaging: 'slice',
x: 45,
y: 45,
width: 522,
windowWidth: previewContent.scrollWidth,
});
};
const closeHandler = () => {
previewModal.classList.add('hidden');
previewContent.innerHTML = '';
downloadBtn.removeEventListener('click', downloadHandler);
closeBtn.removeEventListener('click', closeHandler);
};
downloadBtn.addEventListener('click', downloadHandler);
closeBtn.addEventListener('click', closeHandler);
} catch (e) {
console.error(e);
hideLoader();
showAlert(
'Preview Error',
`Could not generate a preview. The file may be corrupt or contain unsupported features. Error: ${e.message}`
);
}
} }
+1 -9
View File
@@ -226,15 +226,7 @@ const init = () => {
} }
}); });
dom.backToGridBtn.addEventListener('click', () => switchView('grid')); dom.backToGridBtn.addEventListener('click', () => switchView('grid'));
dom.alertOkBtn.addEventListener('click', () => { dom.alertOkBtn.addEventListener('click', hideAlert);
// Execute callback if it exists
const callback = (dom.alertModal as any).onConfirmCallback;
if (callback && typeof callback === 'function') {
callback();
(dom.alertModal as any).onConfirmCallback = null; // Clear callback
}
hideAlert();
});
const faqAccordion = document.getElementById('faq-accordion'); const faqAccordion = document.getElementById('faq-accordion');
if (faqAccordion) { if (faqAccordion) {
+73 -142
View File
@@ -31,15 +31,10 @@ export const showLoader = (text = 'Processing...') => {
export const hideLoader = () => dom.loaderModal.classList.add('hidden'); export const hideLoader = () => dom.loaderModal.classList.add('hidden');
export const showAlert = (title: any, message: any, onConfirm?: () => void) => { export const showAlert = (title: any, message: any) => {
dom.alertTitle.textContent = title; dom.alertTitle.textContent = title;
dom.alertMessage.textContent = message; dom.alertMessage.textContent = message;
dom.alertModal.classList.remove('hidden'); dom.alertModal.classList.remove('hidden');
// Store callback for when user clicks OK
if (onConfirm) {
(dom.alertModal as any).onConfirmCallback = onConfirm;
}
}; };
export const hideAlert = () => dom.alertModal.classList.add('hidden'); export const hideAlert = () => dom.alertModal.classList.add('hidden');
@@ -537,7 +532,7 @@ export const toolTemplates = {
compress: () => ` compress: () => `
<h2 class="text-2xl font-bold text-white mb-4">Compress PDF</h2> <h2 class="text-2xl font-bold text-white mb-4">Compress PDF</h2>
<p class="mb-6 text-gray-400">Reduce file size by choosing the compression method that best suits your document.</p> <p class="mb-6 text-gray-400">Reduce file size by choosing the compression method that best suits your document.</p>
${createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true })} ${createFileInputHTML()}
<div id="file-display-area" class="mt-4 space-y-2"></div> <div id="file-display-area" class="mt-4 space-y-2"></div>
<div id="compress-options" class="hidden mt-6 space-y-6"> <div id="compress-options" class="hidden mt-6 space-y-6">
<div> <div>
@@ -978,6 +973,39 @@ export const toolTemplates = {
<div id="file-display-area" class="mt-4 space-y-2"></div> <div id="file-display-area" class="mt-4 space-y-2"></div>
<button id="process-btn" class="hidden btn-gradient w-full mt-6">Reverse & Download</button> <button id="process-btn" class="hidden btn-gradient w-full mt-6">Reverse & Download</button>
`, `,
'md-to-pdf': () => `
<h2 class="text-2xl font-bold text-white mb-4">Markdown to PDF</h2>
<p class="mb-6 text-gray-400">Write in Markdown, select your formatting options, and get a high-quality, multi-page PDF. <br><strong class="text-gray-300">Note:</strong> Images linked from the web (e.g., https://...) require an internet connection to be rendered.</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div>
<label for="page-format" class="block mb-2 text-sm font-medium text-gray-300">Page Format</label>
<select id="page-format" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5">
<option value="a4">A4</option>
<option value="letter">Letter</option>
</select>
</div>
<div>
<label for="orientation" class="block mb-2 text-sm font-medium text-gray-300">Orientation</label>
<select id="orientation" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5">
<option value="portrait">Portrait</option>
<option value="landscape">Landscape</option>
</select>
</div>
<div>
<label for="margin-size" class="block mb-2 text-sm font-medium text-gray-300">Margin Size</label>
<select id="margin-size" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5">
<option value="normal">Normal</option>
<option value="narrow">Narrow</option>
<option value="wide">Wide</option>
</select>
</div>
</div>
<div class="h-[50vh]">
<label for="md-input" class="block mb-2 text-sm font-medium text-gray-300">Markdown Editor</label>
<textarea id="md-input" class="w-full h-full bg-gray-900 border border-gray-600 text-gray-300 rounded-lg p-3 font-mono resize-none" placeholder="# Welcome to Markdown..."></textarea>
</div>
<button id="process-btn" class="btn-gradient w-full mt-6">Create PDF from Markdown</button>
`,
'svg-to-pdf': () => ` 'svg-to-pdf': () => `
<h2 class="text-2xl font-bold text-white mb-4">SVG to PDF</h2> <h2 class="text-2xl font-bold text-white mb-4">SVG to PDF</h2>
<p class="mb-6 text-gray-400">Convert one or more SVG vector images into a single PDF file.</p> <p class="mb-6 text-gray-400">Convert one or more SVG vector images into a single PDF file.</p>
@@ -1449,27 +1477,23 @@ export const toolTemplates = {
`, `,
'word-to-pdf': () => ` 'word-to-pdf': () => `
<div id="word-to-pdf-output" > <h2 class="text-2xl font-bold text-white mb-4">Word to PDF Converter</h2>
<canvas id="qtcanvas" class="hidden"></canvas> <p class="mb-6 text-gray-400">Upload a .docx file to convert it into a high-quality PDF with selectable text. Complex layouts may not be perfectly preserved.</p>
<iframe id="frame" class="w-full h-[600px] mt-4 border border-gray-600 rounded-lg"></iframe>
<input type="checkbox" id="download" class="hidden" checked /> <!-- optional -->
<div id="file-input-wrapper"> <div id="file-input-wrapper">
<div class="relative flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-900 hover:bg-gray-700"> <div class="relative flex flex-col items-center justify-center w-full h-48 border-2 border-dashed border-gray-600 rounded-xl cursor-pointer bg-gray-900 hover:bg-gray-700">
<div class="flex flex-col items-center justify-center pt-5 pb-6"> <div class="flex flex-col items-center justify-center pt-5 pb-6">
<i data-lucide="file-text" class="w-10 h-10 mb-3 text-gray-400"></i> <i data-lucide="file-text" class="w-10 h-10 mb-3 text-gray-400"></i>
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold">Click to select a file</span> or drag and drop</p> <p class="mb-2 text-sm text-gray-400"><span class="font-semibold">Click to select a file</span> or drag and drop</p>
<p class="text-xs text-gray-500">A single .docx file</p> <p class="text-xs text-gray-500">A single .docx file</p>
</div>
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document">
</div> </div>
<input id="file-input" type="file" class="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document">
</div> </div>
</div>
<div id="file-display-area" class="mt-4 space-y-2"></div> <div id="file-display-area" class="mt-4 space-y-2"></div>
<button id="process-btn" class="btn-gradient w-full mt-6" disabled>Preview & Convert</button> <button id="process-btn" class="btn-gradient w-full mt-6" disabled>Preview & Convert</button>
</div> `,
`
,
'sign-pdf': () => ` 'sign-pdf': () => `
<h2 class="text-2xl font-bold text-white mb-4">Sign PDF</h2> <h2 class="text-2xl font-bold text-white mb-4">Sign PDF</h2>
@@ -1855,128 +1879,35 @@ export const toolTemplates = {
</div> </div>
`, `,
'md-to-pdf': () => ` linearize: () => `
<h2 class="text-2xl font-bold text-white mb-4">Markdown to PDF</h2> <h2 class="text-2xl font-bold text-white mb-4">Linearize PDFs (Fast Web View)</h2>
<p class="mb-6 text-gray-400">Convert Markdown documents to PDF with support for GitHub Flavored Markdown, CommonMark, and Pandoc Markdown. Edit on the left, preview on the right.</p> <p class="mb-6 text-gray-400">Optimize multiple PDFs for faster loading over the web. Files will be downloaded in a ZIP archive.</p>
${createFileInputHTML({ multiple: true, accept: 'application/pdf', showControls: true })}
<div id="file-display-area" class="mt-4 space-y-2"></div>
<button id="process-btn" class="hidden btn-gradient w-full mt-6" disabled>Linearize PDFs & Download ZIP</button>
`,
'add-attachments': () => `
<h2 class="text-2xl font-bold text-white mb-4">Add Attachments to PDF</h2>
<p class="mb-6 text-gray-400">First, upload the PDF document you want to add files to.</p>
${createFileInputHTML({ accept: 'application/pdf' })}
<div id="file-display-area" class="mt-4 space-y-2"></div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div id="attachment-options" class="hidden mt-8">
<!-- Left Panel: Markdown Editor --> <h3 class="text-lg font-semibold text-white mb-3">Upload Files to Attach</h3>
<div class="space-y-4"> <p class="mb-4 text-gray-400">Select one or more files to embed within the PDF. You can attach any file type (images, documents, spreadsheets, etc.).</p>
<div>
<label for="file-input" class="block mb-2 text-sm font-medium text-gray-300">Upload .md file (optional)</label> <label for="attachment-files-input" class="w-full flex justify-center items-center px-6 py-10 bg-gray-900 text-gray-400 rounded-lg border-2 border-dashed border-gray-600 hover:bg-gray-800 hover:border-gray-500 cursor-pointer transition-colors">
<input type="file" id="file-input" accept=".md,text/markdown" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5"> <div class="text-center">
<svg class="mx-auto h-12 w-12" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true"><path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>
<span class="mt-2 block text-sm font-medium">Click to upload files</span>
<span class="mt-1 block text-xs">Any file type, multiple files allowed</span>
</div> </div>
<input id="attachment-files-input" name="attachment-files" type="file" class="sr-only" multiple>
</label>
<div> <div id="attachment-file-list" class="mt-4 space-y-2"></div>
<label for="markdown-flavor" class="block mb-2 text-sm font-medium text-gray-300">Markdown Flavor</label>
<select id="markdown-flavor" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5">
<option value="github">GitHub Flavored Markdown</option>
<option value="commonmark">CommonMark</option>
<option value="pandoc">Pandoc Markdown</option>
</select>
</div>
<div> <button id="process-btn" class="hidden btn-gradient w-full mt-6" disabled>Embed Files & Download</button>
<label for="md-input" class="block mb-2 text-sm font-medium text-gray-300">Markdown Content</label>
<textarea
id="md-input"
rows="20"
class="w-full bg-gray-900 border border-gray-600 text-gray-300 rounded-lg p-3 font-mono text-sm"
placeholder="# Your Markdown Content Here
## Features Supported
- **Bold** and *italic* text
- Lists (ordered and unordered)
- [Links](https://example.com)
- \`inline code\` and code blocks
- Tables
- > Blockquotes
- Horizontal rules
- Images
- Mathematical expressions
### Mathematical Expressions
Inline math: $E = mc^2$
Block math:
$$
\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}
$$
### Code Block Example
\`\`\`javascript
function hello() {
console.log('Hello, World!');
}
\`\`\`
### Table Example
| Feature | Support |
|---------|---------|
| Headers | ✅ |
| Lists | ✅ |
| Links | ✅ |
| Code | ✅ |
| Math | ✅ |"
></textarea>
</div>
</div>
<!-- Right Panel: Preview -->
<div class="space-y-4">
<div>
<label class="block mb-2 text-sm font-medium text-gray-300">Live Preview</label>
<div
id="markdown-preview"
class="w-full h-96 bg-white border border-gray-600 rounded-lg p-4 overflow-auto text-gray-900"
style="min-height: 500px;"
>
<p class="text-gray-500 italic">Preview will appear here...</p>
</div>
</div>
</div>
</div>
<!-- PDF Options -->
<div class="mt-8 grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<label for="page-format" class="block mb-2 text-sm font-medium text-gray-300">Page Format</label>
<select id="page-format" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5">
<option value="a4">A4</option>
<option value="letter">Letter</option>
<option value="legal">Legal</option>
</select>
</div>
<div>
<label for="orientation" class="block mb-2 text-sm font-medium text-gray-300">Orientation</label>
<select id="orientation" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5">
<option value="portrait">Portrait</option>
<option value="landscape">Landscape</option>
</select>
</div>
<div>
<label for="margin-size" class="block mb-2 text-sm font-medium text-gray-300">Margins</label>
<select id="margin-size" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5">
<option value="narrow">Narrow</option>
<option value="normal" selected>Normal</option>
<option value="wide">Wide</option>
</select>
</div>
<div>
<label for="image-quality" class="block mb-2 text-sm font-medium text-gray-300">Image Quality</label>
<select id="image-quality" class="w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5">
<option value="high" selected>High Quality (Best Results)</option>
<option value="medium">Medium Quality (Balanced)</option>
<option value="low">Low Quality (Smaller Files)</option>
</select>
</div>
<div class="flex items-end">
<button id="process-btn" class="btn-gradient w-full">Convert to PDF</button>
</div>
</div> </div>
`, `,
-83
View File
@@ -1,83 +0,0 @@
import { ZetaHelperMain } from '@zetajs/zetaHelper.js';
// Elements
const canvas = document.getElementById('qtcanvas');
const input = document.getElementById('input');
const fileButton = document.getElementById('file-button');
const downloadCheckbox = document.getElementById('download');
const iframe = document.getElementById('frame');
const wasmPkg = 'url:./static/'; // adjust if needed
const zHM = new ZetaHelperMain('./src/office_thread.js', {
threadJsType: 'module',
wasmPkg
});
zHM.start(() => {
// ✅ Assign onmessage handler immediately after start
zHM.thrPort.onmessage = (e) => {
switch (e.data.cmd) {
case 'converted':
try { window.FS.unlink(e.data.from); } catch {}
const data = window.FS.readFile(e.data.to);
const blob = new Blob([data], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
iframe.src = url;
if (downloadCheckbox.checked) {
const a = document.createElement('a');
a.href = url;
a.download = `${e.data.name || 'output'}.pdf`;
document.body.appendChild(a);
a.click();
a.remove();
}
try { window.FS.unlink(e.data.to); } catch {}
URL.revokeObjectURL(url);
break;
case 'start':
input.disabled = false;
fileButton.disabled = false;
break;
default:
console.warn('Unknown message command:', e.data.cmd);
}
};
// ✅ Assign fileButton click
fileButton.onclick = () => input.click();
// ✅ Assign input onchange
input.onchange = async () => {
input.disabled = true;
fileButton.disabled = true;
const file = input.files?.[0];
if (!file) return;
if (!window.FS) {
console.error('FS not initialized yet.');
input.disabled = false;
fileButton.disabled = false;
return;
}
let name = file.name;
let from = '/tmp/input';
const n = name.lastIndexOf('.');
if (n > 0) {
from += name.substring(n);
name = name.substring(0, n);
}
const arrayBuffer = await file.arrayBuffer();
window.FS.writeFile(from, new Uint8Array(arrayBuffer));
zHM.thrPort.postMessage({ cmd: 'convert', name, from, to: '/tmp/output' });
};
});
File diff suppressed because it is too large. Load diff
-209
View File
@@ -1,209 +0,0 @@
// ATTENTION: Experimental code! Expect heavy API changes.
/**
* Helper for code in the browsers main thread.
* @beta
*/
export class ZetaHelperMain {
/**
* Use absolute URLs or URLs relative to the root HTML document (location.href).
* @param threadJs - URL for JS code file running inside the office thread (web worker).
* @param options.threadJsType - 'classic' or 'module' (ES2015)
* see: https://developer.mozilla.org/docs/Web/API/Worker/Worker#type
* @param options.wasmPkg - Which WASM binaries to use. Possible options:
* 'free', 'business', 'url:YOUR_CUSTOM_URL'
* @param options.blockPageScroll - Don't scroll the HTML page while the cursor is above
* the canvas. (default: true)
*/
constructor(threadJs, options) {
this.threadJsType = 'classic';
// Enable usage of LOWA builds with UI.
const canvas = document.getElementById('qtcanvas');
const thisFileUrl = import.meta.url;
const modUrlDir = thisFileUrl.substring(0, thisFileUrl.length - 'zetaHelper.js'.length);
if (threadJs)
threadJs = (new URL(threadJs, location.href)).toString();
const zetajsScript = modUrlDir + 'zeta.js';
const threadWrapScript = 'data:text/javascript;charset=UTF-8,' +
'import("' + import.meta.url + '").then(m => {m.zetaHelperWrapThread();});';
const wasmPkg = options.wasmPkg || 'free';
let soffice_base_url = ZetaHelperMain.wasmUrls[wasmPkg] || ZetaHelperMain.wasmUrls['free'];
if (wasmPkg?.substring(0, 4) === 'url:')
soffice_base_url = wasmPkg.substring(4, wasmPkg.length);
if (soffice_base_url === '')
soffice_base_url = './';
soffice_base_url = (new URL(soffice_base_url, location.href)).toString();
const Module = {
canvas,
uno_scripts: [zetajsScript, threadWrapScript],
locateFile: (path, prefix) => { return (prefix || soffice_base_url) + path; },
modUrlDir,
};
Module.mainScriptUrlOrBlob = new Blob(["importScripts('" + (new URL('soffice.js', soffice_base_url)) + "');"], { type: 'text/javascript' });
let lastDevicePixelRatio = window.devicePixelRatio;
addEventListener('resize', () => {
// Workaround to inform Qt5 about changed browser zoom.
setTimeout(() => {
if (lastDevicePixelRatio != -1) {
if (lastDevicePixelRatio != window.devicePixelRatio) {
lastDevicePixelRatio = -1;
this.widthPxAdd(canvas.style, +1);
window.dispatchEvent(new Event('resize'));
}
}
else {
lastDevicePixelRatio = window.devicePixelRatio;
this.widthPxAdd(canvas.style, -1);
window.dispatchEvent(new Event('resize'));
}
}, 100);
});
// Scroll only the canvas while the mouse cursor is above it.
if (options.blockPageScroll != false)
canvas.addEventListener('wheel', (event) => {
event.preventDefault();
}, { passive: false });
window.Module = Module; // window.* is global
this.canvas = canvas;
this.Module = Module;
this.threadJs = threadJs?.toString() || null;
if (options.threadJsType === 'module')
this.threadJsType = 'module';
this.soffice_base_url = soffice_base_url;
}
start(app_init) {
const zHM = this;
const soffice_js = document.createElement("script");
soffice_js.src = zHM.soffice_base_url + "soffice.js";
// "onload" runs after the loaded script has run.
soffice_js.onload = () => {
console.log('zetaHelper: Configuring Module');
zHM.Module.uno_main.then((pThrPort) => {
zHM.thrPort = pThrPort;
zHM.FS = window.FS;
zHM.thrPort.onmessage = (e) => {
switch (e.data.cmd) {
case 'ZetaHelper::thr_started':
// Trigger resize of the embedded window to match the canvas size.
// May somewhen be obsoleted by:
// https://gerrit.libreoffice.org/c/core/+/174040
window.dispatchEvent(new Event('resize'));
zHM.thrPort.postMessage({
cmd: 'ZetaHelper::run_thr_script',
threadJs: zHM.threadJs,
threadJsType: zHM.threadJsType
});
app_init();
break;
default:
throw Error('Unknown message command: ' + e.data.cmd);
}
;
};
});
};
console.log('zetaHelper: Loading WASM binaries for ZetaJS from: ' + zHM.soffice_base_url);
// Hint: The global objects "canvas" and "Module" must exist before the next line.
document.body.appendChild(soffice_js);
}
widthPxAdd(obj, value) {
if (/\A\d+px\z/.test(obj.width)) {
obj.width = parseInt(obj.width) + value + 'px';
}
}
}
ZetaHelperMain.wasmUrls = {
free: 'https://cdn.zetaoffice.net/zetaoffice_latest/',
business: 'https://business-cdn.zetaoffice.net/zetaoffice_latest/',
};
/**
* Initializes zetajs in the office thread (web worker).
* Will be called by ZetaHelperMain.
* NOT MEANT FOR DIRECT USE
* @beta
*/
export function zetaHelperWrapThread() {
const zJsModule = globalThis.Module;
zJsModule.zetajs.then((zetajs) => {
const port = zetajs.mainPort;
port.onmessage = (e) => {
switch (e.data.cmd) {
case 'ZetaHelper::run_thr_script':
port.onmessage = null;
globalThis.zetajsStore = { zetajs, zJsModule };
let threadJs = e.data.threadJs;
if (threadJs) {
if (e.data.threadJsType === 'module') {
console.log('zetaHelper: Loading threadJs as module from: ' + threadJs);
import(threadJs).then(module => {
// Make exports of threadJs accessible for debugging.
globalThis.zetajsStore.threadJsContext = module;
});
}
else { // classic
console.log('zetaHelper: Loading threadJs as script from: ' + threadJs);
importScripts(threadJs);
}
}
else {
console.log('zetaHelper: Office loaded. No threadJs given.');
}
break;
default:
throw Error('Unknown message command: ' + e.data.cmd);
}
;
};
port.postMessage({
cmd: 'ZetaHelper::thr_started'
});
});
}
/**
* Helper for inside the office thread (web worker).
* @beta
*/
export class ZetaHelperThread {
constructor() {
this.zetajs = globalThis.zetajsStore.zetajs;
this.zJsModule = globalThis.zetajsStore.zJsModule;
this.thrPort = this.zetajs.mainPort;
this.css = this.zetajs.uno.com.sun.star;
this.context = this.zetajs.getUnoComponentContext();
this.desktop = this.css.frame.Desktop.create(this.context);
this.config = this.css.configuration.ReadWriteAccess.create(this.context, 'en-US');
}
/**
* Turn off toolbars.
* @param officeModules - ["Base", "Calc", "Draw", "Impress", "Math", "Writer"];
*/
configDisableToolbars(officeModules) {
for (const mod of officeModules) {
const modName = "/org.openoffice.Office.UI." + mod + "WindowState/UIElements/States";
const uielems = this.config.getByHierarchicalName(modName);
for (const i of uielems.getElementNames()) {
if (i.startsWith("private:resource/toolbar/")) {
const uielem = uielems.getByName(i); // SLOW OPERATION
if (uielem.getByName('Visible')) {
uielem.setPropertyValue('Visible', false);
}
}
}
}
this.config.commitChanges();
}
/**
* @param unoUrl - string following ".uno:" (e.g. "Bold")
*/
transformUrl(unoUrl) {
const ioparam = { val: new this.css.util.URL({ Complete: '.uno:' + unoUrl }) };
this.css.util.URLTransformer.create(this.context).parseStrict(ioparam);
return ioparam.val;
}
queryDispatch(ctrl, urlObj) {
return ctrl.queryDispatch(urlObj, '_self', 0);
}
dispatch(ctrl, unoUrl, params = []) {
const urlObj = this.transformUrl(unoUrl);
this.queryDispatch(ctrl, urlObj).dispatch(urlObj, params);
}
}
-172
View File
@@ -1,172 +0,0 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import JSZip from 'jszip';
import * as helpers from '../js/utils/helpers';
import * as ui from '../js/ui';
import { state } from '../js/state';
import { compress } from '../js/logic/compress';
import { PDFDocument } from 'pdf-lib';
// --- Mock UI functions ---
vi.mock('../js/ui', () => ({
showLoader: vi.fn(),
hideLoader: vi.fn(),
showAlert: vi.fn(),
}));
// --- Mock helpers ---
vi.mock('../js/utils/helpers', () => ({
readFileAsArrayBuffer: vi.fn(),
downloadFile: vi.fn(),
formatBytes: (size: number) => `${size}B`,
}));
// --- Mock PDF-lib ---
vi.mock('pdf-lib', async () => {
const actual = await vi.importActual<typeof import('pdf-lib')>('pdf-lib');
return {
...actual,
PDFDocument: {
load: vi.fn().mockResolvedValue({
getPages: vi.fn().mockReturnValue([{ node: { Resources: () => null } }]),
save: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3])),
setTitle: vi.fn(),
setAuthor: vi.fn(),
setSubject: vi.fn(),
setKeywords: vi.fn(),
setCreator: vi.fn(),
setProducer: vi.fn(),
}),
create: vi.fn().mockResolvedValue({
addPage: vi.fn().mockReturnValue({ drawImage: vi.fn() }),
embedJpg: vi.fn().mockResolvedValue({}),
save: vi.fn().mockResolvedValue(new Uint8Array([1, 2, 3])),
}),
},
};
});
vi.mock('pdfjs-dist', () => {
return {
GlobalWorkerOptions: { workerSrc: '' },
getDocument: vi.fn(() => ({
promise: Promise.resolve({
numPages: 1,
getPage: vi.fn().mockResolvedValue({
getViewport: vi.fn().mockReturnValue({ width: 100, height: 100 }),
render: vi.fn().mockReturnValue({ promise: Promise.resolve() }),
}),
}),
})),
};
});
// --- Mock canvas & Image for Node environment ---
class MockCanvas {
width = 0;
height = 0;
getContext = vi.fn().mockReturnValue({
drawImage: vi.fn(),
imageSmoothingEnabled: true,
imageSmoothingQuality: 'medium',
filter: '',
});
toDataURL = vi.fn().mockReturnValue('data:image/jpeg;base64,abc');
}
vi.stubGlobal('HTMLCanvasElement', MockCanvas as any);
vi.stubGlobal('Image', class {
onload: Function = () => {};
onerror: Function = () => {};
set src(_url: string) { setTimeout(() => this.onload(), 0); }
});
vi.stubGlobal('URL', {
createObjectURL: vi.fn().mockReturnValue('blob://mock'),
revokeObjectURL: vi.fn(),
});
beforeEach(() => {
state.files = [];
vi.clearAllMocks();
// Fake DOM inputs
document.body.innerHTML = `
<input id="compression-level" value="balanced" />
<input id="compression-algorithm" value="vector" />
`;
});
afterEach(() => {
document.body.innerHTML = '';
});
describe('compress()', () => {
it('should show alert if no PDFs are loaded', async () => {
state.files = [];
await compress();
expect(ui.showAlert).toHaveBeenCalledWith('Error', 'No PDF files found to compress.');
});
it('should compress multiple PDFs successfully (vector)', async () => {
const mockFile = (name: string, size = 1000) => ({
name,
type: 'application/pdf',
size,
});
state.files = [mockFile('a.pdf'), mockFile('b.pdf')];
vi.spyOn(helpers, 'readFileAsArrayBuffer').mockResolvedValue(new ArrayBuffer(8));
vi.spyOn(helpers, 'downloadFile').mockImplementation(() => {});
await compress();
expect(ui.showLoader).toHaveBeenCalledWith(expect.stringContaining('Compressing 2 PDF'));
expect(helpers.downloadFile).toHaveBeenCalledWith(expect.any(Blob), 'compressed_pdfs.zip');
expect(ui.showAlert).toHaveBeenCalledWith(
'All Files Compressed',
expect.stringContaining('Total saved:')
);
});
it('should handle errors gracefully', async () => {
state.files = [{ name: 'a.pdf', type: 'application/pdf', size: 1000 }];
vi.spyOn(helpers, 'readFileAsArrayBuffer').mockRejectedValue(new Error('read failed'));
await compress();
expect(ui.showAlert).toHaveBeenCalledWith(
'Error',
expect.stringContaining('An error occurred during compression')
);
expect(ui.hideLoader).toHaveBeenCalled();
});
it('should fallback to Photon when vector compression does not reduce size', async () => {
state.files = [{ name: 'a.pdf', type: 'application/pdf', size: 1000 }];
vi.spyOn(helpers, 'readFileAsArrayBuffer').mockResolvedValue(new ArrayBuffer(8));
// Force vector compression to not reduce size
vi.spyOn(PDFDocument, 'load').mockResolvedValueOnce({
getPages: () => [{ node: { Resources: () => null } }],
save: async () => new Uint8Array(1000), // same size as input
setTitle: vi.fn(),
setAuthor: vi.fn(),
setSubject: vi.fn(),
setKeywords: vi.fn(),
setCreator: vi.fn(),
setProducer: vi.fn(),
} as any);
await compress();
expect(ui.showAlert).toHaveBeenCalledWith(
'Compression Finished',
expect.stringContaining('Could not reduce file size'),
'warning'
);
expect(ui.showAlert).toHaveBeenCalledWith(
'All Files Compressed',
expect.stringContaining('Total saved: 0.0%')
);
});
});
+1 -1
View File
@@ -61,7 +61,7 @@ describe('Tool Configuration Arrays', () => {
}); });
it('should have the correct number of tools', () => { it('should have the correct number of tools', () => {
expect(multiFileTools).toHaveLength(14); expect(multiFileTools).toHaveLength(13);
}); });
it('should not contain any duplicate tools', () => { it('should not contain any duplicate tools', () => {
-8
View File
@@ -1,9 +1 @@
declare const __SIMPLE_MODE__: boolean; declare const __SIMPLE_MODE__: boolean;
interface Window {
FS: {
writeFile: (path: string, data: Uint8Array) => void;
readFile: (path: string) => Uint8Array;
unlink: (path: string) => void;
};
}
-6
View File
@@ -39,12 +39,6 @@ export default defineConfig(({ mode }) => ({
}, },
}, },
}, },
server: {
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
},
test: { test: {
globals: true, globals: true,
environment: 'jsdom', environment: 'jsdom',