parses xjustiz and orders attachments

This commit is contained in:
2026-06-12 12:17:13 +02:00
parent 187f7b3a6a
commit 15a3fc5877
3 changed files with 144 additions and 16 deletions
+129 -6
View File
@@ -10,9 +10,8 @@ export type ZipAttachment = {
};
export type ProcessedZipArchive = {
name: string;
attachments: ZipAttachment[];
xjustizNachrichtXml: Uint8Array | null;
name: string
attachments: ZipAttachment[]
};
const IMAGE_EXTENSIONS = new Set([
@@ -30,6 +29,7 @@ const IMAGE_EXTENSIONS = new Set([
]);
const ZIP_META_FILE_NAME = "xjustiz_nachricht.xml";
const XML_TEXT_DECODER = new TextDecoder();
const unzipArchive = (data: Uint8Array) =>
new Promise<Record<string, Uint8Array>>((resolve, reject) => {
@@ -56,6 +56,126 @@ const isPdf = (path: string) => getExtension(path) === ".pdf";
const isImage = (path: string) => IMAGE_EXTENSIONS.has(getExtension(path));
const normalizeKey = (value: string) => value.replaceAll("\\", "/").toLowerCase().trim();
const matchesElementName = (element: Element, requestedName: string) => {
const requestedLocalName = requestedName.split(".").pop() ?? requestedName;
return (
element.localName === requestedName ||
element.localName === requestedLocalName ||
element.tagName === requestedName ||
element.tagName === requestedLocalName ||
element.tagName.endsWith(`:${requestedLocalName}`)
);
};
const getDirectChildByLocalName = (element: Element, localName: string) =>
Array.from(element.children).find((child) => matchesElementName(child, localName)) ?? null;
const getFirstDescendantByLocalName = (root: Document | Element, localName: string) =>
Array.from(root.querySelectorAll("*")).find((element) => matchesElementName(element, localName)) ?? null;
const getTextContentByPath = (root: Element, path: string[]) => {
let current: Element | null = root;
for (const localName of path) {
current = current ? getDirectChildByLocalName(current, localName) : null;
if (!current) {
return null;
}
}
const textContent = current.textContent?.trim();
return textContent ? textContent : null;
};
const parseXmlDocument = (xmlBytes: Uint8Array) => {
const parser = new DOMParser();
const document = parser.parseFromString(XML_TEXT_DECODER.decode(xmlBytes), "application/xml");
if (document.querySelector("parsererror")) {
return null;
}
return document;
};
const getXJustizDocumentNames = (xmlBytes: Uint8Array) => {
const document = parseXmlDocument(xmlBytes);
if (!document) {
return [];
}
const schriftgutobjekte = getFirstDescendantByLocalName(document, "schriftgutobjekte");
if (!schriftgutobjekte) {
return [];
}
const dokumentNodes = Array.from(schriftgutobjekte.children).filter((child) =>
matchesElementName(child, "dokument"),
);
return dokumentNodes
.map((dokument) => getTextContentByPath(dokument, ["xjustiz.fachspezifischeDaten", "datei", "dateiname"]))
.filter((name): name is string => Boolean(name));
};
const orderAttachmentsByDocumentNames = (attachments: ZipAttachment[], documentNames: string[]) => {
const buckets = new Map<string, ZipAttachment[]>();
for (const attachment of attachments) {
const keys = new Set([normalizeKey(attachment.path), normalizeKey(attachment.name)]);
for (const key of keys) {
const bucket = buckets.get(key) ?? [];
bucket.push(attachment);
buckets.set(key, bucket);
}
}
const usedAttachments = new Set<ZipAttachment>();
const orderedAttachments: ZipAttachment[] = [];
const takeAttachment = (key: string) => {
const bucket = buckets.get(key);
if (!bucket) {
return null;
}
const nextAttachment = bucket.find((attachment) => !usedAttachments.has(attachment)) ?? null;
if (nextAttachment) {
usedAttachments.add(nextAttachment);
}
return nextAttachment;
};
for (const documentName of documentNames) {
const normalizedName = normalizeKey(documentName);
const normalizedBaseName = normalizeKey(getBaseName(documentName));
const matchedAttachment = takeAttachment(normalizedName) ?? takeAttachment(normalizedBaseName);
if (matchedAttachment) {
orderedAttachments.push(matchedAttachment);
}
}
for (const attachment of attachments) {
if (!usedAttachments.has(attachment)) {
orderedAttachments.push(attachment);
}
}
return orderedAttachments;
};
export const extractZipArchive = async (file: File): Promise<ProcessedZipArchive> => {
const archiveBytes = new Uint8Array(await file.arrayBuffer());
const files = await unzipArchive(archiveBytes);
@@ -91,9 +211,12 @@ export const extractZipArchive = async (file: File): Promise<ProcessedZipArchive
}
}
const documentNames = xjustizNachrichtXml ? getXJustizDocumentNames(xjustizNachrichtXml) : [];
const orderedAttachments = orderAttachmentsByDocumentNames(attachments, documentNames);
const archiveName = documentNames[0] ? getBaseName(documentNames[0]) : file.name;
return {
name: file.name,
attachments,
xjustizNachrichtXml,
name: archiveName,
attachments: orderedAttachments
};
};
-4
View File
@@ -20,7 +20,6 @@
try {
const processedZipArchive = await extractZipArchive(file);
processedZipArchive.xjustizNachrichtXml
processedZipFiles = [...processedZipFiles, processedZipArchive];
} catch (error) {
console.error(`Failed to process ${file.name}`, error);
@@ -53,9 +52,6 @@
{#each processedZipFiles as file}
<li class="rounded-container bg-primary-50 px-4 py-3 text-[14px] font-medium text-primary-900">
{file.name} - {file.attachments.length} relevante Datei{file.attachments.length === 1 ? "" : "en"}
{#if file.xjustizNachrichtXml}
- xjustiz_nachricht.xml vorhanden
{/if}
</li>
{/each}
</ul>