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",
|
"diff": "^8.0.3",
|
||||||
"dompurify": "^3.4.0",
|
"dompurify": "^3.4.0",
|
||||||
"embedpdf-snippet": "file:vendor/embedpdf/embedpdf-snippet-2.9.1.tgz",
|
"embedpdf-snippet": "file:vendor/embedpdf/embedpdf-snippet-2.9.1.tgz",
|
||||||
|
"fast-xml-parser": "^5.7.1",
|
||||||
"heic2any": "^0.0.4",
|
"heic2any": "^0.0.4",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
@@ -2074,6 +2075,18 @@
|
|||||||
"integrity": "sha512-ObyTmabopTMwN6AGwrEkmDbdmM5wnwlRdn9jnJYQ2KbBUPdPDcfNf1QkcWFOVD+qIgSqILz9nK51e22T+x+gkQ==",
|
"integrity": "sha512-ObyTmabopTMwN6AGwrEkmDbdmM5wnwlRdn9jnJYQ2KbBUPdPDcfNf1QkcWFOVD+qIgSqILz9nK51e22T+x+gkQ==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/@oxc-project/types": {
|
||||||
"version": "0.122.0",
|
"version": "0.122.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
|
||||||
@@ -6993,6 +7006,42 @@
|
|||||||
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
|
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
|
||||||
"license": "(MIT AND Zlib)"
|
"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": {
|
"node_modules/fdir": {
|
||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
@@ -9861,6 +9910,21 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/path-key": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
"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"
|
"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": {
|
"node_modules/stylis": {
|
||||||
"version": "4.3.6",
|
"version": "4.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
|
||||||
|
|||||||
@@ -94,6 +94,7 @@
|
|||||||
"diff": "^8.0.3",
|
"diff": "^8.0.3",
|
||||||
"dompurify": "^3.4.0",
|
"dompurify": "^3.4.0",
|
||||||
"embedpdf-snippet": "file:vendor/embedpdf/embedpdf-snippet-2.9.1.tgz",
|
"embedpdf-snippet": "file:vendor/embedpdf/embedpdf-snippet-2.9.1.tgz",
|
||||||
|
"fast-xml-parser": "^5.7.1",
|
||||||
"heic2any": "^0.0.4",
|
"heic2any": "^0.0.4",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"html2canvas": "^1.4.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) => {
|
self.addEventListener('message', (event) => {
|
||||||
if (!event.data) return;
|
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') {
|
if (event.data.type === 'SKIP_WAITING') {
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -43,7 +43,26 @@ interface ErrorResult {
|
|||||||
message: string;
|
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;
|
const msg = e.data;
|
||||||
try {
|
try {
|
||||||
if (msg.type === 'diff') {
|
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';
|
'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');
|
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';
|
img.className = 'max-w-full max-h-full object-contain';
|
||||||
|
|
||||||
const p = document.createElement('p');
|
const p = document.createElement('p');
|
||||||
|
|||||||
@@ -203,9 +203,16 @@ function initializePage(): void {
|
|||||||
|
|
||||||
if (sigImageThumb && sigImagePreview) {
|
if (sigImageThumb && sigImagePreview) {
|
||||||
const url = URL.createObjectURL(file);
|
const url = URL.createObjectURL(file);
|
||||||
sigImageThumb.src = url;
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
if (parsed.protocol === 'blob:') {
|
||||||
|
sigImageThumb.src = parsed.href;
|
||||||
sigImagePreview.classList.remove('hidden');
|
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 { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
|
||||||
import { downloadFile, escapeHtml, hexToRgb } from '../utils/helpers.js';
|
import { downloadFile, escapeHtml, hexToRgb } from '../utils/helpers.js';
|
||||||
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.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 class="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-semibold text-gray-300 mb-1">Field Name ${field.type === 'radio' ? '(Group Name)' : ''}</label>
|
<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>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
propertiesPanel.innerHTML = DOMPurify.sanitize(propertiesHtml, {
|
||||||
|
ADD_ATTR: ['target'],
|
||||||
|
});
|
||||||
|
|
||||||
// Common listeners
|
// Common listeners
|
||||||
const propName = document.getElementById('propName') as HTMLInputElement;
|
const propName = document.getElementById('propName') as HTMLInputElement;
|
||||||
const nameError = document.getElementById('nameError') as HTMLDivElement;
|
const nameError = document.getElementById('nameError') as HTMLDivElement;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { jsPDF } from 'jspdf';
|
import { jsPDF } from 'jspdf';
|
||||||
import autoTable from 'jspdf-autotable';
|
import autoTable from 'jspdf-autotable';
|
||||||
|
import { XMLParser } from 'fast-xml-parser';
|
||||||
|
|
||||||
export interface XmlToPdfOptions {
|
export interface XmlToPdfOptions {
|
||||||
onProgress?: (percent: number, message: string) => void;
|
onProgress?: (percent: number, message: string) => void;
|
||||||
@@ -9,6 +10,9 @@ interface jsPDFWithAutoTable extends jsPDF {
|
|||||||
lastAutoTable?: { finalY: number };
|
lastAutoTable?: { finalY: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ATTR_PREFIX = '@_';
|
||||||
|
const TEXT_KEY = '#text';
|
||||||
|
|
||||||
export async function convertXmlToPdf(
|
export async function convertXmlToPdf(
|
||||||
file: File,
|
file: File,
|
||||||
options?: XmlToPdfOptions
|
options?: XmlToPdfOptions
|
||||||
@@ -16,48 +20,82 @@ export async function convertXmlToPdf(
|
|||||||
const { onProgress } = options || {};
|
const { onProgress } = options || {};
|
||||||
|
|
||||||
onProgress?.(10, 'Reading XML file...');
|
onProgress?.(10, 'Reading XML file...');
|
||||||
const xmlText = await file.text();
|
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...');
|
onProgress?.(30, 'Parsing XML structure...');
|
||||||
const parser = new DOMParser();
|
const parser = new XMLParser({
|
||||||
const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
|
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');
|
let parsed: Record<string, unknown>;
|
||||||
if (parseError) {
|
try {
|
||||||
throw new Error('Invalid XML: ' + parseError.textContent);
|
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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
|
||||||
onProgress?.(50, 'Analyzing data structure...');
|
onProgress?.(50, 'Analyzing data structure...');
|
||||||
|
|
||||||
const doc: jsPDFWithAutoTable = new jsPDF({
|
const doc: jsPDFWithAutoTable = new jsPDF({
|
||||||
orientation: 'landscape',
|
orientation: 'landscape',
|
||||||
unit: 'mm',
|
unit: 'mm',
|
||||||
format: 'a4'
|
format: 'a4',
|
||||||
});
|
});
|
||||||
|
|
||||||
const pageWidth = doc.internal.pageSize.getWidth();
|
const pageWidth = doc.internal.pageSize.getWidth();
|
||||||
let yPosition = 20;
|
let yPosition = 20;
|
||||||
|
|
||||||
const root = xmlDoc.documentElement;
|
|
||||||
const rootName = formatTitle(root.tagName);
|
|
||||||
|
|
||||||
doc.setFontSize(18);
|
doc.setFontSize(18);
|
||||||
doc.setFont('helvetica', 'bold');
|
doc.setFont('helvetica', 'bold');
|
||||||
doc.text(rootName, pageWidth / 2, yPosition, { align: 'center' });
|
doc.text(formatTitle(rootName), pageWidth / 2, yPosition, {
|
||||||
|
align: 'center',
|
||||||
|
});
|
||||||
yPosition += 15;
|
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) {
|
if (childEntries.length > 0) {
|
||||||
const groups = groupByTagName(children);
|
const groups = groupChildrenByTagName(childEntries);
|
||||||
|
|
||||||
|
const renderableGroups: Array<[string, Record<string, unknown>[]]> = [];
|
||||||
for (const [groupName, elements] of Object.entries(groups)) {
|
for (const [groupName, elements] of Object.entries(groups)) {
|
||||||
const { headers, rows } = extractTableData(elements);
|
const { headers, rows } = extractTableData(elements);
|
||||||
|
|
||||||
if (headers.length > 0 && rows.length > 0) {
|
if (headers.length > 0 && rows.length > 0) {
|
||||||
if (Object.keys(groups).length > 1) {
|
renderableGroups.push([groupName, elements]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [groupName, elements] of renderableGroups) {
|
||||||
|
const { headers, rows } = extractTableData(elements);
|
||||||
|
|
||||||
|
if (renderableGroups.length > 1) {
|
||||||
doc.setFontSize(14);
|
doc.setFontSize(14);
|
||||||
doc.setFont('helvetica', 'bold');
|
doc.setFont('helvetica', 'bold');
|
||||||
doc.text(formatTitle(groupName), 14, yPosition);
|
doc.text(formatTitle(groupName), 14, yPosition);
|
||||||
@@ -65,7 +103,7 @@ export async function convertXmlToPdf(
|
|||||||
}
|
}
|
||||||
|
|
||||||
autoTable(doc, {
|
autoTable(doc, {
|
||||||
head: [headers.map(h => formatTitle(h))],
|
head: [headers.map((h) => formatTitle(h))],
|
||||||
body: rows,
|
body: rows,
|
||||||
startY: yPosition,
|
startY: yPosition,
|
||||||
styles: {
|
styles: {
|
||||||
@@ -85,14 +123,13 @@ export async function convertXmlToPdf(
|
|||||||
theme: 'striped',
|
theme: 'striped',
|
||||||
didDrawPage: (data) => {
|
didDrawPage: (data) => {
|
||||||
yPosition = (data.cursor?.y || yPosition) + 10;
|
yPosition = (data.cursor?.y || yPosition) + 10;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
yPosition = (doc.lastAutoTable?.finalY || yPosition) + 15;
|
yPosition = (doc.lastAutoTable?.finalY || yPosition) + 15;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const kvPairs = extractKeyValuePairs(root);
|
const kvPairs = extractKeyValuePairs(rootObj);
|
||||||
if (kvPairs.length > 0) {
|
if (kvPairs.length > 0) {
|
||||||
autoTable(doc, {
|
autoTable(doc, {
|
||||||
head: [['Property', 'Value']],
|
head: [['Property', 'Value']],
|
||||||
@@ -116,6 +153,7 @@ export async function convertXmlToPdf(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onProgress?.(90, 'Finalizing PDF...');
|
onProgress?.(90, 'Finalizing PDF...');
|
||||||
|
|
||||||
@@ -125,40 +163,48 @@ export async function convertXmlToPdf(
|
|||||||
return pdfBlob;
|
return pdfBlob;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPlainObject(val: unknown): val is Record<string, unknown> {
|
||||||
function groupByTagName(elements: Element[]): Record<string, Element[]> {
|
return val !== null && typeof val === 'object' && !Array.isArray(val);
|
||||||
const groups: Record<string, Element[]> = {};
|
|
||||||
|
|
||||||
for (const element of elements) {
|
|
||||||
const tagName = element.tagName;
|
|
||||||
if (!groups[tagName]) {
|
|
||||||
groups[tagName] = [];
|
|
||||||
}
|
|
||||||
groups[tagName].push(element);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractTableData(elements: Element[]): { headers: string[], rows: string[][] } {
|
function extractTableData(elements: Record<string, unknown>[]): {
|
||||||
if (elements.length === 0) {
|
headers: string[];
|
||||||
return { headers: [], rows: [] };
|
rows: string[][];
|
||||||
}
|
} {
|
||||||
|
if (elements.length === 0) return { headers: [], rows: [] };
|
||||||
|
|
||||||
const headerSet = new Set<string>();
|
const headerSet = new Set<string>();
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
for (const child of Array.from(element.children)) {
|
for (const key of Object.keys(element)) {
|
||||||
headerSet.add(child.tagName);
|
if (key.startsWith(ATTR_PREFIX)) continue;
|
||||||
|
if (key === TEXT_KEY) continue;
|
||||||
|
headerSet.add(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const headers = Array.from(headerSet);
|
const headers = Array.from(headerSet);
|
||||||
|
if (headers.length === 0) return { headers: [], rows: [] };
|
||||||
|
|
||||||
const rows: string[][] = [];
|
const rows: string[][] = [];
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
const row: string[] = [];
|
const row: string[] = [];
|
||||||
for (const header of headers) {
|
for (const header of headers) {
|
||||||
const child = element.querySelector(header);
|
row.push(toSafeText(stringifyValue(element[header])));
|
||||||
row.push(child?.textContent?.trim() || '');
|
|
||||||
}
|
}
|
||||||
rows.push(row);
|
rows.push(row);
|
||||||
}
|
}
|
||||||
@@ -166,31 +212,69 @@ function extractTableData(elements: Element[]): { headers: string[], rows: strin
|
|||||||
return { headers, rows };
|
return { headers, rows };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractKeyValuePairs(obj: Record<string, unknown>): string[][] {
|
||||||
function extractKeyValuePairs(element: Element): string[][] {
|
|
||||||
const pairs: string[][] = [];
|
const pairs: string[][] = [];
|
||||||
|
|
||||||
for (const child of Array.from(element.children)) {
|
for (const [key, val] of Object.entries(obj)) {
|
||||||
const key = child.tagName;
|
if (key.startsWith(ATTR_PREFIX)) continue;
|
||||||
const value = child.textContent?.trim() || '';
|
if (key === TEXT_KEY) continue;
|
||||||
if (value) {
|
const strVal = toSafeText(stringifyValue(val));
|
||||||
pairs.push([formatTitle(key), value]);
|
if (strVal) {
|
||||||
|
pairs.push([formatTitle(key), strVal]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const attr of Array.from(element.attributes)) {
|
for (const [key, val] of Object.entries(obj)) {
|
||||||
pairs.push([formatTitle(attr.name), attr.value]);
|
if (!key.startsWith(ATTR_PREFIX)) continue;
|
||||||
|
const attrName = key.slice(ATTR_PREFIX.length);
|
||||||
|
pairs.push([formatTitle(attrName), toSafeText(stringifyValue(val))]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return pairs;
|
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 {
|
function formatTitle(tagName: string): string {
|
||||||
return tagName
|
return tagName
|
||||||
.replace(/[_-]/g, ' ')
|
.replace(/[_-]/g, ' ')
|
||||||
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||||
.split(' ')
|
.split(' ')
|
||||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||||
.join(' ');
|
.join(' ');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user