Features/shadcn #1

Merged
rehlert merged 17 commits from features/shadcn into main 2026-07-22 19:15:48 +00:00
Showing only changes of commit e3518eaed0 - Show all commits
+78 -45
View File
@@ -1,6 +1,6 @@
import { unzip } from "fflate"; import { unzip } from 'fflate';
export type ZipAttachmentKind = "pdf" | "image"; export type ZipAttachmentKind = 'pdf' | 'image';
export type ZipAttachment = { export type ZipAttachment = {
name: string; name: string;
@@ -10,25 +10,31 @@ export type ZipAttachment = {
}; };
export type ProcessedZipArchive = { export type ProcessedZipArchive = {
name: string name: string;
attachments: ZipAttachment[] attachments: ZipAttachment[];
};
type XJustizMetadata = {
sender: string;
receiver: string;
documentNames: string[];
}; };
const IMAGE_EXTENSIONS = new Set([ const IMAGE_EXTENSIONS = new Set([
".avif", '.avif',
".bmp", '.bmp',
".gif", '.gif',
".heic", '.heic',
".jpeg", '.jpeg',
".jpg", '.jpg',
".jp2", '.jp2',
".png", '.png',
".tif", '.tif',
".tiff", '.tiff',
".webp", '.webp'
]); ]);
const ZIP_META_FILE_NAME = "xjustiz_nachricht.xml"; const ZIP_META_FILE_NAME = 'xjustiz_nachricht.xml';
const XML_TEXT_DECODER = new TextDecoder(); const XML_TEXT_DECODER = new TextDecoder();
const unzipArchive = (data: Uint8Array) => const unzipArchive = (data: Uint8Array) =>
@@ -43,37 +49,37 @@ const unzipArchive = (data: Uint8Array) =>
}); });
}); });
const getBaseName = (path: string) => path.split("/").pop() ?? path; const getBaseName = (path: string) => path.split('/').pop() ?? path;
const getExtension = (path: string) => { const getExtension = (path: string) => {
const baseName = getBaseName(path).toLowerCase(); const baseName = getBaseName(path).toLowerCase();
const lastDotIndex = baseName.lastIndexOf("."); const lastDotIndex = baseName.lastIndexOf('.');
return lastDotIndex === -1 ? "" : baseName.slice(lastDotIndex); return lastDotIndex === -1 ? '' : baseName.slice(lastDotIndex);
}; };
const isPdf = (path: string) => getExtension(path) === ".pdf"; const isPdf = (path: string) => getExtension(path) === '.pdf';
const isImage = (path: string) => IMAGE_EXTENSIONS.has(getExtension(path)); const isImage = (path: string) => IMAGE_EXTENSIONS.has(getExtension(path));
const isZipArchive = (path: string) => getExtension(path) === ".zip"; const isZipArchive = (path: string) => getExtension(path) === '.zip';
const normalizeKey = (value: string) => value.replaceAll("\\", "/").toLowerCase().trim(); const normalizeKey = (value: string) => value.replaceAll('\\', '/').toLowerCase().trim();
const isMacMetadataEntry = (path: string) => { const isMacMetadataEntry = (path: string) => {
const normalizedPath = normalizeKey(path); const normalizedPath = normalizeKey(path);
const baseName = getBaseName(normalizedPath); const baseName = getBaseName(normalizedPath);
return ( return (
normalizedPath.startsWith("__macosx/") || normalizedPath.startsWith('__macosx/') ||
normalizedPath.includes("/__macosx/") || normalizedPath.includes('/__macosx/') ||
baseName === ".ds_store" || baseName === '.ds_store' ||
baseName.startsWith("._") baseName.startsWith('._')
); );
}; };
const matchesElementName = (element: Element, requestedName: string) => { const matchesElementName = (element: Element, requestedName: string) => {
const requestedLocalName = requestedName.split(".").pop() ?? requestedName; const requestedLocalName = requestedName.split('.').pop() ?? requestedName;
return ( return (
element.localName === requestedName || element.localName === requestedName ||
@@ -88,7 +94,9 @@ const getDirectChildByLocalName = (element: Element, localName: string) =>
Array.from(element.children).find((child) => matchesElementName(child, localName)) ?? null; Array.from(element.children).find((child) => matchesElementName(child, localName)) ?? null;
const getFirstDescendantByLocalName = (root: Document | Element, localName: string) => const getFirstDescendantByLocalName = (root: Document | Element, localName: string) =>
Array.from(root.querySelectorAll("*")).find((element) => matchesElementName(element, localName)) ?? null; Array.from(root.querySelectorAll('*')).find((element) =>
matchesElementName(element, localName)
) ?? null;
const getTextContentByPath = (root: Element, path: string[]) => { const getTextContentByPath = (root: Element, path: string[]) => {
let current: Element | null = root; let current: Element | null = root;
@@ -106,37 +114,54 @@ const getTextContentByPath = (root: Element, path: string[]) => {
return textContent ? textContent : null; return textContent ? textContent : null;
}; };
const getTextContentByLocalName = (root: Document | Element, localName: string) =>
getFirstDescendantByLocalName(root, localName)?.textContent?.trim() ?? null;
const parseXmlDocument = (xmlBytes: Uint8Array) => { const parseXmlDocument = (xmlBytes: Uint8Array) => {
const parser = new DOMParser(); const parser = new DOMParser();
const document = parser.parseFromString(XML_TEXT_DECODER.decode(xmlBytes), "application/xml"); const document = parser.parseFromString(XML_TEXT_DECODER.decode(xmlBytes), 'application/xml');
if (document.querySelector("parsererror")) { if (document.querySelector('parsererror')) {
return null; return null;
} }
return document; return document;
}; };
const getXJustizDocumentNames = (xmlBytes: Uint8Array) => { const parseXJustizMetadata = (xmlBytes: Uint8Array): XJustizMetadata | null => {
const document = parseXmlDocument(xmlBytes); const document = parseXmlDocument(xmlBytes);
if (!document) { if (!document) {
return []; return null;
} }
const schriftgutobjekte = getFirstDescendantByLocalName(document, "schriftgutobjekte"); const sender = getTextContentByLocalName(document, 'aktenzeichen.absender') ?? '';
const receiver = getTextContentByLocalName(document, 'aktenzeichen.empfaenger') ?? '';
const schriftgutobjekte = getFirstDescendantByLocalName(document, 'schriftgutobjekte');
if (!schriftgutobjekte) { if (!schriftgutobjekte) {
return []; return {
sender,
receiver,
documentNames: []
};
} }
const dokumentNodes = Array.from(schriftgutobjekte.children).filter((child) => const dokumentNodes = Array.from(schriftgutobjekte.children).filter((child) =>
matchesElementName(child, "dokument"), matchesElementName(child, 'dokument')
); );
return dokumentNodes const documentNames = dokumentNodes
.map((dokument) => getTextContentByPath(dokument, ["xjustiz.fachspezifischeDaten", "datei", "dateiname"])) .map((dokument) =>
getTextContentByPath(dokument, ['xjustiz.fachspezifischeDaten', 'datei', 'dateiname'])
)
.filter((name): name is string => Boolean(name)); .filter((name): name is string => Boolean(name));
return {
sender,
receiver,
documentNames
};
}; };
const orderAttachmentsByDocumentNames = (attachments: ZipAttachment[], documentNames: string[]) => { const orderAttachmentsByDocumentNames = (attachments: ZipAttachment[], documentNames: string[]) => {
@@ -190,7 +215,10 @@ const orderAttachmentsByDocumentNames = (attachments: ZipAttachment[], documentN
return orderedAttachments; return orderedAttachments;
}; };
const extractZipEntries = async (archiveBytes: Uint8Array, fallbackName: string): Promise<ProcessedZipArchive[]> => { const extractZipEntries = async (
archiveBytes: Uint8Array,
_fallbackName: string
): Promise<ProcessedZipArchive[]> => {
const files = await unzipArchive(archiveBytes); const files = await unzipArchive(archiveBytes);
const attachments: ZipAttachment[] = []; const attachments: ZipAttachment[] = [];
const nestedArchives: ProcessedZipArchive[] = []; const nestedArchives: ProcessedZipArchive[] = [];
@@ -218,8 +246,8 @@ const extractZipEntries = async (archiveBytes: Uint8Array, fallbackName: string)
attachments.push({ attachments.push({
name: baseName, name: baseName,
path, path,
kind: "pdf", kind: 'pdf',
data, data
}); });
continue; continue;
} }
@@ -228,21 +256,26 @@ const extractZipEntries = async (archiveBytes: Uint8Array, fallbackName: string)
attachments.push({ attachments.push({
name: baseName, name: baseName,
path, path,
kind: "image", kind: 'image',
data, data
}); });
} }
} }
const documentNames = xjustizNachrichtXml ? getXJustizDocumentNames(xjustizNachrichtXml) : []; const metadata = xjustizNachrichtXml ? parseXJustizMetadata(xjustizNachrichtXml) : null;
const documentNames = metadata?.documentNames ?? [];
const orderedAttachments = orderAttachmentsByDocumentNames(attachments, documentNames); const orderedAttachments = orderAttachmentsByDocumentNames(attachments, documentNames);
const archiveName = documentNames[0] ? getBaseName(documentNames[0]) : fallbackName; const now = new Date();
const date = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
const archiveName = metadata
? `${date}_${metadata.sender}_${metadata.receiver}.pdf`
: `${date}_unbekannt_unbekannt.pdf`;
const archives: ProcessedZipArchive[] = []; const archives: ProcessedZipArchive[] = [];
if (orderedAttachments.length > 0) { if (orderedAttachments.length > 0) {
archives.push({ archives.push({
name: archiveName, name: archiveName,
attachments: orderedAttachments, attachments: orderedAttachments
}); });
} }