feat(Security): fixes
Some checks failed
Build and Push Docker Images (Default + Simple Mode) / build-and-release (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / build-amd64 (map[name:default simple_mode:false suffix:]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / build-amd64 (map[name:simple simple_mode:true suffix:-simple]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / build-arm64 (map[name:default simple_mode:false suffix:]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / build-arm64 (map[name:simple simple_mode:true suffix:-simple]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / merge-manifests-ghcr (map[name:default suffix:]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / merge-manifests-ghcr (map[name:simple suffix:-simple]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / push-to-dockerhub (map[name:default suffix:]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / push-to-dockerhub (map[name:simple suffix:-simple]) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy static content to Pages / deploy (push) Has been cancelled
Trivy Security Scan / scan-image (map[file:Dockerfile name:bentopdf]) (push) Has been cancelled
Trivy Security Scan / scan-image (map[file:Dockerfile.nonroot name:bentopdf-nonroot]) (push) Has been cancelled
Trivy Security Scan / scan-dependencies (push) Has been cancelled
Trivy Security Scan / scan-config (push) Has been cancelled
Some checks failed
Build and Push Docker Images (Default + Simple Mode) / build-and-release (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / build-amd64 (map[name:default simple_mode:false suffix:]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / build-amd64 (map[name:simple simple_mode:true suffix:-simple]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / build-arm64 (map[name:default simple_mode:false suffix:]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / build-arm64 (map[name:simple simple_mode:true suffix:-simple]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / merge-manifests-ghcr (map[name:default suffix:]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / merge-manifests-ghcr (map[name:simple suffix:-simple]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / push-to-dockerhub (map[name:default suffix:]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / push-to-dockerhub (map[name:simple suffix:-simple]) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy static content to Pages / deploy (push) Has been cancelled
Trivy Security Scan / scan-image (map[file:Dockerfile name:bentopdf]) (push) Has been cancelled
Trivy Security Scan / scan-image (map[file:Dockerfile.nonroot name:bentopdf-nonroot]) (push) Has been cancelled
Trivy Security Scan / scan-dependencies (push) Has been cancelled
Trivy Security Scan / scan-config (push) Has been cancelled
This commit is contained in:
76
package-lock.json
generated
76
package-lock.json
generated
@@ -33,6 +33,7 @@
|
||||
"diff": "^8.0.3",
|
||||
"dompurify": "^3.4.0",
|
||||
"embedpdf-snippet": "file:vendor/embedpdf/embedpdf-snippet-2.9.1.tgz",
|
||||
"fast-xml-parser": "^5.7.1",
|
||||
"heic2any": "^0.0.4",
|
||||
"highlight.js": "^11.11.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
@@ -2074,6 +2075,18 @@
|
||||
"integrity": "sha512-ObyTmabopTMwN6AGwrEkmDbdmM5wnwlRdn9jnJYQ2KbBUPdPDcfNf1QkcWFOVD+qIgSqILz9nK51e22T+x+gkQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@nodable/entities": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz",
|
||||
"integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nodable"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.122.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
|
||||
@@ -6993,6 +7006,42 @@
|
||||
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/fast-xml-builder": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz",
|
||||
"integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-expression-matcher": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.1.tgz",
|
||||
"integrity": "sha512-8Cc3f8GUGUULg34pBch/KGyPLglS+OFs05deyOlY7fL2MTagYPKrVQNmR1fLF/yJ9PH5ZSTd3YDF6pnmeZU+zA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodable/entities": "^2.1.0",
|
||||
"fast-xml-builder": "^1.1.5",
|
||||
"path-expression-matcher": "^1.5.0",
|
||||
"strnum": "^2.2.3"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
@@ -9861,6 +9910,21 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-expression-matcher": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz",
|
||||
"integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
@@ -11347,6 +11411,18 @@
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/strnum": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz",
|
||||
"integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stylis": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
"diff": "^8.0.3",
|
||||
"dompurify": "^3.4.0",
|
||||
"embedpdf-snippet": "file:vendor/embedpdf/embedpdf-snippet-2.9.1.tgz",
|
||||
"fast-xml-parser": "^5.7.1",
|
||||
"heic2any": "^0.0.4",
|
||||
"highlight.js": "^11.11.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
|
||||
16
public/sw.js
16
public/sw.js
@@ -330,6 +330,22 @@ async function cacheInBatches(cache, urls, batchSize = 5) {
|
||||
self.addEventListener('message', (event) => {
|
||||
if (!event.data) return;
|
||||
|
||||
if (event.origin && event.origin !== self.location.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const source = event.source;
|
||||
if (source && typeof source === 'object' && 'url' in source) {
|
||||
try {
|
||||
const sourceOrigin = new URL(source.url).origin;
|
||||
if (sourceOrigin !== self.location.origin) {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
return;
|
||||
|
||||
@@ -43,7 +43,26 @@ interface ErrorResult {
|
||||
message: string;
|
||||
}
|
||||
|
||||
self.onmessage = function (e: MessageEvent<WorkerMessage>) {
|
||||
function isValidMessage(data: unknown): data is WorkerMessage {
|
||||
if (!data || typeof data !== 'object') return false;
|
||||
const m = data as Record<string, unknown>;
|
||||
if (typeof m.id !== 'number') return false;
|
||||
if (m.type === 'diff') {
|
||||
return Array.isArray(m.beforeItems) && Array.isArray(m.afterItems);
|
||||
}
|
||||
if (m.type === 'pair') {
|
||||
return Array.isArray(m.leftPages) && Array.isArray(m.rightPages);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
self.onmessage = function (e: MessageEvent<unknown>) {
|
||||
if (e.origin && e.origin !== '' && e.origin !== self.location.origin) {
|
||||
return;
|
||||
}
|
||||
if (!isValidMessage(e.data)) {
|
||||
return;
|
||||
}
|
||||
const msg = e.data;
|
||||
try {
|
||||
if (msg.type === 'diff') {
|
||||
|
||||
@@ -720,7 +720,14 @@ async function handleMultiFileUpload(toolId: string) {
|
||||
'w-full h-36 sm:h-40 md:h-44 bg-gray-900 rounded-md border-2 border-gray-600 flex items-center justify-center overflow-hidden';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = url;
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (parsed.protocol === 'blob:') {
|
||||
img.src = parsed.href;
|
||||
}
|
||||
} catch {
|
||||
console.warn('Invalid blob URL for preview');
|
||||
}
|
||||
img.className = 'max-w-full max-h-full object-contain';
|
||||
|
||||
const p = document.createElement('p');
|
||||
|
||||
@@ -203,8 +203,15 @@ function initializePage(): void {
|
||||
|
||||
if (sigImageThumb && sigImagePreview) {
|
||||
const url = URL.createObjectURL(file);
|
||||
sigImageThumb.src = url;
|
||||
sigImagePreview.classList.remove('hidden');
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (parsed.protocol === 'blob:') {
|
||||
sigImageThumb.src = parsed.href;
|
||||
sigImagePreview.classList.remove('hidden');
|
||||
}
|
||||
} catch {
|
||||
console.warn('Invalid blob URL for signature preview');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ type LucideWindow = Window & {
|
||||
};
|
||||
};
|
||||
|
||||
import DOMPurify from 'dompurify';
|
||||
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
||||
import { downloadFile, escapeHtml, hexToRgb } from '../utils/helpers.js';
|
||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
|
||||
@@ -1327,7 +1328,7 @@ function showProperties(field: FormField): void {
|
||||
`;
|
||||
}
|
||||
|
||||
propertiesPanel.innerHTML = `
|
||||
const propertiesHtml = `
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Field Name ${field.type === 'radio' ? '(Group Name)' : ''}</label>
|
||||
@@ -1399,6 +1400,10 @@ function showProperties(field: FormField): void {
|
||||
</div>
|
||||
`;
|
||||
|
||||
propertiesPanel.innerHTML = DOMPurify.sanitize(propertiesHtml, {
|
||||
ADD_ATTR: ['target'],
|
||||
});
|
||||
|
||||
// Common listeners
|
||||
const propName = document.getElementById('propName') as HTMLInputElement;
|
||||
const nameError = document.getElementById('nameError') as HTMLDivElement;
|
||||
|
||||
@@ -1,196 +1,280 @@
|
||||
import { jsPDF } from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
import { XMLParser } from 'fast-xml-parser';
|
||||
|
||||
export interface XmlToPdfOptions {
|
||||
onProgress?: (percent: number, message: string) => void;
|
||||
onProgress?: (percent: number, message: string) => void;
|
||||
}
|
||||
|
||||
interface jsPDFWithAutoTable extends jsPDF {
|
||||
lastAutoTable?: { finalY: number };
|
||||
lastAutoTable?: { finalY: number };
|
||||
}
|
||||
|
||||
const ATTR_PREFIX = '@_';
|
||||
const TEXT_KEY = '#text';
|
||||
|
||||
export async function convertXmlToPdf(
|
||||
file: File,
|
||||
options?: XmlToPdfOptions
|
||||
file: File,
|
||||
options?: XmlToPdfOptions
|
||||
): Promise<Blob> {
|
||||
const { onProgress } = options || {};
|
||||
const { onProgress } = options || {};
|
||||
|
||||
onProgress?.(10, 'Reading XML file...');
|
||||
const xmlText = await file.text();
|
||||
onProgress?.(10, 'Reading XML file...');
|
||||
const rawXmlText = await file.text();
|
||||
const xmlText = String(rawXmlText)
|
||||
.replace(/<!DOCTYPE[\s\S]*?>/gi, '')
|
||||
.replace(/<!ENTITY[\s\S]*?>/gi, '')
|
||||
.replace(/<\?xml-stylesheet[\s\S]*?\?>/gi, '');
|
||||
|
||||
onProgress?.(30, 'Parsing XML structure...');
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
|
||||
onProgress?.(30, 'Parsing XML structure...');
|
||||
const parser = new XMLParser({
|
||||
ignoreAttributes: false,
|
||||
attributeNamePrefix: ATTR_PREFIX,
|
||||
textNodeName: TEXT_KEY,
|
||||
allowBooleanAttributes: true,
|
||||
parseTagValue: false,
|
||||
parseAttributeValue: false,
|
||||
trimValues: true,
|
||||
processEntities: true,
|
||||
ignoreDeclaration: true,
|
||||
ignorePiTags: true,
|
||||
});
|
||||
|
||||
const parseError = xmlDoc.querySelector('parsererror');
|
||||
if (parseError) {
|
||||
throw new Error('Invalid XML: ' + parseError.textContent);
|
||||
}
|
||||
let parsed: Record<string, unknown>;
|
||||
try {
|
||||
parsed = parser.parse(xmlText) as Record<string, unknown>;
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
throw new Error('Invalid XML: ' + toSafeText(msg), { cause: err });
|
||||
}
|
||||
|
||||
onProgress?.(50, 'Analyzing data structure...');
|
||||
const rootKeys = Object.keys(parsed);
|
||||
if (rootKeys.length === 0) {
|
||||
throw new Error('Invalid XML: no root element');
|
||||
}
|
||||
const rootName = rootKeys[0];
|
||||
const rootValue = parsed[rootName];
|
||||
|
||||
const doc: jsPDFWithAutoTable = new jsPDF({
|
||||
orientation: 'landscape',
|
||||
unit: 'mm',
|
||||
format: 'a4'
|
||||
});
|
||||
onProgress?.(50, 'Analyzing data structure...');
|
||||
|
||||
const pageWidth = doc.internal.pageSize.getWidth();
|
||||
let yPosition = 20;
|
||||
const doc: jsPDFWithAutoTable = new jsPDF({
|
||||
orientation: 'landscape',
|
||||
unit: 'mm',
|
||||
format: 'a4',
|
||||
});
|
||||
|
||||
const root = xmlDoc.documentElement;
|
||||
const rootName = formatTitle(root.tagName);
|
||||
const pageWidth = doc.internal.pageSize.getWidth();
|
||||
let yPosition = 20;
|
||||
|
||||
doc.setFontSize(18);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(rootName, pageWidth / 2, yPosition, { align: 'center' });
|
||||
yPosition += 15;
|
||||
doc.setFontSize(18);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(formatTitle(rootName), pageWidth / 2, yPosition, {
|
||||
align: 'center',
|
||||
});
|
||||
yPosition += 15;
|
||||
|
||||
onProgress?.(60, 'Generating formatted content...');
|
||||
onProgress?.(60, 'Generating formatted content...');
|
||||
|
||||
const children = Array.from(root.children);
|
||||
if (isPlainObject(rootValue)) {
|
||||
const rootObj = rootValue;
|
||||
const childEntries = Object.entries(rootObj).filter(
|
||||
([k]) => !k.startsWith(ATTR_PREFIX) && k !== TEXT_KEY
|
||||
);
|
||||
|
||||
if (children.length > 0) {
|
||||
const groups = groupByTagName(children);
|
||||
if (childEntries.length > 0) {
|
||||
const groups = groupChildrenByTagName(childEntries);
|
||||
|
||||
for (const [groupName, elements] of Object.entries(groups)) {
|
||||
const { headers, rows } = extractTableData(elements);
|
||||
|
||||
if (headers.length > 0 && rows.length > 0) {
|
||||
if (Object.keys(groups).length > 1) {
|
||||
doc.setFontSize(14);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(formatTitle(groupName), 14, yPosition);
|
||||
yPosition += 8;
|
||||
}
|
||||
|
||||
autoTable(doc, {
|
||||
head: [headers.map(h => formatTitle(h))],
|
||||
body: rows,
|
||||
startY: yPosition,
|
||||
styles: {
|
||||
fontSize: 9,
|
||||
cellPadding: 4,
|
||||
overflow: 'linebreak',
|
||||
},
|
||||
headStyles: {
|
||||
fillColor: [79, 70, 229],
|
||||
textColor: 255,
|
||||
fontStyle: 'bold',
|
||||
},
|
||||
alternateRowStyles: {
|
||||
fillColor: [243, 244, 246],
|
||||
},
|
||||
margin: { top: 20, left: 14, right: 14 },
|
||||
theme: 'striped',
|
||||
didDrawPage: (data) => {
|
||||
yPosition = (data.cursor?.y || yPosition) + 10;
|
||||
}
|
||||
});
|
||||
|
||||
yPosition = (doc.lastAutoTable?.finalY || yPosition) + 15;
|
||||
}
|
||||
const renderableGroups: Array<[string, Record<string, unknown>[]]> = [];
|
||||
for (const [groupName, elements] of Object.entries(groups)) {
|
||||
const { headers, rows } = extractTableData(elements);
|
||||
if (headers.length > 0 && rows.length > 0) {
|
||||
renderableGroups.push([groupName, elements]);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [groupName, elements] of renderableGroups) {
|
||||
const { headers, rows } = extractTableData(elements);
|
||||
|
||||
if (renderableGroups.length > 1) {
|
||||
doc.setFontSize(14);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(formatTitle(groupName), 14, yPosition);
|
||||
yPosition += 8;
|
||||
}
|
||||
|
||||
autoTable(doc, {
|
||||
head: [headers.map((h) => formatTitle(h))],
|
||||
body: rows,
|
||||
startY: yPosition,
|
||||
styles: {
|
||||
fontSize: 9,
|
||||
cellPadding: 4,
|
||||
overflow: 'linebreak',
|
||||
},
|
||||
headStyles: {
|
||||
fillColor: [79, 70, 229],
|
||||
textColor: 255,
|
||||
fontStyle: 'bold',
|
||||
},
|
||||
alternateRowStyles: {
|
||||
fillColor: [243, 244, 246],
|
||||
},
|
||||
margin: { top: 20, left: 14, right: 14 },
|
||||
theme: 'striped',
|
||||
didDrawPage: (data) => {
|
||||
yPosition = (data.cursor?.y || yPosition) + 10;
|
||||
},
|
||||
});
|
||||
|
||||
yPosition = (doc.lastAutoTable?.finalY || yPosition) + 15;
|
||||
}
|
||||
} else {
|
||||
const kvPairs = extractKeyValuePairs(root);
|
||||
if (kvPairs.length > 0) {
|
||||
autoTable(doc, {
|
||||
head: [['Property', 'Value']],
|
||||
body: kvPairs,
|
||||
startY: yPosition,
|
||||
styles: {
|
||||
fontSize: 10,
|
||||
cellPadding: 5,
|
||||
},
|
||||
headStyles: {
|
||||
fillColor: [79, 70, 229],
|
||||
textColor: 255,
|
||||
fontStyle: 'bold',
|
||||
},
|
||||
columnStyles: {
|
||||
0: { fontStyle: 'bold', cellWidth: 60 },
|
||||
1: { cellWidth: 'auto' },
|
||||
},
|
||||
margin: { left: 14, right: 14 },
|
||||
theme: 'striped',
|
||||
});
|
||||
}
|
||||
const kvPairs = extractKeyValuePairs(rootObj);
|
||||
if (kvPairs.length > 0) {
|
||||
autoTable(doc, {
|
||||
head: [['Property', 'Value']],
|
||||
body: kvPairs,
|
||||
startY: yPosition,
|
||||
styles: {
|
||||
fontSize: 10,
|
||||
cellPadding: 5,
|
||||
},
|
||||
headStyles: {
|
||||
fillColor: [79, 70, 229],
|
||||
textColor: 255,
|
||||
fontStyle: 'bold',
|
||||
},
|
||||
columnStyles: {
|
||||
0: { fontStyle: 'bold', cellWidth: 60 },
|
||||
1: { cellWidth: 'auto' },
|
||||
},
|
||||
margin: { left: 14, right: 14 },
|
||||
theme: 'striped',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onProgress?.(90, 'Finalizing PDF...');
|
||||
onProgress?.(90, 'Finalizing PDF...');
|
||||
|
||||
const pdfBlob = doc.output('blob');
|
||||
const pdfBlob = doc.output('blob');
|
||||
|
||||
onProgress?.(100, 'Complete!');
|
||||
return pdfBlob;
|
||||
onProgress?.(100, 'Complete!');
|
||||
return pdfBlob;
|
||||
}
|
||||
|
||||
|
||||
function groupByTagName(elements: Element[]): Record<string, Element[]> {
|
||||
const groups: Record<string, Element[]> = {};
|
||||
|
||||
for (const element of elements) {
|
||||
const tagName = element.tagName;
|
||||
if (!groups[tagName]) {
|
||||
groups[tagName] = [];
|
||||
}
|
||||
groups[tagName].push(element);
|
||||
}
|
||||
|
||||
return groups;
|
||||
function isPlainObject(val: unknown): val is Record<string, unknown> {
|
||||
return val !== null && typeof val === 'object' && !Array.isArray(val);
|
||||
}
|
||||
|
||||
function extractTableData(elements: Element[]): { headers: string[], rows: string[][] } {
|
||||
if (elements.length === 0) {
|
||||
return { headers: [], rows: [] };
|
||||
}
|
||||
|
||||
const headerSet = new Set<string>();
|
||||
for (const element of elements) {
|
||||
for (const child of Array.from(element.children)) {
|
||||
headerSet.add(child.tagName);
|
||||
}
|
||||
}
|
||||
const headers = Array.from(headerSet);
|
||||
|
||||
const rows: string[][] = [];
|
||||
for (const element of elements) {
|
||||
const row: string[] = [];
|
||||
for (const header of headers) {
|
||||
const child = element.querySelector(header);
|
||||
row.push(child?.textContent?.trim() || '');
|
||||
}
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
return { headers, rows };
|
||||
function groupChildrenByTagName(
|
||||
entries: [string, unknown][]
|
||||
): Record<string, Record<string, unknown>[]> {
|
||||
const groups: Record<string, Record<string, unknown>[]> = {};
|
||||
for (const [tagName, value] of entries) {
|
||||
const items = Array.isArray(value) ? value : [value];
|
||||
const normalized: Record<string, unknown>[] = items.map((v) => {
|
||||
if (isPlainObject(v)) return v;
|
||||
if (v == null) return {};
|
||||
return { [TEXT_KEY]: String(v) };
|
||||
});
|
||||
groups[tagName] = normalized;
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
function extractTableData(elements: Record<string, unknown>[]): {
|
||||
headers: string[];
|
||||
rows: string[][];
|
||||
} {
|
||||
if (elements.length === 0) return { headers: [], rows: [] };
|
||||
|
||||
function extractKeyValuePairs(element: Element): string[][] {
|
||||
const pairs: string[][] = [];
|
||||
|
||||
for (const child of Array.from(element.children)) {
|
||||
const key = child.tagName;
|
||||
const value = child.textContent?.trim() || '';
|
||||
if (value) {
|
||||
pairs.push([formatTitle(key), value]);
|
||||
}
|
||||
const headerSet = new Set<string>();
|
||||
for (const element of elements) {
|
||||
for (const key of Object.keys(element)) {
|
||||
if (key.startsWith(ATTR_PREFIX)) continue;
|
||||
if (key === TEXT_KEY) continue;
|
||||
headerSet.add(key);
|
||||
}
|
||||
}
|
||||
const headers = Array.from(headerSet);
|
||||
if (headers.length === 0) return { headers: [], rows: [] };
|
||||
|
||||
for (const attr of Array.from(element.attributes)) {
|
||||
pairs.push([formatTitle(attr.name), attr.value]);
|
||||
const rows: string[][] = [];
|
||||
for (const element of elements) {
|
||||
const row: string[] = [];
|
||||
for (const header of headers) {
|
||||
row.push(toSafeText(stringifyValue(element[header])));
|
||||
}
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
return pairs;
|
||||
return { headers, rows };
|
||||
}
|
||||
|
||||
function extractKeyValuePairs(obj: Record<string, unknown>): string[][] {
|
||||
const pairs: string[][] = [];
|
||||
|
||||
for (const [key, val] of Object.entries(obj)) {
|
||||
if (key.startsWith(ATTR_PREFIX)) continue;
|
||||
if (key === TEXT_KEY) continue;
|
||||
const strVal = toSafeText(stringifyValue(val));
|
||||
if (strVal) {
|
||||
pairs.push([formatTitle(key), strVal]);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, val] of Object.entries(obj)) {
|
||||
if (!key.startsWith(ATTR_PREFIX)) continue;
|
||||
const attrName = key.slice(ATTR_PREFIX.length);
|
||||
pairs.push([formatTitle(attrName), toSafeText(stringifyValue(val))]);
|
||||
}
|
||||
|
||||
return pairs;
|
||||
}
|
||||
|
||||
function stringifyValue(val: unknown): string {
|
||||
if (val == null) return '';
|
||||
if (typeof val === 'string') return val;
|
||||
if (typeof val === 'number' || typeof val === 'boolean') return String(val);
|
||||
if (Array.isArray(val)) {
|
||||
return val
|
||||
.map((v) => stringifyValue(v))
|
||||
.filter((s) => s.length > 0)
|
||||
.join(', ');
|
||||
}
|
||||
if (isPlainObject(val)) {
|
||||
const parts: string[] = [];
|
||||
if (TEXT_KEY in val) {
|
||||
const t = stringifyValue(val[TEXT_KEY]);
|
||||
if (t) parts.push(t);
|
||||
}
|
||||
for (const [k, v] of Object.entries(val)) {
|
||||
if (k.startsWith(ATTR_PREFIX)) continue;
|
||||
if (k === TEXT_KEY) continue;
|
||||
const inner = stringifyValue(v);
|
||||
if (inner) parts.push(inner);
|
||||
}
|
||||
return parts.join(' ');
|
||||
}
|
||||
return String(val);
|
||||
}
|
||||
|
||||
function toSafeText(raw: string | null | undefined): string {
|
||||
if (raw == null) return '';
|
||||
return (
|
||||
String(raw)
|
||||
// eslint-disable-next-line no-control-regex
|
||||
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
|
||||
function formatTitle(tagName: string): string {
|
||||
return tagName
|
||||
.replace(/[_-]/g, ' ')
|
||||
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||
.split(' ')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(' ');
|
||||
return tagName
|
||||
.replace(/[_-]/g, ' ')
|
||||
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||
.split(' ')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user