feat: Add VitePress docs, EPUB to PDF tool, Phosphor icons, and licensing updates
- Set up VitePress documentation site (docs:dev, docs:build, docs:preview) - Added Getting Started, Tools Reference, Contributing, and Commercial License pages - Created self-hosting guides for Docker, Vercel, Netlify, Cloudflare, AWS, Hostinger, Nginx, Apache - Updated README with documentation link, sponsors section, and docs contribution guide - Added EPUB to PDF converter using LibreOffice WASM - Migrated to Phosphor Icons for consistent iconography - Added donation ribbon banner on landing page - Removed 'Like My Work?' section (replaced by ribbon) - Updated licensing.html with delivery model, AGPL notice, invoicing, and no-refund policy - Added Commercial License documentation page - Updated translations table (Chinese added, marked non-English as In Progress) - Added sponsors.yml workflow for auto-generating sponsor avatars
This commit is contained in:
196
src/js/utils/xml-to-pdf.ts
Normal file
196
src/js/utils/xml-to-pdf.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import { jsPDF } from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
|
||||
export interface XmlToPdfOptions {
|
||||
onProgress?: (percent: number, message: string) => void;
|
||||
}
|
||||
|
||||
interface jsPDFWithAutoTable extends jsPDF {
|
||||
lastAutoTable?: { finalY: number };
|
||||
}
|
||||
|
||||
export async function convertXmlToPdf(
|
||||
file: File,
|
||||
options?: XmlToPdfOptions
|
||||
): Promise<Blob> {
|
||||
const { onProgress } = options || {};
|
||||
|
||||
onProgress?.(10, 'Reading XML file...');
|
||||
const xmlText = await file.text();
|
||||
|
||||
onProgress?.(30, 'Parsing XML structure...');
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
|
||||
|
||||
const parseError = xmlDoc.querySelector('parsererror');
|
||||
if (parseError) {
|
||||
throw new Error('Invalid XML: ' + parseError.textContent);
|
||||
}
|
||||
|
||||
onProgress?.(50, 'Analyzing data structure...');
|
||||
|
||||
const doc: jsPDFWithAutoTable = new jsPDF({
|
||||
orientation: 'landscape',
|
||||
unit: 'mm',
|
||||
format: 'a4'
|
||||
});
|
||||
|
||||
const pageWidth = doc.internal.pageSize.getWidth();
|
||||
let yPosition = 20;
|
||||
|
||||
const root = xmlDoc.documentElement;
|
||||
const rootName = formatTitle(root.tagName);
|
||||
|
||||
doc.setFontSize(18);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(rootName, pageWidth / 2, yPosition, { align: 'center' });
|
||||
yPosition += 15;
|
||||
|
||||
onProgress?.(60, 'Generating formatted content...');
|
||||
|
||||
const children = Array.from(root.children);
|
||||
|
||||
if (children.length > 0) {
|
||||
const groups = groupByTagName(children);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
} 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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onProgress?.(90, 'Finalizing PDF...');
|
||||
|
||||
const pdfBlob = doc.output('blob');
|
||||
|
||||
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 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 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]);
|
||||
}
|
||||
}
|
||||
|
||||
for (const attr of Array.from(element.attributes)) {
|
||||
pairs.push([formatTitle(attr.name), attr.value]);
|
||||
}
|
||||
|
||||
return pairs;
|
||||
}
|
||||
|
||||
|
||||
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(' ');
|
||||
}
|
||||
Reference in New Issue
Block a user