diff --git a/.gitignore b/.gitignore index 59c20a0..13fc0ae 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ dist-ssr *.sw? .vercel vercel.json +emsdk diff --git a/README.md b/README.md index 0c059bd..c6a285d 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ model-checker/ ├── public/ # Static assets and Python files │ ├── locales/ # Translation files │ ├── python/ # Python scripts -│ └── pyodideWorker.js # Web worker for Pyodide +│ └── pyodideWorkerClean.js # Web worker for Pyodide ├── src/ # Source code │ ├── assets/ # Images and assets │ ├── components/ # React components diff --git a/emsdk b/emsdk new file mode 160000 index 0000000..eff90ca --- /dev/null +++ b/emsdk @@ -0,0 +1 @@ +Subproject commit eff90ca04a3785f571a8095b3a42b63799cf384a diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json index 44decbe..f9851e1 100644 --- a/public/locales/de/translation.json +++ b/public/locales/de/translation.json @@ -116,6 +116,7 @@ "report-format-description": "Wählen Sie eines oder beide Berichtsformate. HTML-Berichte können direkt angezeigt und gedruckt werden, während BCF-Berichte in BIM-Kollaborationstools verwendet werden können.", "view-html": "HTML", "download-bcf": "BCF", + "downloadHtmlTooltip": "HTML-Bericht herunterladen", "select-report-format": "Bitte wählen Sie mindestens ein Berichtsformat aus", "report": { "summary": "Zusammenfassung", @@ -127,6 +128,8 @@ "name": "Name", "description": "Beschreibung", "warning": "Warnung", + "failureReason": "Fehlergrund", + "specNotApplyToVersion": "Spezifikation gilt nicht für diese IFC-Version", "globalId": "Globale ID", "tag": "Kennzeichnung", "reportBy": "Bericht von", @@ -138,14 +141,38 @@ "skipped": "ÜBERSPRUNGEN" }, "errorMessages": { - "propertySetNotExist": "Der erforderliche Eigenschaftssatz existiert nicht", + "propertySetNotExist": "Der erforderliche PropertySet existiert nicht", "dataShallBeProvided": "Daten müssen im Datensatz bereitgestellt werden", "propertyNotExist": "Die erforderliche Eigenschaft existiert nicht", "invalidValue": "hat einen ungültigen Wert", "notInRange": "liegt nicht im erlaubten Bereich", "notFound": "wurde nicht gefunden", "doesNotHave": "hat keine", - "missingProperty": "fehlt die erforderliche Eigenschaft" + "missingProperty": "fehlt die erforderliche Eigenschaft", + "attributeValueDoesNotMatch": "Der Attributwert \"{{value}}\" entspricht nicht der Anforderung", + "doesNotMatchRequirement": "entspricht nicht der Anforderung", + "entityClassDoesNotMeet": "Die Entitätsklasse \"{{actual}}\" erfüllt nicht die erforderliche IFC-Klasse", + "predefinedTypeDoesNotMeet": "Der vordefinierte Typ \"{{actual}}\" erfüllt nicht den erforderlichen Typ", + "requiredAttributeNotExist": "Das erforderliche Attribut existiert nicht", + "attributeValueEmpty": "Der Attributwert \"{{actual}}\" ist leer", + "invalidAttributeName": "Ein ungültiger Attributname wurde in der IDS angegeben", + "attributeShouldNotMeet": "Der Attributwert hätte die Anforderung nicht erfüllen dürfen", + "noClassification": "Die Entität hat keine Klassifizierung", + "referencesDoNotMatch": "Die Referenzen \"{{actual}}\" entsprechen nicht den Anforderungen", + "systemsDoNotMatch": "Die Systeme \"{{actual}}\" entsprechen nicht den Anforderungen", + "classificationShouldNotMeet": "Die Klassifizierung hätte die Anforderung nicht erfüllen dürfen", + "noRelationship": "Die Entität hat keine Beziehung", + "relationshipIncorrectEntities": "Die Entität hat eine Beziehung mit falschen Entitäten: \"{{actual}}\"", + "relationshipIncorrectPredefinedType": "Die Entität hat eine Beziehung mit falschem vordefinierten Typ: \"{{actual}}\"", + "relationshipShouldNotMeet": "Die Beziehung hätte die Anforderung nicht erfüllen dürfen", + "propertySetDoesNotContain": "Der PropertySet enthält nicht die erforderliche Eigenschaft", + "propertyDataTypeMismatch": "Der Datentyp \"{{actual}}\" der Eigenschaft entspricht nicht dem erforderlichen Datentyp \"{{dataType}}\"", + "propertyValueDoesNotMatch": "Der Eigenschaftswert \"{{actual}}\" entspricht nicht den Anforderungen", + "propertyValuesDoNotMatch": "Die Eigenschaftswerte \"{{actual}}\" entsprechen nicht den Anforderungen", + "propertyShouldNotMeet": "Die Eigenschaft hätte die Anforderung nicht erfüllen dürfen", + "noMaterial": "Die Entität hat kein Material", + "materialDoesNotMatch": "Die Materialnamen und -kategorien \"{{actual}}\" entsprechen nicht der Anforderung", + "materialShouldNotMeet": "Das Material hätte die Anforderung nicht erfüllen dürfen" }, "interface": { "passed": "erfüllt", @@ -156,25 +183,64 @@ "elementsPassedPrefix": "Elemente erfüllt", "applicability": "Anwendbarkeit", "all": "Alle", - "data": "Daten" + "data": "Daten", + "allEntityData": "Alle {{entity}} Daten", + "matchesPattern": "entspricht dem Muster {{pattern}}", + "equals": "ist {{value}}", + "oneOf": "ist einer von: {{values}}", + "range": "liegt zwischen {{min}} und {{max}}", + "greaterOrEqual": "ist grösser oder gleich {{value}}", + "greaterThan": "ist grösser als {{value}}", + "lessOrEqual": "ist kleiner oder gleich {{value}}", + "lessThan": "ist kleiner als {{value}}" }, "phrases": { "moreOfSameType": "... {{count}} weitere vom gleichen Elementtyp ({{type}} mit Kennzeichnung {{tag}} und Globaler ID {{id}}) nicht angezeigt ...", "moreElementsNotShown": "... {{count}} weitere {{type}} Elemente nicht angezeigt von insgesamt {{total}} ..." }, "ids": { - "section": { - "loadBearing": "Tragende Struktur" + "sections": { + "projectDesignation": "Projektbezeichnung", + "projectPerimeter": "Projektperimeter", + "buildingDesignation": "Gebäudebezeichnung", + "storeyDesignation": "Geschossbezeichnung" }, - "description": { - "shouldHaveLoadBearing": "Alle Structure Elements sollte haben eine tragende Struktur" + "entities": { + "IfcProject": "Projekt", + "IfcSite": "Grundstück", + "IfcBuilding": "Gebäude", + "IfcBuildingStorey": "Geschoss" }, - "pattern": { - "nameShallBe": "Der Name muss {{value}} sein", + "fields": { + "name": "Name", + "description": "Beschreibung", + "phase": "Phase" + }, + "propertySets": { + "Pset_PropertyAgreement": "Eigenschaftsvereinbarung", + "Cust_Site": "Standort-Eigenschaften" + }, + "patterns": { + "nameShallBe": "{{field}} muss {{value}} sein", "descriptionRequired": "Die Beschreibung muss angegeben werden", - "propertyRequired": "{{property}} muss im Eigenschaftssatz {{propertySet}} angegeben werden", - "propertyValue": "{{property}} muss {{value}} im Eigenschaftssatz {{propertySet}} sein", - "enumRequired": "Eine der Aufzählungswerte {{values}} muss im Eigenschaftssatz {{propertySet}} angegeben werden" + "propertyRequired": "{{property}} muss im PropertySet {{propertySet}} angegeben werden", + "propertyValue": "{{property}} muss {{value}} im PropertySet {{propertySet}} sein", + "dataRequired": "Die Daten zu {{property}} müssen {{value}} sein und im PropertySet {{propertySet}} enthalten sein", + "enumRequired": "{{field}} muss einer der folgenden Werte sein: {{values}}", + "propertyInSet": "Eigenschaft {{property}} im PropertySet {{propertySet}}", + "propertyWithValue": "Eigenschaft {{property}} im PropertySet {{propertySet}} mit Wert {{value}}" + }, + "enumerations": { + "51_0": "51.0", + "52_0": "52.0", + "53_0": "53.0", + "untergeschoss": "Untergeschoss", + "erdgeschoss": "Erdgeschoss", + "obergeschoss": "Obergeschoss", + "zwischengeschoss": "Zwischengeschoss", + "dach": "Dach", + "u01": "U01", + "da": "DA" } } }, @@ -182,6 +248,7 @@ "loading": { "pyodide": "Pyodide wird geladen...", "pyodideSuccess": "Pyodide erfolgreich geladen", + "idsFile": "IDS-Datei wird gelesen...", "translations": "Übersetzungen werden geladen...", "packages": "Erforderliche Pakete werden installiert...", "micropipPatch": "Micropip wird für Kompatibilitätsprüfungen angepasst...", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 23a670c..1baff4d 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -124,6 +124,7 @@ "report-format-description": "Select one or both report formats. HTML reports can be viewed and printed directly, while BCF reports can be used in BIM collaboration tools.", "view-html": "HTML", "download-bcf": "BCF", + "downloadHtmlTooltip": "Download HTML report", "select-report-format": "Please select at least one report format", "report": { "summary": "Summary", @@ -135,6 +136,8 @@ "name": "Name", "description": "Description", "warning": "Warning", + "failureReason": "Failure Reason", + "specNotApplyToVersion": "specification does not apply to this IFC version", "globalId": "GlobalId", "tag": "Tag", "reportBy": "Report by", @@ -146,14 +149,38 @@ "skipped": "SKIPPED" }, "errorMessages": { - "propertySetNotExist": "The required property set does not exist", + "propertySetNotExist": "The required PropertySet does not exist", "dataShallBeProvided": "data shall be provided in the dataset", "propertyNotExist": "The required property does not exist", "invalidValue": "has an invalid value", "notInRange": "is not in the allowed range", "notFound": "was not found", "doesNotHave": "does not have", - "missingProperty": "is missing the required property" + "missingProperty": "is missing the required property", + "attributeValueDoesNotMatch": "The attribute value \"{{value}}\" does not match the requirement", + "doesNotMatchRequirement": "does not match the requirement", + "entityClassDoesNotMeet": "The entity class \"{{actual}}\" does not meet the required IFC class", + "predefinedTypeDoesNotMeet": "The predefined type \"{{actual}}\" does not meet the required type", + "requiredAttributeNotExist": "The required attribute did not exist", + "attributeValueEmpty": "The attribute value \"{{actual}}\" is empty", + "invalidAttributeName": "An invalid attribute name was specified in the IDS", + "attributeShouldNotMeet": "The attribute value should not have met the requirement", + "noClassification": "The entity has no classification", + "referencesDoNotMatch": "The references \"{{actual}}\" do not match the requirements", + "systemsDoNotMatch": "The systems \"{{actual}}\" do not match the requirements", + "classificationShouldNotMeet": "The classification should not have met the requirement", + "noRelationship": "The entity has no relationship", + "relationshipIncorrectEntities": "The entity has a relationship with incorrect entities: \"{{actual}}\"", + "relationshipIncorrectPredefinedType": "The entity has a relationship with incorrect predefined type: \"{{actual}}\"", + "relationshipShouldNotMeet": "The relationship should not have met the requirement", + "propertySetDoesNotContain": "The PropertySet does not contain the required property", + "propertyDataTypeMismatch": "The property's data type \"{{actual}}\" does not match the required data type of \"{{dataType}}\"", + "propertyValueDoesNotMatch": "The property value \"{{actual}}\" does not match the requirements", + "propertyValuesDoNotMatch": "The property values \"{{actual}}\" do not match the requirements", + "propertyShouldNotMeet": "The property should not have met the requirement", + "noMaterial": "The entity has no material", + "materialDoesNotMatch": "The material names and categories of \"{{actual}}\" does not match the requirement", + "materialShouldNotMeet": "The material should not have met the requirement" }, "interface": { "passed": "passed", @@ -166,6 +193,15 @@ "all": "All", "data": "data" }, + "allEntityData": "All {{entity}} data", + "matchesPattern": "matches the pattern {{pattern}}", + "equals": "is {{value}}", + "oneOf": "is one of: {{values}}", + "range": "is between {{min}} and {{max}}", + "greaterOrEqual": "is greater than or equal to {{value}}", + "greaterThan": "is greater than {{value}}", + "lessOrEqual": "is less than or equal to {{value}}", + "lessThan": "is less than {{value}}", "phrases": { "moreOfSameType": "... {{count}} more of the same element type ({{type}} with Tag {{tag}} and GlobalId {{id}}) not shown ...", "moreElementsNotShown": "... {{count}} more {{type}} elements not shown out of {{total}} total ..." @@ -190,6 +226,7 @@ "loading": { "pyodide": "Loading Pyodide...", "pyodideSuccess": "Pyodide loaded successfully", + "idsFile": "Reading IDS file content...", "translations": "Loading translations...", "packages": "Installing required packages...", "micropipPatch": "Patching micropip for compatibility check bypass...", diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index 80a3389..4d8235e 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -117,6 +117,7 @@ "report-format-description": "Sélectionnez un ou plusieurs formats de rapport. Les rapports HTML peuvent être consultés et imprimés directement, tandis que les rapports BCF peuvent être utilisés dans les outils de collaboration BIM.", "view-html": "HTML", "download-bcf": "BCF", + "downloadHtmlTooltip": "Télécharger le rapport HTML", "select-report-format": "Veuillez sélectionner au moins un format de rapport", "report": { "summary": "Résumé", @@ -128,6 +129,8 @@ "name": "Nom", "description": "Description", "warning": "Avertissement", + "failureReason": "Raison de l'échec", + "specNotApplyToVersion": "la spécification ne s'applique pas à cette version IFC", "globalId": "ID Global", "tag": "Étiquette", "reportBy": "Rapport par", @@ -139,14 +142,38 @@ "skipped": "IGNORÉ" }, "errorMessages": { - "propertySetNotExist": "L'ensemble de propriétés requis n'existe pas", + "propertySetNotExist": "Le PropertySet requis n'existe pas", "dataShallBeProvided": "Les données doivent être fournies dans l'ensemble de données", "propertyNotExist": "La propriété requise n'existe pas", "invalidValue": "a une valeur invalide", "notInRange": "n'est pas dans la plage autorisée", "notFound": "n'a pas été trouvé", "doesNotHave": "n'a pas de", - "missingProperty": "la propriété requise est manquante" + "missingProperty": "la propriété requise est manquante", + "attributeValueDoesNotMatch": "La valeur de l'attribut \"{{value}}\" ne correspond pas à l'exigence", + "doesNotMatchRequirement": "ne correspond pas à l'exigence", + "entityClassDoesNotMeet": "La classe d'entité \"{{actual}}\" ne répond pas à la classe IFC requise", + "predefinedTypeDoesNotMeet": "Le type prédéfini \"{{actual}}\" ne répond pas au type requis", + "requiredAttributeNotExist": "L'attribut requis n'existait pas", + "attributeValueEmpty": "La valeur de l'attribut \"{{actual}}\" est vide", + "invalidAttributeName": "Un nom d'attribut invalide a été spécifié dans l'IDS", + "attributeShouldNotMeet": "La valeur de l'attribut n'aurait pas dû répondre à l'exigence", + "noClassification": "L'entité n'a pas de classification", + "referencesDoNotMatch": "Les références \"{{actual}}\" ne correspondent pas aux exigences", + "systemsDoNotMatch": "Les systèmes \"{{actual}}\" ne correspondent pas aux exigences", + "classificationShouldNotMeet": "La classification n'aurait pas dû répondre à l'exigence", + "noRelationship": "L'entité n'a pas de relation", + "relationshipIncorrectEntities": "L'entité a une relation avec des entités incorrectes : \"{{actual}}\"", + "relationshipIncorrectPredefinedType": "L'entité a une relation avec un type prédéfini incorrect : \"{{actual}}\"", + "relationshipShouldNotMeet": "La relation n'aurait pas dû répondre à l'exigence", + "propertySetDoesNotContain": "Le PropertySet ne contient pas la propriété requise", + "propertyDataTypeMismatch": "Le type de données \"{{actual}}\" de la propriété ne correspond pas au type de données requis \"{{dataType}}\"", + "propertyValueDoesNotMatch": "La valeur de la propriété \"{{actual}}\" ne correspond pas aux exigences", + "propertyValuesDoNotMatch": "Les valeurs de la propriété \"{{actual}}\" ne correspondent pas aux exigences", + "propertyShouldNotMeet": "La propriété n'aurait pas dû répondre à l'exigence", + "noMaterial": "L'entité n'a pas de matériau", + "materialDoesNotMatch": "Les noms et catégories de matériau \"{{actual}}\" ne correspondent pas à l'exigence", + "materialShouldNotMeet": "Le matériau n'aurait pas dû répondre à l'exigence" }, "interface": { "passed": "réussi", @@ -159,6 +186,15 @@ "all": "Tous", "data": "Données" }, + "allEntityData": "Toutes les données {{entity}}", + "matchesPattern": "correspond au modèle {{pattern}}", + "equals": "est {{value}}", + "oneOf": "est l'un de : {{values}}", + "range": "est entre {{min}} et {{max}}", + "greaterOrEqual": "est supérieur ou égal à {{value}}", + "greaterThan": "est supérieur à {{value}}", + "lessOrEqual": "est inférieur ou égal à {{value}}", + "lessThan": "est inférieur à {{value}}", "phrases": { "moreOfSameType": "... {{count}} autres du même type d'élément ({{type}} avec l'étiquette {{tag}} et l'ID global {{id}}) non affichés ...", "moreElementsNotShown": "... {{count}} autres éléments {{type}} non affichés sur un total de {{total}} ..." @@ -173,9 +209,9 @@ "pattern": { "nameShallBe": "Le nom doit être {{value}}", "descriptionRequired": "La description doit être fournie", - "propertyRequired": "{{property}} doit être fourni dans l'ensemble de propriétés {{propertySet}}", - "propertyValue": "{{property}} doit être {{value}} dans l'ensemble de propriétés {{propertySet}}", - "enumRequired": "L'une des valeurs d'énumération {{values}} doit être fournie dans l'ensemble de propriétés {{propertySet}}" + "propertyRequired": "{{property}} doit être fourni dans le PropertySet {{propertySet}}", + "propertyValue": "{{property}} doit être {{value}} dans le PropertySet {{propertySet}}", + "enumRequired": "L'une des valeurs d'énumération {{values}} doit être fournie dans le PropertySet {{propertySet}}" } } }, diff --git a/public/locales/it/translation.json b/public/locales/it/translation.json index 18bf448..013799c 100644 --- a/public/locales/it/translation.json +++ b/public/locales/it/translation.json @@ -117,6 +117,7 @@ "report-format-description": "Seleziona uno o più formati di rapporto. I rapporti HTML possono essere visualizzati e stampati direttamente, mentre i rapporti BCF possono essere utilizzati negli strumenti di collaborazione BIM.", "view-html": "HTML", "download-bcf": "BCF", + "downloadHtmlTooltip": "Scarica rapporto HTML", "select-report-format": "Seleziona almeno un formato di rapporto", "report": { "summary": "Riepilogo", @@ -128,6 +129,8 @@ "name": "Nome", "description": "Descrizione", "warning": "Avviso", + "failureReason": "Motivo del fallimento", + "specNotApplyToVersion": "la specifica non si applica a questa versione IFC", "globalId": "ID Globale", "tag": "Tag", "reportBy": "Report di", @@ -139,14 +142,38 @@ "skipped": "SALTATO" }, "errorMessages": { - "propertySetNotExist": "Il set di proprietà richiesto non esiste", + "propertySetNotExist": "Il PropertySet richiesto non esiste", "dataShallBeProvided": "I dati devono essere forniti nel set di dati", "propertyNotExist": "La proprietà richiesta non esiste", "invalidValue": "ha un valore non valido", "notInRange": "non è nell'intervallo consentito", "notFound": "non è stato trovato", "doesNotHave": "non ha", - "missingProperty": "manca la proprietà richiesta" + "missingProperty": "manca la proprietà richiesta", + "attributeValueDoesNotMatch": "Il valore dell'attributo \"{{value}}\" non corrisponde al requisito", + "doesNotMatchRequirement": "non corrisponde al requisito", + "entityClassDoesNotMeet": "La classe dell'entità \"{{actual}}\" non soddisfa la classe IFC richiesta", + "predefinedTypeDoesNotMeet": "Il tipo predefinito \"{{actual}}\" non soddisfa il tipo richiesto", + "requiredAttributeNotExist": "L'attributo richiesto non esisteva", + "attributeValueEmpty": "Il valore dell'attributo \"{{actual}}\" è vuoto", + "invalidAttributeName": "È stato specificato un nome di attributo non valido nell'IDS", + "attributeShouldNotMeet": "Il valore dell'attributo non avrebbe dovuto soddisfare il requisito", + "noClassification": "L'entità non ha classificazione", + "referencesDoNotMatch": "Le referenze \"{{actual}}\" non corrispondono ai requisiti", + "systemsDoNotMatch": "I sistemi \"{{actual}}\" non corrispondono ai requisiti", + "classificationShouldNotMeet": "La classificazione non avrebbe dovuto soddisfare il requisito", + "noRelationship": "L'entità non ha relazione", + "relationshipIncorrectEntities": "L'entità ha una relazione con entità errate: \"{{actual}}\"", + "relationshipIncorrectPredefinedType": "L'entità ha una relazione con tipo predefinito errato: \"{{actual}}\"", + "relationshipShouldNotMeet": "La relazione non avrebbe dovuto soddisfare il requisito", + "propertySetDoesNotContain": "Il PropertySet non contiene la proprietà richiesta", + "propertyDataTypeMismatch": "Il tipo di dati \"{{actual}}\" della proprietà non corrisponde al tipo di dati richiesto \"{{dataType}}\"", + "propertyValueDoesNotMatch": "Il valore della proprietà \"{{actual}}\" non corrisponde ai requisiti", + "propertyValuesDoNotMatch": "I valori della proprietà \"{{actual}}\" non corrispondono ai requisiti", + "propertyShouldNotMeet": "La proprietà non avrebbe dovuto soddisfare il requisito", + "noMaterial": "L'entità non ha materiale", + "materialDoesNotMatch": "I nomi e le categorie del materiale \"{{actual}}\" non corrispondono al requisito", + "materialShouldNotMeet": "Il materiale non avrebbe dovuto soddisfare il requisito" }, "interface": { "passed": "superato", @@ -159,6 +186,15 @@ "all": "Tutti", "data": "Dati" }, + "allEntityData": "Tutti i dati {{entity}}", + "matchesPattern": "corrisponde al modello {{pattern}}", + "equals": "è {{value}}", + "oneOf": "è uno di: {{values}}", + "range": "è compreso tra {{min}} e {{max}}", + "greaterOrEqual": "è maggiore o uguale a {{value}}", + "greaterThan": "è maggiore di {{value}}", + "lessOrEqual": "è minore o uguale a {{value}}", + "lessThan": "è minore di {{value}}", "phrases": { "moreOfSameType": "... {{count}} altri dello stesso tipo di elemento ({{type}} con Tag {{tag}} e ID Globale {{id}}) non mostrati ...", "moreElementsNotShown": "... {{count}} altri elementi {{type}} non mostrati su un totale di {{total}} ..." @@ -173,9 +209,9 @@ "pattern": { "nameShallBe": "Il nome deve essere {{value}}", "descriptionRequired": "La descrizione deve essere fornita", - "propertyRequired": "{{property}} deve essere fornito nel set di proprietà {{propertySet}}", - "propertyValue": "{{property}} deve essere {{value}} nel set di proprietà {{propertySet}}", - "enumRequired": "Uno dei valori di enumerazione {{values}} deve essere fornito nel set di proprietà {{propertySet}}" + "propertyRequired": "{{property}} deve essere fornito nel PropertySet {{propertySet}}", + "propertyValue": "{{property}} deve essere {{value}} nel PropertySet {{propertySet}}", + "enumRequired": "Uno dei valori di enumerazione {{values}} deve essere fornito nel PropertySet {{propertySet}}" } } }, diff --git a/public/locales/rm/translation.json b/public/locales/rm/translation.json index 14692fe..c22974a 100644 --- a/public/locales/rm/translation.json +++ b/public/locales/rm/translation.json @@ -117,6 +117,7 @@ "report-format-description": "Tscherna in u plirs formats da rapport. Rapports HTML pon vegnir vesids e stampads directamain, entant che rapports BCF pon vegnir utilisads en instruments da collavuraziun BIM.", "view-html": "HTML", "download-bcf": "BCF", + "downloadHtmlTooltip": "Telechargiar rapport HTML", "select-report-format": "Tscherna almain in format da rapport", "report": { "summary": "Resumaziun", @@ -128,6 +129,8 @@ "name": "Num", "description": "Descripziun", "warning": "Avertiment", + "failureReason": "Motiv dal falliment", + "specNotApplyToVersion": "la specificaziun na s'applitgescha betg a questa versiun IFC", "globalId": "ID global", "tag": "Tag", "reportBy": "Rapport da", @@ -139,14 +142,38 @@ "skipped": "SURSIGLÌ" }, "errorMessages": { - "propertySetNotExist": "Il set da proprietads pretendì n'exista betg", + "propertySetNotExist": "Il PropertySet pretendì n'exista betg", "dataShallBeProvided": "Las datas ston vegnir furnidas en il set da datas", "propertyNotExist": "La proprietad pretendida n'exista betg", "invalidValue": "ha ina valur nunvalida", "notInRange": "n'è betg en il spectrum permess", "notFound": "n'è betg vegnì chattà", "doesNotHave": "n'ha betg", - "missingProperty": "manca la proprietad pretendida" + "missingProperty": "manca la proprietad pretendida", + "attributeValueDoesNotMatch": "La valur da l'attribut \"{{value}}\" na correspunda betg a la pretensiun", + "doesNotMatchRequirement": "na correspunda betg a la pretensiun", + "entityClassDoesNotMeet": "La classa da l'entitad \"{{actual}}\" na cuntent betg la classa IFC pretendida", + "predefinedTypeDoesNotMeet": "Il tip predefinì \"{{actual}}\" na cuntent betg il tip pretendì", + "requiredAttributeNotExist": "L'attribut pretendì n'existiva betg", + "attributeValueEmpty": "La valur da l'attribut \"{{actual}}\" è vida", + "invalidAttributeName": "In num d'attribut nunvalid è vegnì specificà en l'IDS", + "attributeShouldNotMeet": "La valur da l'attribut na duess betg avair cuntent la pretensiun", + "noClassification": "L'entitad n'ha nagina classificaziun", + "referencesDoNotMatch": "Las referenzas \"{{actual}}\" na correspundan betg a las pretensiuns", + "systemsDoNotMatch": "Ils sistems \"{{actual}}\" na correspundan betg a las pretensiuns", + "classificationShouldNotMeet": "La classificaziun na duess betg avair cuntent la pretensiun", + "noRelationship": "L'entitad n'ha nagina relaziun", + "relationshipIncorrectEntities": "L'entitad ha ina relaziun cun entitads sbagliadas: \"{{actual}}\"", + "relationshipIncorrectPredefinedType": "L'entitad ha ina relaziun cun tip predefinì sbaglià: \"{{actual}}\"", + "relationshipShouldNotMeet": "La relaziun na duess betg avair cuntent la pretensiun", + "propertySetDoesNotContain": "Il PropertySet na cuntegna betg la proprietad pretendida", + "propertyDataTypeMismatch": "Il tip da datas \"{{actual}}\" da la proprietad na correspunda betg al tip da datas pretendì \"{{dataType}}\"", + "propertyValueDoesNotMatch": "La valur da la proprietad \"{{actual}}\" na correspunda betg a las pretensiuns", + "propertyValuesDoNotMatch": "Las valurs da la proprietad \"{{actual}}\" na correspundan betg a las pretensiuns", + "propertyShouldNotMeet": "La proprietad na duess betg avair cuntent la pretensiun", + "noMaterial": "L'entitad n'ha nagin material", + "materialDoesNotMatch": "Ils nums e las categorias dal material \"{{actual}}\" na correspundan betg a la pretensiun", + "materialShouldNotMeet": "Il material na duess betg avair cuntent la pretensiun" }, "interface": { "passed": "reussì", @@ -159,6 +186,15 @@ "all": "Tut", "data": "Datas" }, + "allEntityData": "Tut las datas {{entity}}", + "matchesPattern": "correspunda al model {{pattern}}", + "equals": "è {{value}}", + "oneOf": "è in dad: {{values}}", + "range": "è tranter {{min}} e {{max}}", + "greaterOrEqual": "è pli grond u ugual a {{value}}", + "greaterThan": "è pli grond che {{value}}", + "lessOrEqual": "è pli pitschen u ugual a {{value}}", + "lessThan": "è pli pitschen che {{value}}", "phrases": { "moreOfSameType": "... {{count}} dapli dal medem tip d'element ({{type}} cun Tag {{tag}} ed ID global {{id}}) betg mussads ...", "moreElementsNotShown": "... {{count}} ulteriurs elements {{type}} betg mussads ord in total da {{total}} ..." @@ -173,9 +209,9 @@ "pattern": { "nameShallBe": "Il num sto esser {{value}}", "descriptionRequired": "La descripziun sto vegnir furnida", - "propertyRequired": "{{property}} sto vegnir furnì en il set da proprietads {{propertySet}}", - "propertyValue": "{{property}} sto esser {{value}} en il set da proprietads {{propertySet}}", - "enumRequired": "Ina da las valurs d'enumeraziun {{values}} sto vegnir furnida en il set da proprietads {{propertySet}}" + "propertyRequired": "{{property}} sto vegnir furnì en il PropertySet {{propertySet}}", + "propertyValue": "{{property}} sto esser {{value}} en il PropertySet {{propertySet}}", + "enumRequired": "Ina da las valurs d'enumeraziun {{values}} sto vegnir furnida en il PropertySet {{propertySet}}" } } }, diff --git a/public/pyodideWorker.js b/public/pyodideWorker.js deleted file mode 100644 index f2ab873..0000000 --- a/public/pyodideWorker.js +++ /dev/null @@ -1,1566 +0,0 @@ -/* global importScripts */ -importScripts('https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.js') - -// We'll load translations from language files instead of hardcoding them -let translations = {} -let consoleTranslations = {} - -// Basic initial translations for when the translation system isn't loaded yet -const INITIAL_TRANSLATIONS = { - en: { - loadingTranslations: 'Loading translations...', - }, - de: { - loadingTranslations: 'Übersetzungen werden geladen...', - }, - fr: { - loadingTranslations: 'Chargement des traductions...', - }, - it: { - loadingTranslations: 'Caricamento delle traduzioni...', - }, - rm: { - loadingTranslations: 'Chargiar translaziuns...', - }, -} - -let pyodide = null - -// Function to load translations from JSON files -async function loadTranslations(lang) { - try { - // Fetch the translation file for the given language - const response = await fetch(`./locales/${lang}/translation.json`) - if (!response.ok) { - console.error(`Failed to load translations for ${lang}:`, response.statusText) - // If loading fails, try loading English as fallback - if (lang !== 'en') { - return loadTranslations('en') - } - return {} - } - - const translationData = await response.json() - - // Save console translations separately for easier access - if (translationData.console) { - consoleTranslations = translationData - } - - // If there's a report section, use it for our translations - if (translationData.report) { - return translationData.report - } - - // If for some reason the report section doesn't exist, return empty object - console.error(`No report section found in ${lang} translations`) - return {} - } catch (error) { - console.error(`Error loading translations for ${lang}:`, error) - // If there's an error and we're not already trying English, fall back to English - if (lang !== 'en') { - return loadTranslations('en') - } - return {} - } -} - -// Helper function to get a translated console message -function getConsoleMessage(key, defaultMessage, params = {}) { - try { - // If we don't have any translations, return the default message - if (!consoleTranslations) { - return defaultMessage - } - - // For keys that don't start with 'console.', add the prefix - if (!key.startsWith('console.')) { - key = 'console.' + key - } - - // Split the key by dots to navigate nested objects - const keys = key.split('.') - let message = consoleTranslations - - // Navigate through the keys - for (const k of keys) { - if (message && message[k]) { - message = message[k] - } else { - // If we can't find the key, return the default message - return defaultMessage - } - } - - // If we found a string, use it; otherwise use the default - if (typeof message === 'string') { - // Replace any parameters in the message - let translatedMessage = message - for (const [key, value] of Object.entries(params)) { - translatedMessage = translatedMessage.replace(`{{${key}}}`, value) - } - return translatedMessage - } - - return defaultMessage - } catch (error) { - console.error('Error getting console message:', error) - return defaultMessage - } -} - -// Error type constants for specific handling -const ERROR_TYPES = { - OUT_OF_MEMORY: 'out_of_memory', -} - -// Helper function to detect specific error types -function detectErrorType(error) { - const errorStr = error.toString().toLowerCase() - - if (errorStr.includes('out of memory') || errorStr.includes('internalerror: out of memory')) { - return ERROR_TYPES.OUT_OF_MEMORY - } - - return null -} - -async function loadPyodide() { - if (pyodide !== null) { - return pyodide - } - - try { - self.postMessage({ - type: 'progress', - message: getConsoleMessage('console.loading.pyodide', 'Loading Pyodide...'), - }) - - pyodide = await loadPyodide({ - indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.23.4/full/', - }) - - self.postMessage({ - type: 'progress', - message: getConsoleMessage('console.loading.pyodideSuccess', 'Pyodide loaded successfully'), - }) - - return pyodide - } catch (error) { - self.postMessage({ - type: 'error', - message: getConsoleMessage('console.error.pyodideLoad', `Failed to load Pyodide: ${error.message}`, { - message: error.message, - }), - }) - throw error - } -} - -// Apply translations to the HTML report -function applyTranslations(html, translations, language) { - if (!translations || language === 'en') { - return html - } - - let translatedHtml = html; - - // Simple translation of key terms and phrases using patterns from the translation file - if (translations.ids && translations.ids.pattern) { - // Apply pattern-based translations using the templated strings in translation files - const patterns = translations.ids.pattern; - - // Replace name pattern - if (patterns.nameShallBe) { - translatedHtml = translatedHtml.replace(/The Name shall be (.*)/g, - (match, value) => patterns.nameShallBe.replace('{{value}}', value)); - } - - // Replace description pattern - if (patterns.descriptionRequired) { - translatedHtml = translatedHtml.replace(/The Description shall be provided/g, - patterns.descriptionRequired); - } - - // Replace property required pattern - if (patterns.propertyRequired) { - translatedHtml = translatedHtml.replace(/(\w+) shall be provided in the dataset (\w+)/g, - (match, property, propertySet) => patterns.propertyRequired - .replace('{{property}}', property) - .replace('{{propertySet}}', propertySet)); - } - - // Replace property value pattern - if (patterns.propertyValue) { - translatedHtml = translatedHtml.replace(/(\w+) shall be (.*?) in the dataset (\w+)/g, - (match, property, value, propertySet) => patterns.propertyValue - .replace('{{property}}', property) - .replace('{{value}}', value) - .replace('{{propertySet}}', propertySet)); - } - - // Replace enumeration pattern - if (patterns.enumRequired) { - translatedHtml = translatedHtml.replace(/One of the enumeration values \[(.*?)\] shall be provided in the dataset (\w+)/g, - (match, values, propertySet) => patterns.enumRequired - .replace('{{values}}', values) - .replace('{{propertySet}}', propertySet)); - } - } - - // Basic term replacements - const basicTerms = [ - { en: 'Summary', field: 'summary' }, - { en: 'Specifications', field: 'specifications' }, - { en: 'Requirements', field: 'requirements' }, - { en: 'Details', field: 'details' }, - { en: 'Class', field: 'class' }, - { en: 'PredefinedType', field: 'predefinedType' }, - { en: 'Name', field: 'name' }, - { en: 'Description', field: 'description' }, - { en: 'Warning', field: 'warning' }, - { en: 'GlobalId', field: 'globalId' }, - { en: 'Tag', field: 'tag' }, - { en: 'Report by', field: 'reportBy' }, - { en: 'and', field: 'and' }, - { en: 'PASS', field: 'status.pass' }, - { en: 'FAIL', field: 'status.fail' }, - { en: 'UNTESTED', field: 'status.untested' }, - { en: 'SKIPPED', field: 'status.skipped' } - ]; - - basicTerms.forEach(term => { - const fieldPath = term.field.split('.'); - let translation; - - if (fieldPath.length > 1) { - if (translations[fieldPath[0]] && translations[fieldPath[0]][fieldPath[1]]) { - translation = translations[fieldPath[0]][fieldPath[1]]; - } - } else { - translation = translations[term.field]; - } - - if (translation) { - const regex = new RegExp(`\\b${term.en}\\b`, 'g'); - translatedHtml = translatedHtml.replace(regex, translation); - } - }); - - // Interface element translations - if (translations.interface) { - // Replace passed/failed - if (translations.interface.passed) { - translatedHtml = translatedHtml.replace(/\bpassed\b/g, translations.interface.passed); - } - if (translations.interface.failed) { - translatedHtml = translatedHtml.replace(/\bfailed\b/g, translations.interface.failed); - } - - // Replace prefix phrases - [ - { prefix: 'Checks passed', field: 'checksPassedPrefix' }, - { prefix: 'Specifications passed', field: 'specificationsPassedPrefix' }, - { prefix: 'Requirements passed', field: 'requirementsPassedPrefix' }, - { prefix: 'Elements passed', field: 'elementsPassedPrefix' }, - { prefix: 'Applicability', field: 'applicability' }, - { prefix: 'All', field: 'all' }, - ].forEach(term => { - if (translations.interface[term.field]) { - const regex = new RegExp(`\\b${term.prefix}\\b`, 'g'); - translatedHtml = translatedHtml.replace(regex, translations.interface[term.field]); - } - }); - - // Handle "All X data" patterns - if (translations.interface.all && translations.interface.data) { - const regex = new RegExp(`All ([\\w]+) data`, 'g'); - translatedHtml = translatedHtml.replace(regex, (match, type) => { - return `${translations.interface.all} ${type} ${translations.interface.data}`; - }); - } - } - - // Error message translations - if (translations.errorMessages) { - Object.entries(translations.errorMessages).forEach(([key, translation]) => { - if (translation) { - // Match based on the English error messages - let pattern; - switch (key) { - case 'propertySetNotExist': - pattern = 'The required property set does not exist'; - break; - case 'dataShallBeProvided': - pattern = 'data shall be provided in the dataset'; - break; - case 'propertyNotExist': - pattern = 'The required property does not exist'; - break; - case 'invalidValue': - pattern = 'has an invalid value'; - break; - case 'notInRange': - pattern = 'is not in the allowed range'; - break; - case 'notFound': - pattern = 'was not found'; - break; - case 'doesNotHave': - pattern = 'does not have'; - break; - case 'missingProperty': - pattern = 'is missing the required property'; - break; - } - - if (pattern) { - translatedHtml = translatedHtml.replace(new RegExp(pattern, 'g'), translation); - } - } - }); - } - - // Handle complex phrases with placeholders - if (translations.phrases) { - // More of same type message - if (translations.phrases.moreOfSameType) { - const regex = /\.\.\. (\d+) more of the same element type \((.*?) with Tag (.*?) and GlobalId (.*?)\) not shown \.\.\./g; - translatedHtml = translatedHtml.replace(regex, (match, count, type, tag, id) => { - return translations.phrases.moreOfSameType - .replace('{{count}}', count) - .replace('{{type}}', type) - .replace('{{tag}}', tag) - .replace('{{id}}', id); - }); - } - - // More elements not shown message - if (translations.phrases.moreElementsNotShown) { - const regex = /\.\.\. (\d+) more (.*?) elements not shown out of (\d+) total \.\.\./g; - translatedHtml = translatedHtml.replace(regex, (match, count, type, total) => { - return translations.phrases.moreElementsNotShown - .replace('{{count}}', count) - .replace('{{type}}', type) - .replace('{{total}}', total); - }); - } - } - - // Handle section title translations for IDS - if (translations.ids && translations.ids.section) { - Object.entries(translations.ids.section).forEach(([key, translation]) => { - // Match based on English section titles - let pattern; - switch (key) { - case 'loadBearing': - pattern = 'Load Bearing'; - break; - // Add more sections as needed - } - - if (pattern && translation) { - const regex = new RegExp(`\\b${pattern}\\b`, 'g'); - translatedHtml = translatedHtml.replace(regex, translation); - } - }); - } - - // Handle description translations for IDS - if (translations.ids && translations.ids.description) { - Object.entries(translations.ids.description).forEach(([key, translation]) => { - // Match based on English descriptions - let pattern; - switch (key) { - case 'shouldHaveLoadBearing': - pattern = 'All Structure Elements should have a load bearing'; - break; - // Add more descriptions as needed - } - - if (pattern && translation) { - const regex = new RegExp(pattern, 'g'); - translatedHtml = translatedHtml.replace(regex, translation); - } - }); - } - - return translatedHtml; -} - -self.onmessage = async (event) => { - const { arrayBuffer, idsContent, fileName, language = 'en', generateBcf = false } = event.data - - console.log('Worker: Language received:', language) - - // Validate and normalize the language code - const normalizedLanguage = language.toLowerCase().split('-')[0] - const supportedLanguages = ['en', 'de', 'fr', 'it', 'rm'] // List of supported languages - const effectiveLanguage = supportedLanguages.includes(normalizedLanguage) ? normalizedLanguage : 'en' - - console.log('Worker: Using language:', effectiveLanguage) - - try { - // Load the translations first - self.postMessage({ - type: 'progress', - message: INITIAL_TRANSLATIONS[effectiveLanguage]?.loadingTranslations || 'Loading translations...', - }) - - translations = await loadTranslations(effectiveLanguage) - - // Now we can use translated messages - // Ensure pyodide is loaded - pyodide = await loadPyodide() - if (!pyodide) { - throw new Error('Failed to initialize Pyodide') - } - - // Import required packages - self.postMessage({ - type: 'progress', - message: getConsoleMessage('console.loading.packages', 'Preloading essential packages...'), - }) - - // Preload essential packages for better performance - await pyodide.loadPackage(['micropip', 'python-dateutil', 'six', 'numpy']) - - // Bypass the Emscripten version compatibility check for wheels. - self.postMessage({ - type: 'progress', - message: getConsoleMessage( - 'console.loading.micropipPatch', - 'Patching micropip for compatibility check bypass...', - ), - }) - - await pyodide.runPythonAsync(` -import micropip -from micropip._micropip import WheelInfo -WheelInfo.check_compatible = lambda self: None - `) - - // Install IfcOpenShell and dependencies - self.postMessage({ - type: 'progress', - message: getConsoleMessage('console.loading.ifcOpenShell', 'Installing IfcOpenShell...'), - }) - - await pyodide.runPythonAsync(` -import micropip -await micropip.install('https://cdn.jsdelivr.net/gh/IfcOpenShell/wasm-wheels@33b437e5fd5425e606f34aff602c42034ff5e6dc/ifcopenshell-0.8.1+latest-cp312-cp312-emscripten_3_1_58_wasm32.whl') - `) - - self.postMessage({ - type: 'progress', - message: getConsoleMessage('console.loading.dependencies', 'Installing additional dependencies...'), - }) - - await pyodide.runPythonAsync(` -import micropip -DEBUG = False -if DEBUG: - print("Installing core validation packages...") -await micropip.install(['lark', 'ifctester==0.8.1', 'bcf-client==0.8.1', 'pystache'], keep_going=True) -if DEBUG: - print("Core packages installed successfully") - `) - - self.postMessage({ - type: 'progress', - message: getConsoleMessage('console.loading.inputFiles', 'Processing input files...'), - }) - - // Run validation - self.postMessage({ - type: 'progress', - message: getConsoleMessage('console.loading.validation', 'Running IFC validation...'), - }) - - // Skip sqlite3 loading as it's not needed for basic IFC validation - - // Create virtual files for IFC and IDS data - self.postMessage({ - type: 'progress', - message: getConsoleMessage('console.loading.inputFiles', 'Processing input files...'), - }) - const uint8Array = new Uint8Array(arrayBuffer) - pyodide.FS.writeFile('model.ifc', uint8Array) - - if (idsContent) { - pyodide.FS.writeFile('spec.ids', idsContent) - } - - // Run the validation and generate reports directly using ifctester - self.postMessage({ - type: 'progress', - message: getConsoleMessage('console.loading.validation', 'Running IFC validation...'), - }) - await pyodide.runPythonAsync(` -import ifcopenshell -import os -import json -import base64 -import re -from datetime import datetime - -# Optimization flags - set to True for debugging empty reports issue -DEBUG = False - -# Performance note: This conditional BCF generation saves ~30-50% time -# when BCF is not requested by the user - -# Get BCF generation flag from worker data -generate_bcf = "` + generateBcf + `" == "true" - -# Open the IFC model from the virtual file system and detect version inline -model = ifcopenshell.open("model.ifc") - -# Detect IFC version from the loaded model -try: - schema_raw = getattr(model, 'schema_identifier', None) - schema_raw = schema_raw() if callable(schema_raw) else getattr(model, 'schema', '') - schema = (schema_raw or '').upper() - if 'IFC4X3' in schema: - detected_ifc_version = 'IFC4X3_ADD2' - elif 'IFC4' in schema: - detected_ifc_version = 'IFC4' - elif 'IFC2X3' in schema: - detected_ifc_version = 'IFC2X3' - else: - detected_ifc_version = 'IFC4' -except Exception: - detected_ifc_version = 'IFC4' - -# Create and load IDS specification -from ifctester.ids import Ids, get_schema -import xml.etree.ElementTree as ET - -# Register XML namespaces for correct parsing -ET.register_namespace('xs', 'http://www.w3.org/2001/XMLSchema') -ET.register_namespace('', 'http://standards.buildingsmart.org/IDS') - -# Helper: detect normalized IFC version from opened model -def _detect_ifc_version_from_model(model): - try: - from ifcopenshell.util.schema import get_fallback_schema - raw = (getattr(model, 'schema', '') or '').upper() - fb = get_fallback_schema(raw) - name = str(fb).upper() - except Exception: - name = (getattr(model, 'schema', '') or '').upper() - if 'IFC4X3' in name: - return 'IFC4X3_ADD2' - if 'IFC4' in name: - return 'IFC4' - if 'IFC2X3' in name: - return 'IFC2X3' - return 'IFC4' - -# Helper: inject ifcVersion attributes into IDS specifications in-memory -def _augment_ids_ifcversion(ids_xml_text, version_str): - try: - root = ET.fromstring(ids_xml_text) - # Determine namespace dynamically and support both prefixed and default ns - default_ns = 'http://standards.buildingsmart.org/IDS' - ns = {'ids': default_ns} - # Try common paths first - specs = root.findall('.//ids:specification', ns) - if not specs: - # Fallback for documents without explicit namespace prefixes - specs = root.findall('.//specification') - if not specs: - # Last resort: iterate and pick elements ending with 'specification' - specs = [el for el in root.iter() if isinstance(el.tag, str) and el.tag.endswith('specification')] - - def _normalize_tokens(value: str) -> str: - tokens = [t for t in (value or '').replace(',', ' ').split() if t] - normalized = [] - for t in tokens: - up = t.upper() - if 'IFC4X3' in up: - normalized.append('IFC4X3_ADD2') - elif 'IFC4' in up: - normalized.append('IFC4') - elif 'IFC2X3' in up: - normalized.append('IFC2X3') - else: - # keep unknown tokens to avoid being destructive - normalized.append(up) - # De-duplicate while preserving order - seen = set() - out = [] - for v in normalized: - if v not in seen: - seen.add(v) - out.append(v) - return ' '.join(out) if out else '' - - changed = False - for spec in specs: - current = spec.get('ifcVersion') - if current in (None, ''): - # Set a safe default when detection failed - value_to_set = (version_str or 'IFC2X3 IFC4 IFC4X3_ADD2') - spec.set('ifcVersion', value_to_set) - changed = True - else: - normalized = _normalize_tokens(current) - if normalized and normalized != current: - spec.set('ifcVersion', normalized) - changed = True - if changed: - return ET.tostring(root, encoding='utf-8', xml_declaration=True).decode('utf-8') - return ids_xml_text - except Exception as e: - print(f"Augment IDS ifcVersion failed: {e}") - return ids_xml_text - -if os.path.exists("spec.ids"): - try: - # 1. Read the IDS XML content - with open("spec.ids", "r") as f: - ids_content = f.read() - - # 1a. Ensure ifcVersion exists on all specifications (in-memory only) - # Use the IFC version detected from the loaded model - ids_content = _augment_ids_ifcversion(ids_content, detected_ifc_version) - if DEBUG: - print(f"Using detected IFC version for IDS augmentation: {detected_ifc_version}") - - if DEBUG: - print(f"Original IDS content length: {len(ids_content)}") - print(f"First 300 chars: {ids_content[:300]}") - - - # 2. Build an ElementTree from the XML - # Note: Pyodide doesn't support resolve_entities parameter, so we use basic parsing - tree = ET.ElementTree(ET.fromstring(ids_content)) - - # 3. Decode the XML using the IDS schema with proper namespace handling - # Use a more permissive schema for parsing - try: - decoded = get_schema().decode( - tree, - strip_namespaces=True, - namespaces={ - "": "http://standards.buildingsmart.org/IDS", - "xs": "http://www.w3.org/2001/XMLSchema" - } - ) - if DEBUG: - print("Standard schema decode succeeded") - except Exception as decode_error: - if DEBUG: - print(f"Standard schema decode failed: {decode_error}") - print("Attempting manual decode without strict validation...") - - # Parse the XML manually to extract specifications - root = tree.getroot() - specifications_elem = root.find('.//{http://standards.buildingsmart.org/IDS}specifications') - - if specifications_elem is not None: - # Extract info section first - info_elem = root.find('.//{http://standards.buildingsmart.org/IDS}info') - info_dict = {} - if info_elem is not None: - title_elem = info_elem.find('.//{http://standards.buildingsmart.org/IDS}title') - desc_elem = info_elem.find('.//{http://standards.buildingsmart.org/IDS}description') - if title_elem is not None: - info_dict['title'] = title_elem.text or 'Untitled' - if desc_elem is not None: - info_dict['description'] = desc_elem.text or '' - else: - info_dict = {'title': 'Untitled', 'description': ''} - - decoded = { - 'info': info_dict, - 'specifications': [] - } - - for spec_elem in specifications_elem.findall('.//{http://standards.buildingsmart.org/IDS}specification'): - spec_dict = { - 'name': spec_elem.get('name', 'Unknown'), - 'ifcVersion': ['IFC2X3', 'IFC4', 'IFC4X3_ADD2'], # Default fallback - 'applicability': [], - 'requirements': [] - } - - if DEBUG: - print(f"Processing specification: {spec_dict['name']}") - - # Extract applicability - for app_elem in spec_elem.findall('.//{http://standards.buildingsmart.org/IDS}applicability'): - app_dict = {'entity': []} - for entity_elem in app_elem.findall('.//{http://standards.buildingsmart.org/IDS}entity'): - name_elem = entity_elem.find('.//{http://standards.buildingsmart.org/IDS}name') - if name_elem is not None: - simple_value = name_elem.find('.//{http://standards.buildingsmart.org/IDS}simpleValue') - if simple_value is not None: - entity_name = simple_value.text - app_dict['entity'].append({'name': entity_name}) - if DEBUG: - print(f" Found entity: {entity_name}") - if app_dict['entity']: - spec_dict['applicability'].append(app_dict) - - # Extract requirements - for req_elem in spec_elem.findall('.//{http://standards.buildingsmart.org/IDS}requirements'): - req_dict = {'attribute': []} - for attr_elem in req_elem.findall('.//{http://standards.buildingsmart.org/IDS}attribute'): - attr_dict = { - 'cardinality': attr_elem.get('cardinality', 'required'), - 'name': None - } - name_elem = attr_elem.find('.//{http://standards.buildingsmart.org/IDS}name') - if name_elem is not None: - simple_value = name_elem.find('.//{http://standards.buildingsmart.org/IDS}simpleValue') - if simple_value is not None: - attr_name = simple_value.text - attr_dict['name'] = attr_name - if DEBUG: - print(f" Found attribute: {attr_name}") - if attr_dict['name']: - req_dict['attribute'].append(attr_dict) - if req_dict['attribute']: - spec_dict['requirements'].append(req_dict) - - if DEBUG: - print(f" Applicability count: {len(spec_dict['applicability'])}") - print(f" Requirements count: {len(spec_dict['requirements'])}") - - decoded['specifications'].append(spec_dict) - - if DEBUG: - print(f"Manual decode created {len(decoded['specifications'])} specifications") - else: - print("Could not find specifications element, creating empty structure") - decoded = { - 'info': {'title': 'Untitled', 'description': ''}, - 'specifications': [] - } - - # Note: ifcVersion is now added to XML before parsing, so this fallback is no longer needed - - # 3.5 Process schema values for proper type conversion and format simplification - if DEBUG: - print("Processing schema values for compatibility...") - - def process_schema_values(obj): - """ - Recursively process schema values for compatibility with Pyodide: - 1. Convert string numeric values to actual numeric types - 2. Transform complex XML schema structures into simpler formats - 3. Flatten nested restriction types into a format that ifctester can handle - """ - if isinstance(obj, dict): - # Handle special case of XSD restriction type - if 'xs:restriction' in obj: - restriction = obj['xs:restriction'] - base_type = restriction.get('@base', '') - result = {} - - # Process numeric restrictions (decimal, double, float) - if base_type in ('xs:decimal', 'xs:double', 'xs:float'): - # Extract min/max values - if 'xs:minInclusive' in restriction: - min_value = restriction['xs:minInclusive'].get('@value') - if min_value is not None: - result['min'] = float(min_value) - - if 'xs:maxInclusive' in restriction: - max_value = restriction['xs:maxInclusive'].get('@value') - if max_value is not None: - result['max'] = float(max_value) - - # Return simplified restriction - return result - - # Process standard numeric attributes - numeric_attrs = ['@min', '@max', '@minInclusive', '@maxInclusive', - '@minExclusive', '@maxExclusive', '@value'] - for attr in numeric_attrs: - if attr in obj and obj[attr] is not None: - try: - if isinstance(obj[attr], str) and (obj[attr].replace('.', '', 1).isdigit() or - (obj[attr].startswith('-') and obj[attr][1:].replace('.', '', 1).isdigit())): - obj[attr] = float(obj[attr]) - except (ValueError, TypeError): - pass - - # Process nested structures - for key, value in list(obj.items()): - if isinstance(value, (dict, list)): - obj[key] = process_schema_values(value) - - elif isinstance(obj, list): - return [process_schema_values(item) for item in obj] - - return obj - - # Apply schema processing - decoded = process_schema_values(decoded) - - # 4. Create an Ids instance and parse the decoded IDS - if DEBUG: - print(f"About to parse decoded structure with {len(decoded.get('specifications', []))} specifications") - print(f"Decoded structure keys: {list(decoded.keys())}") - print(f"Decoded info: {decoded.get('info', 'Missing')}") - - try: - ids = Ids().parse(decoded) - if DEBUG: - print(f"After parsing: IDS object has {len(ids.specifications)} specifications") - except Exception as parse_error: - print(f"IDS parsing failed: {parse_error}") - print(f"Creating minimal IDS object without complex validation...") - - # Create empty IDS with minimal functionality - ids = Ids() - ids.specifications = [] - - # Simple approach: create basic specification objects that won't trigger complex reporter methods - print(f"Creating {len(decoded.get('specifications', []))} basic specifications...") - - for i, spec_data in enumerate(decoded.get('specifications', [])): - spec_name = spec_data.get('name', f'Specification {i+1}') - print(f"Creating basic specification: {spec_name}") - print(f" With {len(spec_data.get('applicability', []))} applicability rules") - print(f" With {len(spec_data.get('requirements', []))} requirement rules") - - # Create a comprehensive minimal mock with all possible attributes - class MinimalSpec: - def __init__(self, name, data): - self.name = name - self.description = f"Auto-generated specification for {name}" - self.identifier = f"spec_{i+1}" - self.instructions = "No specific instructions" - # Use detected IFC version if available, otherwise use fallback - if detected_ifc_version and detected_ifc_version != "null": - self.ifcVersion = [detected_ifc_version] - else: - self.ifcVersion = ['IFC2X3', 'IFC4', 'IFC4X3_ADD2'] - - # Convert dictionaries to proper objects with to_string methods - class MockApplicability: - def __init__(self, app_data): - self.entity = app_data.get('entity', []) - - def to_string(self, context=None): - entities = [e.get('name', 'Unknown') for e in self.entity] - return f"Entities: {', '.join(entities)}" - - class MockRequirement: - def __init__(self, req_data): - self.attribute = req_data.get('attribute', []) - self.failures = [] - self.status = 'pass' - self.cardinality = req_data.get('cardinality', 'required') - - def to_string(self, context=None): - attrs = [a.get('name', 'Unknown') for a in self.attribute] - return f"Attributes: {', '.join(attrs)}" - - def asdict(self, context=None): - return { - 'type': 'requirement', - 'context': context or 'attribute', - 'attribute': self.attribute, - 'status': self.status, - 'cardinality': self.cardinality - } - - # Convert extracted data to proper objects - self.applicability = [MockApplicability(app) for app in data.get('applicability', [])] - self.requirements = [MockRequirement(req) for req in data.get('requirements', [])] - self.failed_entities = [] - self.applicable_entities = [] - self.status = 'pass' - self.total_pass = len(self.requirements) if self.requirements else 0 - self.total_fail = 0 - self.total_checks = len(self.requirements) if self.requirements else 0 - self.total_checks_pass = len(self.requirements) if self.requirements else 0 - self.total_checks_fail = 0 - self.minOccurs = 1 - self.maxOccurs = "unbounded" - self.required = True - - def __getattr__(self, name): - """Catch-all for missing attributes""" - print(f"MinimalSpec: Missing attribute '{name}' requested") - # Return sensible defaults for common attribute patterns - if 'total' in name or 'count' in name: - return 0 - elif name in ['total_pass', 'total_fail', 'total_checks', 'total_checks_pass', 'total_checks_fail']: - return 0 - elif name in ['total_applicable', 'percent_pass']: - return 0 - elif 'status' in name: - return 'pass' - elif 'version' in name or 'Version' in name: - return ['IFC2X3', 'IFC4', 'IFC4X3_ADD2'] - else: - return None - - def reset_status(self): - self.status = None - self.failed_entities = [] - self.applicable_entities = [] - - def validate(self, model, should_filter_version=True): - """Basic validation implementation that finds applicable entities and checks requirements""" - try: - # Find applicable entities - applicable_entities = [] - - if self.applicability: - for app in self.applicability: - if hasattr(app, 'entity') and app.entity: - for entity_info in app.entity: - entity_name = entity_info.get('name', '') - if entity_name: - # Try to find entities of this type in the model - try: - entities = model.by_type(entity_name) - applicable_entities.extend(entities) - print(f"Found {len(entities)} {entity_name} entities") - except Exception as e: - print(f"Error finding {entity_name} entities: {e}") - - self.applicable_entities = applicable_entities - print(f"Total applicable entities for {self.name}: {len(applicable_entities)}") - - # Check requirements against applicable entities - failed_entities = [] - passed_count = 0 - failed_count = 0 - - if self.requirements and applicable_entities: - for req in self.requirements: - if hasattr(req, 'attribute') and req.attribute: - for entity in applicable_entities: - # Basic check - just see if entity has the required attributes - entity_failed = False - for attr in req.attribute: - attr_name = attr.get('name', '') - if attr_name: - try: - # Check if entity has this attribute - if hasattr(entity, attr_name): - value = getattr(entity, attr_name) - if value is None or (isinstance(value, str) and not value.strip()): - entity_failed = True - break - else: - entity_failed = True - break - except: - entity_failed = True - break - - if entity_failed: - failed_entities.append(entity) - failed_count += 1 - else: - passed_count += 1 - - self.failed_entities = failed_entities - self.total_pass = passed_count - self.total_fail = failed_count - self.total_checks = len(applicable_entities) - - # Set overall status - if failed_entities: - self.status = 'fail' - else: - self.status = 'pass' - - print(f"Validation result for {self.name}: {self.status} ({passed_count} passed, {failed_count} failed)") - - except Exception as e: - print(f"Error in MinimalSpec.validate: {e}") - self.status = 'pass' # Default to pass on error - - return True - - def check_ifc_version(self, model): - return True - - def filter_elements(self, elements): - return elements - - def is_applicable(self, element): - return True - - def is_ifc_version(self, version): - """Check if this specification applies to the given IFC version""" - return version in self.ifcVersion - - spec_obj = MinimalSpec(spec_name, spec_data) - ids.specifications.append(spec_obj) - - if DEBUG: - print(f"Created minimal IDS with {len(ids.specifications)} specifications") - - # 4.5. Force add ifcVersion to ALL specifications (object level) - if detected_ifc_version and detected_ifc_version != "null": - fallback_version = [detected_ifc_version] - if DEBUG: - print(f"Using detected IFC version: {detected_ifc_version}") - else: - fallback_version = ["IFC2X3", "IFC4", "IFC4X3_ADD2"] - if DEBUG: - print("Using fallback IFC versions: IFC2X3, IFC4, IFC4X3_ADD2") - - if DEBUG: - print(f"Total specifications found: {len(ids.specifications)}") - for i, spec in enumerate(ids.specifications): - print(f"Specification {i+1}: {getattr(spec, 'name', 'Unknown')}") - print(f" - Current ifcVersion: {getattr(spec, 'ifcVersion', 'None')}") - - # Always set ifcVersion regardless of current value - spec.ifcVersion = fallback_version - print(f" - Set ifcVersion to: {fallback_version}") - - print(f"Finished setting ifcVersion for {len(ids.specifications)} specifications") - - # 4.6. Validate IFC compatibility using schema utilities - try: - from ifcopenshell.util.schema import get_declaration, is_a - schema = ifcopenshell.schema_by_name(detected_ifc_version if detected_ifc_version and detected_ifc_version != "null" else "IFC4") - - # Validate that IDS specifications reference valid IFC classes - for spec in ids.specifications: - if hasattr(spec, 'applicability') and spec.applicability: - for applicability in spec.applicability: - if hasattr(applicability, 'entity') and applicability.entity: - for entity in applicability.entity: - if hasattr(entity, 'name') and entity.name: - try: - # Check if the entity name is a valid IFC class - declaration = schema.declaration_by_name(entity.name) - if declaration: - if DEBUG: - print(f"Validated IFC class '{entity.name}' in specification: {getattr(spec, 'name', 'Unknown')}") - else: - if DEBUG: - print(f"Warning: '{entity.name}' is not a valid IFC class in {detected_ifc_version}") - except Exception as class_error: - if DEBUG: - print(f"Could not validate IFC class '{entity.name}': {class_error}") - except Exception as schema_validation_error: - if DEBUG: - print(f"Schema validation skipped: {schema_validation_error}") - - # 5. Validate specifications against the model - if DEBUG: - print(f"About to validate {len(ids.specifications)} specifications against the model") - try: - ids.validate(model) - if DEBUG: - print(f"Validation completed. Checking results...") - - # Memory optimization: Force garbage collection after validation - import gc - gc.collect() - - if DEBUG: - # Debug: Check what happened during validation - for i, spec in enumerate(ids.specifications): - print(f"Spec {i+1} '{spec.name}': status={getattr(spec, 'status', 'Unknown')}") - print(f" - failed_entities: {len(getattr(spec, 'failed_entities', []))}") - print(f" - applicable_entities: {len(getattr(spec, 'applicable_entities', []))}") - - # Debug: Check IFC model contents - print(f"IFC model info:") - print(f" - Schema: {getattr(model, 'schema', 'Unknown')}") - try: - # Count entities by type - entity_counts = {} - for entity in model: - entity_type = entity.is_a() - if entity_type in entity_counts: - entity_counts[entity_type] += 1 - else: - entity_counts[entity_type] = 1 - - print(f" - Total entities: {len(list(model))}") - print(f" - Entity types found: {list(entity_counts.keys())[:10]}") # Show first 10 - - # Check for expected entities from IDS - expected_entities = ['IfcProject', 'IfcBuildingStorey', 'IfcBuilding', 'IfcSpace', 'IfcSite', 'IfcBuildingElementProxy'] - found_entities = [entity for entity in expected_entities if entity in entity_counts] - missing_entities = [entity for entity in expected_entities if entity not in entity_counts] - - print(f" - Expected entities found: {found_entities}") - print(f" - Expected entities missing: {missing_entities}") - - if found_entities: - for entity_type in found_entities: - print(f" - {entity_type}: {entity_counts[entity_type]} instances") - - except Exception as model_error: - print(f" - Error inspecting model: {model_error}") - except Exception as validation_error: - print(f"Validation failed: {validation_error}") - # Continue anyway to see if we can generate reports - except Exception as e: - print(f"IDS Parsing Error: {str(e)}") - # Fallback to empty specs on error - ids = Ids() - ids.specifications = [] - ids.validate(model) -else: - # Validate model without specifications - ids = Ids() - ids.specifications = [] - ids.validate(model) - -# Generate reports using ifctester's built-in reporter classes -from ifctester import reporter - -# Patch the reporter classes to handle complex value structures and missing methods -def patch_reporters(): - """ - Apply runtime patches to ifctester reporter classes to handle - complex value structures and missing methods in the browser environment - """ - # Save the original to_ids_value method - original_to_ids_value = reporter.Facet.to_ids_value - - # Define a patched version that can handle complex structures - def patched_to_ids_value(self, parameter): - try: - # First try the original method - return original_to_ids_value(self, parameter) - except Exception as e: - # If that fails, handle complex structures more gracefully - if isinstance(parameter, dict): - # If it's a dictionary with min/max values, convert to a simple range string - if 'min' in parameter or 'max' in parameter: - min_val = parameter.get('min', '*') - max_val = parameter.get('max', '*') - return f"Range: [{min_val}, {max_val}]" - - # Handle other dictionary types - return str(parameter) - elif isinstance(parameter, (list, tuple)): - # Convert lists to comma-separated strings - return ", ".join(str(x) for x in parameter) - else: - # For other types, use string representation - return str(parameter) - - # Apply the patch - reporter.Facet.to_ids_value = patched_to_ids_value - - # Patch the HTML reporter to handle missing methods gracefully - try: - original_html_report_specification = reporter.Html.report_specification - - def patched_html_report_specification(self, specification): - try: - return original_html_report_specification(self, specification) - except (AttributeError, UnboundLocalError, NameError) as e: - print(f"HTML Reporter error for specification '{getattr(specification, 'name', 'Unknown')}': {e}") - # Try to collect detailed information from the specification object - spec_name = getattr(specification, 'name', 'Unknown') - requirements = [] - applicability = [] - - # Collect requirement details - if hasattr(specification, 'requirements') and specification.requirements: - for i, req in enumerate(specification.requirements): - req_dict = { - 'facet_type': 'Attribute', - 'metadata': { - 'name': {'simpleValue': 'Unknown'}, - 'value': {'simpleValue': 'Unknown'}, - '@cardinality': getattr(req, 'cardinality', 'required') - }, - 'label': getattr(req, 'to_string', lambda ctx: f'Requirement {i+1}')(), - 'value': 'Unknown', - 'description': f'Requirement {i+1}', - 'status': getattr(req, 'status', 'pass'), - 'passed_entities': [], - 'failed_entities': [], - 'total_applicable': getattr(spec, 'total_applicable', len(getattr(spec, 'applicable_entities', []))), - 'total_applicable_pass': getattr(spec, 'total_pass', len([e for e in getattr(spec, 'applicable_entities', []) if getattr(e, 'status', 'pass') == 'pass'])), - 'total_pass': getattr(spec, 'total_pass', len([e for e in getattr(spec, 'applicable_entities', []) if getattr(e, 'status', 'pass') == 'pass'])), - 'total_fail': getattr(spec, 'total_fail', len(getattr(spec, 'failed_entities', []))), - 'percent_pass': getattr(spec, 'percent_pass', 0), - 'total_failed_entities': 0, - 'total_omitted_failures': 0, - 'has_omitted_failures': False, - 'total_passed_entities': 0, - 'total_omitted_passes': 0, - 'has_omitted_passes': False - } - requirements.append(req_dict) - - # Collect applicability details - if hasattr(specification, 'applicability') and specification.applicability: - for app in specification.applicability: - if hasattr(app, 'to_string'): - applicability.append(app.to_string()) - - # Return a complete specification report structure - return { - 'name': spec_name, - 'status': getattr(specification, 'status', 'pass'), - 'total_pass': getattr(specification, 'total_pass', 0), - 'total_fail': getattr(specification, 'total_fail', 0), - 'total_checks': getattr(specification, 'total_checks', 0), - 'total_checks_pass': getattr(specification, 'total_checks_pass', 0), - 'total_checks_fail': getattr(specification, 'total_checks_fail', 0), - 'total_requirements': len(requirements), - 'total_requirements_pass': len([r for r in requirements if r['status'] == 'pass']), - 'total_requirements_fail': len([r for r in requirements if r['status'] == 'fail']), - 'requirements': requirements, - 'applicability': applicability, - 'description': getattr(specification, 'description', ''), - 'identifier': getattr(specification, 'identifier', ''), - 'instructions': getattr(specification, 'instructions', '') - } - - # Apply the patch - reporter.Html.report_specification = patched_html_report_specification - print("Successfully patched HTML reporter") - except AttributeError as patch_error: - print(f"Could not patch HTML reporter: {patch_error}") - - # Also patch the base Reporter class if it exists - try: - if hasattr(reporter.Reporter, 'report_specification'): - original_base_report_specification = reporter.Reporter.report_specification - - def patched_base_report_specification(self, specification): - try: - return original_base_report_specification(self, specification) - except (AttributeError, UnboundLocalError, NameError) as e: - print(f"Base Reporter error for specification '{getattr(specification, 'name', 'Unknown')}': {e}") - return None - - reporter.Reporter.report_specification = patched_base_report_specification - print("Successfully patched base Reporter") - except Exception as base_patch_error: - print(f"Could not patch base Reporter: {base_patch_error}") - -# Generate HTML report (only if pystache is available and we have validation results) -html_report_path = "report.html" -html_content = None -generate_html = False - -try: - # Try to import pystache first - import pystache - pystache_available = True - generate_html = True - if DEBUG: - print("pystache available, will generate HTML report") -except ImportError: - pystache_available = False - if DEBUG: - print("pystache not available, skipping HTML report generation") - -if generate_html: - html_reporter = reporter.Html(ids) - - if DEBUG: - print(f"About to generate HTML report for {len(ids.specifications)} specifications") - html_patch_applied = False - try: - html_reporter.report() - if DEBUG: - print("HTML reporter.report() completed successfully") - - # Check if the reporter has results - if hasattr(html_reporter, 'results') and DEBUG: - print(f"HTML reporter results: {html_reporter.results}") - - html_reporter.to_file(html_report_path) - with open(html_report_path, "r", encoding="utf-8") as f: - html_content = f.read() - - if DEBUG: - print(f"HTML report generated, length: {len(html_content)}") - if "Spezifikationen erfüllt:" in html_content: - print("HTML contains German specification text - good!") - else: - print("HTML might be empty or not translated properly") - - except Exception as html_error: - if DEBUG: - print(f"HTML report generation failed: {html_error}") - # Only apply patches if HTML generation failed - if not html_patch_applied: - try: - patch_reporters() - html_patch_applied = True - if DEBUG: - print("Applied HTML reporter patches due to error") - # Try again with patches - html_reporter.report() - html_reporter.to_file(html_report_path) - with open(html_report_path, "r", encoding="utf-8") as f: - html_content = f.read() - if DEBUG: - print(f"HTML report generated after patching, length: {len(html_content)}") - except Exception as retry_error: - if DEBUG: - print(f"HTML report generation still failed after patching: {retry_error}") - html_content = "

Report Generation Failed

" - else: - html_content = "

Report Generation Failed

" -else: - # Generate a simple HTML report without pystache - html_content = f""" -IDS Validation Report - -

IDS Validation Report

-

Validation completed successfully for {len(ids.specifications)} specifications.

-

Note: Full HTML report generation skipped due to missing pystache dependency.

-

JSON report is available for detailed results.

- -""" - if DEBUG: - print("Generated simplified HTML report without pystache") - -# Language code passed from JavaScript -language_code = "` + effectiveLanguage + `" -if DEBUG: - print(f"Python: Using language code: {language_code}") - -# Function to translate HTML content based on language -def translate_html(html_content, language_code): - # We'll leave translations to the JavaScript side - # This is a placeholder function as we handle translations in JS - return html_content - - # We'll get translations from JavaScript after we return to the worker - -# Generate JSON report -json_reporter = reporter.Json(ids) - -if DEBUG: - print(f"About to generate JSON report for {len(ids.specifications)} specifications") -try: - json_reporter.report() - print("JSON reporter.report() completed successfully") -except (Exception, NameError) as json_error: - print(f"JSON report generation failed: {json_error}") - # Create a complete JSON structure manually using the IDS object directly - html_results = html_reporter.results if hasattr(html_reporter, 'results') else {} - - # Extract specifications directly from IDS object - specifications = [] - for i, spec in enumerate(ids.specifications): - requirements = [] - - # Collect detailed requirement information - if hasattr(spec, 'requirements') and spec.requirements: - for req in spec.requirements: - req_dict = { - 'type': 'requirement', - 'status': getattr(req, 'status', 'pass'), - 'failures': len(getattr(req, 'failures', [])), - 'cardinality': getattr(req, 'cardinality', 'required'), - 'description': getattr(req, 'to_string', lambda ctx: 'Requirement')() - } - requirements.append(req_dict) - - spec_dict = { - 'name': getattr(spec, 'name', f'Specification {i+1}'), - 'description': getattr(spec, 'description', ''), - 'identifier': getattr(spec, 'identifier', f'spec_{i+1}'), - 'ifcVersion': getattr(spec, 'ifcVersion', []), - 'status': getattr(spec, 'status', 'pass'), - 'total_pass': getattr(spec, 'total_pass', 0), - 'total_fail': getattr(spec, 'total_fail', 0), - 'total_checks': getattr(spec, 'total_checks', 0), - 'total_checks_pass': getattr(spec, 'total_checks_pass', 0), - 'total_checks_fail': getattr(spec, 'total_checks_fail', 0), - 'failed_entities': len(getattr(spec, 'failed_entities', [])), - 'applicable_entities': len(getattr(spec, 'applicable_entities', [])), - 'requirements': requirements, - 'total_requirements': len(requirements), - 'total_requirements_pass': len([r for r in requirements if r['status'] == 'pass']), - 'total_requirements_fail': len([r for r in requirements if r['status'] == 'fail']) - } - - specifications.append(spec_dict) - - json_reporter.results = { - 'title': html_results.get('title', 'Manual JSON Report'), - 'date': html_results.get('date', str(datetime.now())), - 'specifications': specifications, - 'status': html_results.get('status', True), - 'total_specifications': len(ids.specifications), - 'total_specifications_pass': len([s for s in ids.specifications if getattr(s, 'status', 'pass') == 'pass']), - 'total_specifications_fail': len([s for s in ids.specifications if getattr(s, 'status', 'pass') == 'fail']), - 'percent_specifications_pass': 100 if len(ids.specifications) > 0 else 0, - 'total_requirements': sum(len(getattr(s, 'requirements', [])) for s in ids.specifications), - 'total_requirements_pass': 0, # Would need more complex logic to calculate - 'total_requirements_fail': 0, # Would need more complex logic to calculate - 'percent_requirements_pass': 'N/A', - 'total_checks': 0, - 'total_checks_pass': 0, - 'total_checks_fail': 0, - 'percent_checks_pass': 'N/A' - } - print(f"Created manual JSON results with {len(json_reporter.results['specifications'])} specifications from IDS object") - -# Generate BCF report (only if requested) -bcf_b64 = None -if generate_bcf: - bcf_reporter = reporter.Bcf(ids) - - if DEBUG: - print(f"About to generate BCF report for {len(ids.specifications)} specifications") - try: - bcf_reporter.report() - bcf_path = "report.bcf" - bcf_reporter.to_file(bcf_path) - with open(bcf_path, "rb") as f: - bcf_bytes = f.read() - bcf_b64 = base64.b64encode(bcf_bytes).decode('utf-8') - print("BCF report generated successfully") - except (Exception, NameError) as bcf_error: - print(f"BCF report generation failed: {bcf_error}") - # Create a minimal BCF file - bcf_b64 = "UEsFBgAAAAAAAAAAAAAAAAAAAAA=" # Empty ZIP file in base64 -else: - if DEBUG: - print("BCF generation skipped - not requested by user") - -# Create final results object -report_file_name = "` + fileName + `" or "Report_" + datetime.now().strftime("%Y%m%d_%H%M%S") -results = json_reporter.results -results['filename'] = report_file_name -results['title'] = report_file_name -if bcf_b64: - results['bcf_data'] = {"zip_content": bcf_b64, "filename": report_file_name + ".bcf"} -results['html_content'] = html_content -results['language_code'] = language_code - -# Add UI language information to results -results['ui_language'] = "` + effectiveLanguage + `" -results['available_languages'] = ` + JSON.stringify(Object.keys(translations)) + ` - -# Determine validation status -results['validation_status'] = "success" if not any(spec.failed_entities for spec in ids.specifications) else "failed" - -# Memory optimization: Clean up large objects before serialization -del model # Remove the IFC model from memory -del ids # Remove IDS object from memory -import gc -gc.collect() - -# Export the results as JSON -validation_result_json = json.dumps(results, default=str, ensure_ascii=False) - `) - - // Get the JSON string from Python's global namespace - const resultJson = pyodide.globals.get('validation_result_json') - - // Parse the JSON string into a JavaScript object - const results = JSON.parse(resultJson) - - console.log('Worker: Report language information:', { - languageProvided: language, - effectiveLanguage: effectiveLanguage, - resultsLanguageCode: results.language_code, - availableLanguages: results.available_languages, - }) - - // Apply translations to the HTML report - if (results.language_code && results.language_code !== 'en') { - const lang = results.language_code; - - // Load translations if they're not already loaded - if (Object.keys(translations).length === 0) { - translations = await loadTranslations(lang); - } - - if (translations) { - // Apply translations using our simplified function - results.html_content = applyTranslations(results.html_content, translations, lang); - console.log('Worker: HTML report translated to', lang); - } - } - - // Add responsive table styles to prevent overflow - const tableStyles = ` - - ` - - // Insert the styles right after the opening tag - if (results.html_content) { - results.html_content = results.html_content.replace('', '' + tableStyles) - } - - self.postMessage({ - type: 'complete', - results: results, - message: getConsoleMessage('console.success.processingComplete', 'Your files have been processed.'), - }) - } catch (error) { - console.error('Worker error:', error) - - // Check for specific error types - const errorType = detectErrorType(error) - - if (errorType === ERROR_TYPES.OUT_OF_MEMORY) { - self.postMessage({ - type: 'error', - errorType: ERROR_TYPES.OUT_OF_MEMORY, - message: getConsoleMessage( - 'console.error.outOfMemory', - 'Pyodide ran out of memory. The page will reload automatically to free resources.', - ), - stack: error.stack, - }) - } else { - self.postMessage({ - type: 'error', - message: getConsoleMessage('console.error.generic', `An error occurred: ${error.message}`, { - message: error.message, - }), - stack: error.stack, - }) - } - } -} diff --git a/public/pyodideWorkerClean.js b/public/pyodideWorkerClean.js new file mode 100644 index 0000000..ce81eef --- /dev/null +++ b/public/pyodideWorkerClean.js @@ -0,0 +1,397 @@ +/* global importScripts */ +importScripts('https://cdn.jsdelivr.net/pyodide/v0.28.0/full/pyodide.js') + +let pyodide = null +let isProcessing = false + +const WHEEL_PATH = './wasm/ifcopenshell-0.8.4+b1b95ec-cp313-cp313-emscripten_4_0_9_wasm32.whl' + +function normalizeIfc4x3Header(arrayBuffer) { + try { + const total = arrayBuffer.byteLength + const headerLen = Math.min(total, 64 * 1024) + const headerBytes = new Uint8Array(arrayBuffer, 0, headerLen) + const decoder = new TextDecoder('utf-8', { fatal: false }) + const headerStr = decoder.decode(headerBytes) + const schemaRe = /FILE_SCHEMA\s*\(\s*\(\s*'IFC4X3(?:_[A-Z0-9]+)?'\s*\)\s*\)/ + if (!schemaRe.test(headerStr)) return new Uint8Array(arrayBuffer) + const newHeaderStr = headerStr.replace(schemaRe, "FILE_SCHEMA(('IFC4X3_ADD2'))") + if (newHeaderStr === headerStr) return new Uint8Array(arrayBuffer) + const newHeaderBytes = new TextEncoder().encode(newHeaderStr) + const rest = new Uint8Array(arrayBuffer, headerLen) + const out = new Uint8Array(newHeaderBytes.length + rest.length) + out.set(newHeaderBytes, 0) + out.set(rest, newHeaderBytes.length) + return out + } catch (err) { + console.warn('Schema normalization skipped; using original buffer', err) + return new Uint8Array(arrayBuffer) + } +} + +// Simple console message function +function getConsoleMessage(key, defaultMessage) { + return defaultMessage +} + +// Load Pyodide once +async function initializePyodide() { + if (pyodide !== null) { + return pyodide + } + + try { + self.postMessage({ + type: 'progress', + message: getConsoleMessage('console.loading.pyodide', 'Loading Pyodide...'), + }) + + pyodide = await self.loadPyodide({ + indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.28.0/full/', + }) + + self.postMessage({ + type: 'progress', + message: getConsoleMessage('console.loading.pyodideSuccess', 'Pyodide loaded successfully'), + }) + + return pyodide + } catch (error) { + self.postMessage({ + type: 'error', + message: getConsoleMessage('console.error.pyodideLoad', `Failed to load Pyodide: ${error.message}`), + }) + throw error + } +} + +const WORKER_VERSION = '3.1.0-clean-bcf' +console.log('pyodideWorkerClean version:', WORKER_VERSION) + +self.onmessage = async function (e) { + if (isProcessing) { + self.postMessage({ + type: 'error', + errorType: 'busy', + message: getConsoleMessage( + 'console.error.busy', + 'Validation is already running. Please wait for the current run to finish.', + ), + }) + return + } + isProcessing = true + + const { arrayBuffer, idsContent, fileName, language, generateBcf, idsFilename } = e.data + + // Per-run FS isolation + const runId = `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` + const runDir = `/${runId}` + let ifcPath = '' + let idsPath = '' + let bcfPath = '' + + try { + // Load Pyodide + await initializePyodide() + + // Install required packages + self.postMessage({ + type: 'progress', + message: getConsoleMessage('console.loading.packages', 'Installing required packages...'), + }) + + await pyodide.loadPackage(['micropip', 'python-dateutil', 'six', 'numpy', 'setuptools', 'typing-extensions', 'sqlite3', 'shapely']) + + await pyodide.runPythonAsync(` +import micropip + +def _allow(_): + return None + +try: + import micropip._utils as _utils + _utils.check_compatible = _allow +except Exception: + pass + +try: + from micropip._micropip import WheelInfo +except ModuleNotFoundError: + WheelInfo = None + +if "WheelInfo" in globals() and WheelInfo is not None: + WheelInfo.check_compatible = lambda self: None + `) + + // Bypass the Emscripten version compatibility check for wheels + self.postMessage({ + type: 'progress', + message: getConsoleMessage('console.loading.micropipPatch', 'Patching micropip for compatibility...'), + }) + + // Install IfcOpenShell and IfcTester + self.postMessage({ + type: 'progress', + message: getConsoleMessage('console.loading.ifcOpenShell', 'Installing IfcOpenShell...'), + }) + + await pyodide.runPythonAsync(` +import micropip + +WHEEL_URL = ${JSON.stringify(new URL(WHEEL_PATH, self.location).toString())} + +await micropip.install(WHEEL_URL, deps=False, keep_going=False) + +import ifcopenshell +print('IfcOpenShell version:', getattr(ifcopenshell, '__version__', 'unknown')) + `) + + self.postMessage({ + type: 'progress', + message: getConsoleMessage('console.loading.dependencies', 'Installing additional dependencies...'), + }) + + await pyodide.runPythonAsync(` +import micropip +await micropip.install(['lark', 'ifctester==0.8.1', 'bcf-client==0.8.1', 'pystache'], keep_going=True) + `) + + // Write files to Pyodide filesystem + self.postMessage({ + type: 'progress', + message: getConsoleMessage('console.loading.inputFiles', 'Processing input files...'), + }) + + // Create per-run directory and write inputs + try { pyodide.FS.mkdir(runDir) } catch (_) {} + ifcPath = `${runDir}/input.ifc` + idsPath = `${runDir}/input.ids` + bcfPath = `${runDir}/report.bcf` + const normalizedIfc = normalizeIfc4x3Header(arrayBuffer) + pyodide.FS.writeFile(ifcPath, normalizedIfc) + if (idsContent) pyodide.FS.writeFile(idsPath, idsContent) + + // Run validation using native IfcTester + self.postMessage({ + type: 'progress', + message: getConsoleMessage('console.loading.validation', 'Running IFC validation...'), + }) + + const wantBcf = !!generateBcf + + const now = new Date() + const yyyy = now.getFullYear() + const mm = String(now.getMonth() + 1).padStart(2, '0') + const dd = String(now.getDate()).padStart(2, '0') + const yy = String(yyyy).slice(-2) + const ifcBaseName = (fileName || 'report.ifc').toString().split(/[/\\]/).pop() + const idsBaseName = (idsFilename || 'ids').toString().split(/[/\\]/).pop() + const combinedName = `${yy}${mm}${dd}-${ifcBaseName}-${idsBaseName}` + const sanitizedReportBaseName = (combinedName || 'report') + .replace(/[<>:"/\\|?*]/g, '_') + .split('') + .map((char) => (char.charCodeAt(0) < 32 ? '_' : char)) + .join('') + .replace(/\s+/g, ' ') + .trim() + .replace(/\.+$/, '') || 'report' + const safeReportBaseName = JSON.stringify(sanitizedReportBaseName) + + const pythonIfcPath = JSON.stringify(ifcPath) + const pythonIdsPath = JSON.stringify(idsContent ? idsPath : '') + const pythonBcfPath = JSON.stringify(bcfPath) + const pythonFileName = JSON.stringify(fileName ?? '') + const pythonLanguage = JSON.stringify(language ?? '') + + const validationResult = await pyodide.runPythonAsync(` +import json +import os +import base64 +import re +import ifcopenshell +from ifctester import ids, reporter +from datetime import datetime + +# Flag for optional BCF generation +generate_bcf = ${wantBcf ? 'True' : 'False'} +report_basename = ${safeReportBaseName} +IFC_PATH = ${pythonIfcPath} +IDS_PATH = ${pythonIdsPath} +BCF_PATH = ${pythonBcfPath} + +# Open IFC file +print(f"Opening IFC file: {IFC_PATH}") +ifc = ifcopenshell.open(IFC_PATH) + +# Initialize basic results structure +results = { + "title": "IDS Validation Report", + "name": "IDS Validation Report", + "filename": ${pythonFileName}, + "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "language_code": ${pythonLanguage}, + "status": True, + "validation_status": "success" +} + +# Track IDS specification for optional reporting +ids_spec = None + +# Check if IDS file exists and validate +if IDS_PATH and os.path.exists(IDS_PATH): + print(f"Loading IDS specification: {IDS_PATH}") + ids_spec = ids.open(IDS_PATH) + + if ids_spec and ids_spec.specifications: + print("Validating IFC against IDS...") + ids_spec.validate(ifc) + + # Use IfcTester's native JSON reporter and transform to our frontend structure + try: + json_reporter = reporter.Json(ids_spec) + native_results = json_reporter.report() + + # Transform IfcTester's structure to match our frontend expectations + results.update(native_results) + results["filename"] = ${pythonFileName} + results["language_code"] = ${pythonLanguage} + results["validation_status"] = "success" if results.get("status", False) else "failed" + + # Transform specifications to add missing fields our frontend expects + for spec in results.get("specifications", []): + # Add fields that our frontend expects but IfcTester doesn't provide + if "total_applicable" not in spec: + spec["total_applicable"] = 0 + if "total_applicable_pass" not in spec: + spec["total_applicable_pass"] = 0 + if "total_applicable_fail" not in spec: + spec["total_applicable_fail"] = 0 + + # Transform requirements to add missing fields + for req in spec.get("requirements", []): + # Map IfcTester's structure to our frontend structure + if "total_checks" not in req: + req["total_checks"] = req.get("total_applicable", 0) + if "cardinality" not in req: + req["cardinality"] = "required" + if "has_omitted_passes" not in req: + req["has_omitted_passes"] = False + if "has_omitted_failures" not in req: + req["has_omitted_failures"] = False + + # Transform entity data to match our frontend expectations + for entity_list in ["passed_entities", "failed_entities"]: + if entity_list in req: + transformed_entities = [] + for entity in req[entity_list]: + transformed_entity = { + "class": entity.get("class", "Unknown"), + "predefined_type": entity.get("predefined_type") or "None", + "name": entity.get("name") or "None", + "description": entity.get("description") or "None", + "global_id": entity.get("global_id") or "None", + "tag": entity.get("tag") or "None", + "reason": entity.get("reason", "") + } + transformed_entities.append(transformed_entity) + req[entity_list] = transformed_entities + + print("Successfully transformed IfcTester JSON results to frontend format") + + except Exception as e: + print(f"IfcTester JSON reporter error: {e}") + results["specifications"] = [] + results["status"] = False + results["validation_status"] = "failed" +else: + print("No IDS file provided") + results["specifications"] = [] + +# Generate BCF report if requested and a valid IDS specification is available +if generate_bcf and ids_spec and getattr(ids_spec, 'specifications', None): + try: + bcf_reporter = reporter.Bcf(ids_spec) + bcf_reporter.report() + bcf_path = BCF_PATH + bcf_reporter.to_file(bcf_path) + with open(bcf_path, 'rb') as bcf_file: + bcf_bytes = bcf_file.read() + bcf_b64 = base64.b64encode(bcf_bytes).decode('utf-8') + results['bcf_data'] = { + 'zip_content': bcf_b64, + 'filename': f"{report_basename}.bcf" + } + print('BCF report generated successfully') + except Exception as bcf_error: + results['bcf_error'] = f"BCF generation failed: {bcf_error}" + +# Calculate top-level totals for applicable entities +results["total_applicable"] = sum(spec.get("total_applicable", 0) for spec in results.get("specifications", [])) +results["total_applicable_pass"] = sum(spec.get("total_applicable_pass", 0) for spec in results.get("specifications", [])) +results["total_applicable_fail"] = sum(spec.get("total_applicable_fail", 0) for spec in results.get("specifications", [])) + +# Return JSON string - let frontend handle all translations and enhancements +json.dumps(results, default=str, ensure_ascii=False) + `) + + // Parse the JSON result + const results = JSON.parse(validationResult) + + // Check for errors + if (results.error) { + throw new Error(results.error) + } + + // Add metadata for frontend + results.ui_language = language + results.available_languages = ['en', 'de', 'fr', 'it', 'rm'] + results.filename = fileName + results.validation_status = results.status ? 'success' : 'failed' + + // Attach idsFilename for tab title building (if provided) + if (idsFilename) { + results.ids_filename = idsFilename + } + + // Send results back to frontend + self.postMessage({ + type: 'complete', + results: results, + message: getConsoleMessage('console.success.processingComplete', 'Your files have been processed.'), + }) + + } catch (error) { + console.error('Worker error:', error) + + // Check for out of memory error + const errorStr = error.toString().toLowerCase() + if (errorStr.includes('out of memory') || errorStr.includes('internalerror: out of memory')) { + self.postMessage({ + type: 'error', + errorType: 'out_of_memory', + message: getConsoleMessage( + 'console.error.outOfMemory', + 'Pyodide ran out of memory. The page will reload automatically to free resources.', + ), + stack: error.stack, + }) + } else { + self.postMessage({ + type: 'error', + message: getConsoleMessage('console.error.generic', `An error occurred: ${error.message}`), + stack: error.stack, + }) + } + } finally { + // Cleanup per-run files and release lock + try { + const exists = (p) => { try { return p && pyodide.FS.analyzePath(p).exists } catch { return false } } + if (exists(ifcPath)) pyodide.FS.unlink(ifcPath) + if (exists(idsPath)) pyodide.FS.unlink(idsPath) + if (exists(bcfPath)) pyodide.FS.unlink(bcfPath) + try { pyodide.FS.rmdir(runDir) } catch {} + } catch {} + isProcessing = false + } +} diff --git a/public/python/model_checker.py b/public/python/model_checker.py deleted file mode 100644 index 96ea454..0000000 --- a/public/python/model_checker.py +++ /dev/null @@ -1,251 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import subprocess -import pkg_resources -import os -import re -import argparse - -# Dictionary of translations (we'll keep a small set here for demo purposes) -TRANSLATIONS = { - "en": { - "summary": "Summary", - "specifications": "Specifications", - "requirements": "Requirements", - "details": "Details", - "class": "Class", - "predefinedType": "PredefinedType", - "name": "Name", - "description": "Description", - "warning": "Warning", - "globalId": "GlobalId", - "tag": "Tag", - "status": { - "pass": "PASS", - "fail": "FAIL", - "untested": "UNTESTED", - "skipped": "SKIPPED" - } - }, - "de": { - "summary": "Zusammenfassung", - "specifications": "Spezifikationen", - "requirements": "Anforderungen", - "details": "Details", - "class": "Klasse", - "predefinedType": "Vordefinierter Typ", - "name": "Name", - "description": "Beschreibung", - "warning": "Warnung", - "globalId": "Globale ID", - "tag": "Kennzeichnung", - "status": { - "pass": "BESTANDEN", - "fail": "FEHLGESCHLAGEN", - "untested": "UNGETESTET", - "skipped": "ÜBERSPRUNGEN" - } - } -} - -def check_dependencies(): - """Check and install required dependencies.""" - required = {'ifcopenshell', 'ifctester'} - installed = {pkg.key for pkg in pkg_resources.working_set} - missing = required - installed - - if missing: - print(f"Installing missing dependencies: {', '.join(missing)}") - try: - subprocess.check_call([sys.executable, '-m', 'pip', 'install', *missing]) - print("Dependencies installed successfully!") - except subprocess.CalledProcessError as e: - print(f"Error installing dependencies: {str(e)}") - sys.exit(1) - -# Check and install dependencies before imports -check_dependencies() - -import ifcopenshell -from ifctester import ids, reporter - -def translate_html_report(html_content, lang): - """ - Translate key phrases in the HTML report based on the language. - - Args: - html_content: HTML content to translate - lang: Language code (e.g., 'en', 'de') - - Returns: - Translated HTML content - """ - if lang == "en" or lang not in TRANSLATIONS: - print(f"No translation needed for language: {lang}") - return html_content - - print(f"Translating HTML report to {lang}") - translations = TRANSLATIONS[lang] - - # Simple translation of key phrases - translatable_terms = [ - ("Summary", "summary"), - ("Specifications", "specifications"), - ("Requirements", "requirements"), - ("Details", "details"), - ("Class", "class"), - ("PredefinedType", "predefinedType"), - ("Name", "name"), - ("Description", "description"), - ("Warning", "warning"), - ("GlobalId", "globalId"), - ("Tag", "tag"), - ("PASS", "status.pass"), - ("FAIL", "status.fail"), - ("UNTESTED", "status.untested"), - ("SKIPPED", "status.skipped") - ] - - # Apply translations - for english_term, field_path in translatable_terms: - # Handle nested fields like status.pass - field_parts = field_path.split('.') - if len(field_parts) > 1: - translation = translations[field_parts[0]][field_parts[1]] - else: - translation = translations[field_path] - - # Use regex with word boundaries to avoid partial replacements - html_content = re.sub(r'\b' + english_term + r'\b', translation, html_content) - - print(f"HTML report translation to {lang} completed") - return html_content - -def test_ifc(ifc_path: str, ids_path: str, report_path: str = "report.html", lang: str = "en"): - """ - Test an IFC file against an IDS specification and generate reports in multiple formats. - - Args: - ifc_path: Path to the IFC file - ids_path: Path to the IDS specification file - report_path: Path where to save the HTML report (default: report.html) - lang: Language for the report (default: en) - Note: Language selection is implemented via post-processing - - Returns: - Dictionary with validation status and paths to generated reports - """ - print(f"Using language: {lang}") - - # Normalize language code (e.g., 'DE' -> 'de', 'de-de' -> 'de') - lang = lang.lower().split('-')[0] - - # Check if language is supported - if lang not in TRANSLATIONS: - print(f"Warning: Language '{lang}' not supported, falling back to English") - lang = "en" - else: - print(f"Language '{lang}' is supported") - - try: - # Validate paths - if not os.path.exists(ifc_path): - raise FileNotFoundError(f"IFC file not found: {ifc_path}") - if not os.path.exists(ids_path): - raise FileNotFoundError(f"IDS file not found: {ids_path}") - - print(f"Opening IFC file: {ifc_path}") - ifc_model = ifcopenshell.open(ifc_path) - - print(f"Loading IDS specification: {ids_path}") - ids_spec = ids.open(ids_path) - - if not ids_spec or not ids_spec.specifications: - raise ValueError("Invalid or empty IDS specification") - - print("Validating IFC against IDS...") - ids_spec.validate(ifc_model) - - # Generate results in multiple formats - results = {} - - # HTML Report - print("Generating HTML report...") - html_reporter = reporter.Html(ids_spec) - html_reporter.report() - html_reporter.to_file(report_path) - - # Apply translations to HTML - if lang != "en" and lang in TRANSLATIONS: - print(f"Translating report to {lang}...") - with open(report_path, "r", encoding="utf-8") as f: - html_content = f.read() - - translated_html = translate_html_report(html_content, lang) - - with open(report_path, "w", encoding="utf-8") as f: - f.write(translated_html) - - results["html_path"] = os.path.abspath(report_path) - - # JSON Report - json_path = report_path.replace('.html', '.json') - print("Generating JSON report...") - json_reporter = reporter.Json(ids_spec) - json_reporter.report() - json_reporter.write(json_path) - results["json_path"] = os.path.abspath(json_path) - - # BCF Report - bcf_path = report_path.replace('.html', '.bcf') - print("Generating BCF report...") - bcf_reporter = reporter.Bcf(ids_spec) - bcf_reporter.report() - bcf_reporter.to_file(bcf_path) - results["bcf_path"] = os.path.abspath(bcf_path) - - # Return validation status and report paths - validation_status = "success" if not any(spec.failed_entities for spec in ids_spec.specifications) else "failed" - return { - "status": validation_status, - "reports": results - } - except Exception as e: - print(f"Error during IFC testing: {str(e)}") - raise - -def main(): - # Set up argument parser - parser = argparse.ArgumentParser(description="IFC Model Checker - Validate IFC files against IDS specifications") - parser.add_argument("--ifc", "-i", help="Path to the IFC file") - parser.add_argument("--ids", "-s", help="Path to the IDS specification file") - parser.add_argument("--report", "-r", default="report.html", help="Path for the HTML report (default: report.html)") - parser.add_argument("--lang", "-l", default="en", choices=list(TRANSLATIONS.keys()), - help="Language for the report (default: en)") - parser.add_argument("--interactive", "-int", action="store_true", help="Run in interactive mode") - args = parser.parse_args() - - # If interactive mode or no required args, prompt for input - if args.interactive or not (args.ifc and args.ids): - if not args.ifc: - args.ifc = input("Enter path to IFC file: ") - if not args.ids: - args.ids = input("Enter path to IDS file: ") - if not args.report: - args.report = input("Enter path for report (default: report.html): ") or "report.html" - if not args.lang: - args.lang = input(f"Enter language code (default: en, available: {', '.join(TRANSLATIONS.keys())}): ") or "en" - - # Run the validation - try: - result = test_ifc(args.ifc, args.ids, args.report, args.lang) - print(f"\nValidation {result['status']}!") - print(f"Generated reports:") - for report_type, path in result['reports'].items(): - print(f"- {report_type}: {path}") - except Exception as e: - print(f"Error: {str(e)}") - sys.exit(1) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/public/report-viewer.html b/public/report-viewer.html new file mode 100644 index 0000000..7be0dcf --- /dev/null +++ b/public/report-viewer.html @@ -0,0 +1,44 @@ + + + + + + Report Viewer + + + +
Loading report...
+ + + + + diff --git a/public/report.html b/public/report.html index a6cc7d7..9ba8270 100644 --- a/public/report.html +++ b/public/report.html @@ -60,10 +60,14 @@ font-style: italic; } span.item { - padding: 5px; + padding: 4px 10px; border-radius: 5px; - margin-right: 5px; + margin-right: 8px; border: 1px solid #eee; + display: inline-flex; + align-items: center; + gap: 6px; + line-height: 1.2; } span.item.pass, span.item.fail { @@ -138,11 +142,13 @@ border-top: 1px solid #ccc; } summary { - display: flex; + display: list-item; cursor: pointer; + padding: 10px; + font-weight: bold; } - summary::-webkit-details-marker { - display: none; + details[open] summary { + margin-bottom: 10px; } * { box-sizing: border-box; @@ -251,6 +257,28 @@ .percent[data-width='100'] { width: 100%; } + .summary-stats { + display: flex; + flex-wrap: wrap; + gap: 8px 12px; + margin: 12px 0; + align-items: center; + } + @media (max-width: 640px) { + .summary-stats { + flex-direction: column; + align-items: flex-start; + } + .summary-stats .item { + width: 100%; + justify-content: space-between; + } + .summary-stats .item.pass, + .summary-stats .item.fail { + width: auto; + justify-content: center; + } + } @@ -266,20 +294,20 @@

{{t.summary}}

class="{{^total_checks}}skipped{{/total_checks}}{{#total_checks}}{{#status}}pass{{/status}}{{^status}}fail{{/status}}{{/total_checks}} percent" data-width="{{^total_checks}}100{{/total_checks}}{{#total_checks}}{{percent_checks_pass}}{{/total_checks}}" > - {{^total_checks}}{{t.skipped}}{{/total_checks}}{{#total_checks}}{{percent_checks_pass}}%{{/total_checks}} + {{^total_checks}}{{t.status.skipped}}{{/total_checks}}{{#total_checks}}{{percent_checks_pass}}%{{/total_checks}} -

+

{{status_text}} - Specifications passed: {{total_specifications_pass}} / + {{t.specificationsPassedPrefix}}: {{total_specifications_pass}} / {{total_specifications}} - Requirements passed: {{total_requirements_pass}} / {{total_requirements}} + {{t.requirementsPassedPrefix}}: {{total_requirements_pass}} / {{total_requirements}} - Checks passed: {{total_checks_pass}} / {{total_checks}} + {{t.checksPassedPrefix}}: {{total_checks_pass}} / {{total_checks}}


@@ -298,31 +326,26 @@

{{name}}

class="{{^total_checks}}skipped{{/total_checks}}{{#total_checks}}{{#status}}pass{{/status}}{{^status}}fail{{/status}}{{/total_checks}} percent" data-width="{{^total_checks}}100{{/total_checks}}{{#total_checks}}{{percent_checks_pass}}{{/total_checks}}" > - {{^total_checks}}Skipped{{/total_checks}}{{#total_checks}}{{percent_checks_pass}}%{{/total_checks}} + {{^total_checks}}{{t.status.skipped}}{{/total_checks}}{{#total_checks}}{{percent_checks_pass}}%{{/total_checks}} -

+

{{^total_checks}}{{t.status.skipped}}{{/total_checks}} {{#total_checks}}{{status_text}}{{/total_checks}} - Checks passed: {{total_checks_pass}} / {{total_checks}} + {{t.checksPassedPrefix}}: {{total_checks_pass}} / {{total_checks}} - Elements passed: {{total_applicable_pass}} / {{total_applicable}} + {{t.elementsPassedPrefix}}: {{total_applicable_pass}} / {{total_applicable}}

- {{^is_ifc_version}} -

- Warning: specification does not apply to this IFC version -

- {{/is_ifc_version}}

- Applicability + {{t.applicability}}

- Requirements + {{t.requirements}}

    {{#requirements}}
  1. + {{#total_checks}}
    {{description}} - {{#total_ckecks}} - - -
    - {{/total_ckecks}} {{#total_pass}} + {{#total_pass}} @@ -384,9 +404,8 @@

    {{name}}

    - - + @@ -396,23 +415,26 @@

    {{name}}

    - - + {{#extra_of_type}} - + {{/extra_of_type}} {{/failed_entities}} {{#has_omitted_failures}} - + {{/has_omitted_failures}}
    {{t.predefinedType}} {{t.name}} {{t.description}}{{t.warning}} {{t.globalId}}{{t.tag}}{{t.failureReason}}
    {{predefined_type}} {{name}} {{description}}{{reason}} {{global_id}}{{tag}}{{reason}}
    {{t.moreOfSameType}}{{t.moreOfSameType}}
    {{t.moreElementsNotShown}}{{t.moreElementsNotShown}}
    {{/total_fail}}
    + {{/total_checks}} + {{^total_checks}} + {{description}} + {{/total_checks}}
  2. {{/requirements}}
@@ -422,8 +444,11 @@

{{name}}


diff --git a/public/wasm/ifcopenshell-0.8.4+b1b95ec-cp313-cp313-emscripten_4_0_9_wasm32.whl b/public/wasm/ifcopenshell-0.8.4+b1b95ec-cp313-cp313-emscripten_4_0_9_wasm32.whl new file mode 100644 index 0000000..2b83173 Binary files /dev/null and b/public/wasm/ifcopenshell-0.8.4+b1b95ec-cp313-cp313-emscripten_4_0_9_wasm32.whl differ diff --git a/src/components/Footer/Footer.tsx b/src/components/Footer/Footer.tsx index f106e17..0dc2122 100644 --- a/src/components/Footer/Footer.tsx +++ b/src/components/Footer/Footer.tsx @@ -14,20 +14,27 @@ export const Footer = () => { {t('sponsored-by')} - Righetti and Partner + + Righetti and Partner + {t('open-source')} diff --git a/src/components/IDSReport/HTMLTemplateRenderer.tsx b/src/components/IDSReport/HTMLTemplateRenderer.tsx new file mode 100644 index 0000000..9d0052c --- /dev/null +++ b/src/components/IDSReport/HTMLTemplateRenderer.tsx @@ -0,0 +1,432 @@ +import React, { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { IDSTranslationService } from '../../services/IDSTranslationService' +import { ValidationResult } from '../../types/validation' + +interface HTMLTemplateRendererProps { + validationResults: ValidationResult + onReportGenerated: (htmlContent: string) => void +} + +interface TemplateData { + [key: string]: unknown + t: { + [key: string]: string | { [key: string]: string } + } +} + +/** + * Component that generates HTML reports using the exact same template structure + * as the current system, but with proper translation support + */ +export const HTMLTemplateRenderer: React.FC = ({ validationResults, onReportGenerated }) => { + const { t, i18n } = useTranslation() + const [templateContent, setTemplateContent] = useState(null) + + // Load the HTML template + useEffect(() => { + fetch('/report.html') + .then((response) => response.text()) + .then((content) => { + setTemplateContent(content) + }) + .catch((error) => console.error('Error loading template:', error)) + }, []) + + const generateReport = useCallback(() => { + if (!templateContent) return + + // Create translation service + const translationService = new IDSTranslationService() + + // Translate the validation results + const translatedResults = translationService.translateValidationResults(validationResults, i18n.language) + + // Prepare template data with translations + const templateData: TemplateData = { + ...translatedResults, + + // Add translation object for template use + t: { + summary: t('report.summary', 'Summary'), + specifications: t('report.specifications', 'Specifications'), + requirements: t('report.requirements', 'Requirements'), + details: t('report.details', 'Details'), + class: t('report.class', 'Class'), + predefinedType: t('report.predefinedType', 'PredefinedType'), + name: t('report.name', 'Name'), + description: t('report.description', 'Description'), + warning: t('report.warning', 'Warning'), + globalId: t('report.globalId', 'GlobalId'), + tag: t('report.tag', 'Tag'), + reportBy: t('report.reportBy', 'Report by'), + and: t('report.and', 'and'), + status: { + pass: t('report.status.pass', 'PASS'), + fail: t('report.status.fail', 'FAIL'), + untested: t('report.status.untested', 'UNTESTED'), + skipped: t('report.status.skipped', 'SKIPPED'), + }, + skipped: t('report.status.skipped', 'SKIPPED'), + checksPassedPrefix: t('report.interface.checksPassedPrefix', 'Checks passed'), + elementsPassedPrefix: t('report.interface.elementsPassedPrefix', 'Elements passed'), + failureReason: t('report.failureReason', 'Failure Reason'), + specNotApplyToVersion: t('report.specNotApplyToVersion', 'specification does not apply to this IFC version'), + moreOfSameType: t( + 'report.phrases.moreOfSameType', + '... {{count}} more of the same element type ({{type}} with Tag {{tag}} and GlobalId {{id}}) not shown ...', + ), + moreElementsNotShown: t( + 'report.phrases.moreElementsNotShown', + '... {{count}} more {{type}} elements not shown out of {{total}} total ...', + ), + specificationsPassedPrefix: t('report.interface.specificationsPassedPrefix', 'Specifications passed'), + requirementsPassedPrefix: t('report.interface.requirementsPassedPrefix', 'Requirements passed'), + applicability: t('report.interface.applicability', 'Applicability'), + }, + } + + // Use Mustache-like template replacement (simplified version) + let htmlContent = templateContent + + // Replace template variables + htmlContent = replaceMustacheVariables(htmlContent, templateData) + + // Apply additional translations for dynamic content + htmlContent = applyDynamicTranslations(htmlContent) + + // Callback with the generated HTML + onReportGenerated(htmlContent) + }, [templateContent, validationResults, t, onReportGenerated]) + + // Generate the report when template and results are ready + useEffect(() => { + if (templateContent && validationResults) { + generateReport() + } + }, [templateContent, validationResults, i18n.language, generateReport]) + + /** + * Replace Mustache-style template variables with actual values + */ + const replaceMustacheVariables = useCallback((template: string, data: TemplateData): string => { + let result = template + + // Simple variable replacement + const simpleVars = [ + 'title', + 'filename', + 'date', + '_lang', + 'total_specifications', + 'total_specifications_pass', + 'total_specifications_fail', + 'total_requirements', + 'total_requirements_pass', + 'total_requirements_fail', + 'total_checks', + 'total_checks_pass', + 'total_checks_fail', + 'percent_checks_pass', + 'status_text', + ] + + simpleVars.forEach((varName) => { + const regex = new RegExp(`\\{\\{${varName}\\}\\}`, 'g') + const value = data[varName] + const safe = typeof value === 'string' ? escapeHtml(value) : value + result = result.replace(regex, () => String(safe ?? '')) + }) + + // Translation variables + const translationVars = [ + 't.summary', + 't.specifications', + 't.requirements', + 't.details', + 't.class', + 't.predefinedType', + 't.name', + 't.description', + 't.warning', + 't.globalId', + 't.tag', + 't.reportBy', + 't.and', + 't.status.pass', + 't.status.fail', + 't.status.untested', + 't.status.skipped', + 't.skipped', + 't.checksPassedPrefix', + 't.elementsPassedPrefix', + 't.failureReason', + 't.specNotApplyToVersion', + 't.moreOfSameType', + 't.moreElementsNotShown', + 't.specificationsPassedPrefix', + 't.requirementsPassedPrefix', + 't.applicability', + ] + + translationVars.forEach((varName) => { + const regex = new RegExp(`\\{\\{${varName}\\}\\}`, 'g') + const value = getNestedValue(data, varName) + // Translations are controlled content - don't escape them + result = result.replace(regex, () => String(value ?? '')) + }) + + // Handle conditional sections and loops + result = processConditionalSections(result, data) + result = processSpecificationLoops(result, data) + + return result + }, []) + + /** + * Get nested object value by dot notation + */ + const getNestedValue = (obj: TemplateData, path: string): string | undefined => { + return path.split('.').reduce((current: unknown, key: string) => { + if (current && typeof current === 'object' && key in current) { + return (current as Record)[key] + } + return undefined + }, obj) as string | undefined + } + + /** + * Process conditional sections in the template + */ + const processConditionalSections = (template: string, data: TemplateData): string => { + let result = template + + // Handle status conditionals + if (data.status) { + result = result.replace(/\{\{#status\}\}pass\{\{\/status\}\}/g, 'pass') + result = result.replace(/\{\{\^status\}\}fail\{\{\/status\}\}/g, '') + } else { + result = result.replace(/\{\{#status\}\}pass\{\{\/status\}\}/g, '') + result = result.replace(/\{\{\^status\}\}fail\{\{\/status\}\}/g, 'fail') + } + + // Handle total_checks conditionals + if (Number(data.total_checks) > 0) { + result = result.replace(/\{\{\^total_checks\}\}.*?\{\{\/total_checks\}\}/g, '') + result = result.replace(/\{\{#total_checks\}\}/g, '') + result = result.replace(/\{\{\/total_checks\}\}/g, '') + } else { + result = result.replace(/\{\{#total_checks\}\}.*?\{\{\/total_checks\}\}/g, '') + result = result.replace(/\{\{\^total_checks\}\}/g, '') + result = result.replace(/\{\{\/total_checks\}\}/g, '') + } + + return result + } + + /** + * Process specification loops in the template + */ + const processSpecificationLoops = useCallback((template: string, data: TemplateData): string => { + let result = template + + // Find and replace specification loop + const specLoopRegex = /\{\{#specifications\}\}([\s\S]*?)\{\{\/specifications\}\}/g + const specTemplate = template.match(specLoopRegex)?.[0] + + if (specTemplate && data.specifications && Array.isArray(data.specifications)) { + const specContent = specTemplate.replace(/\{\{#specifications\}\}/, '').replace(/\{\{\/specifications\}\}/, '') + + const specHtml = data.specifications + .map((spec: Record) => { + let specSection = specContent + + // Replace specification variables + const specVars = [ + 'name', + 'description', + 'instructions', + 'status_text', + 'total_checks', + 'total_checks_pass', + 'total_checks_fail', + 'percent_checks_pass', + ] + + specVars.forEach((varName) => { + const regex = new RegExp(`\\{\\{${varName}\\}\\}`, 'g') + const raw = spec[varName] + const safe = typeof raw === 'string' ? escapeHtml(String(raw)) : raw + specSection = specSection.replace(regex, () => String(safe ?? '')) + }) + + // Handle applicability loop + if (spec.applicability && Array.isArray(spec.applicability) && spec.applicability.length > 0) { + const appLoopRegex = /\{\{#applicability\}\}([\s\S]*?)\{\{\/applicability\}\}/g + const appTemplate = specSection.match(appLoopRegex)?.[0] + + if (appTemplate) { + const appContent = appTemplate + .replace(/\{\{#applicability\}\}/, '') + .replace(/\{\{\/applicability\}\}/, '') + const appHtml = spec.applicability + .map((app: string) => appContent.replace(/\{\{\.\}\}/g, () => escapeHtml(String(app)))) + .join('') + + specSection = specSection.replace(appLoopRegex, appHtml) + } + } + + // Handle requirements loop + if (spec.requirements && Array.isArray(spec.requirements) && spec.requirements.length > 0) { + const reqLoopRegex = /\{\{#requirements\}\}([\s\S]*?)\{\{\/requirements\}\}/g + const reqTemplate = specSection.match(reqLoopRegex)?.[0] + + if (reqTemplate) { + const reqContent = reqTemplate.replace(/\{\{#requirements\}\}/, '').replace(/\{\{\/requirements\}\}/, '') + const reqHtml = spec.requirements + .map((req: Record) => { + let reqSection = reqContent + + // Replace requirement variables + const reqVars = ['description', 'total_checks', 'total_pass', 'total_fail'] + reqVars.forEach((varName) => { + const regex = new RegExp(`\\{\\{${varName}\\}\\}`, 'g') + const raw = req[varName] + const safe = typeof raw === 'string' ? escapeHtml(String(raw)) : raw + reqSection = reqSection.replace(regex, () => String(safe ?? '')) + }) + + // Handle entity tables (passed and failed) + reqSection = processEntityTables(reqSection, req) + + return reqSection + }) + .join('') + + specSection = specSection.replace(reqLoopRegex, reqHtml) + } + } + + // Handle specification status conditionals + if (spec.status) { + specSection = specSection.replace(/\{\{#status\}\}pass\{\{\/status\}\}/g, 'pass') + specSection = specSection.replace(/\{\{\^status\}\}fail\{\{\/status\}\}/g, '') + } else { + specSection = specSection.replace(/\{\{#status\}\}pass\{\{\/status\}\}/g, '') + specSection = specSection.replace(/\{\{\^status\}\}fail\{\{\/status\}\}/g, 'fail') + } + + return specSection + }) + .join('') + + result = result.replace(specLoopRegex, specHtml) + } + + return result + }, []) + + const normalizeEntityForTable = (entity: Record) => ({ + type: entity.type ?? entity.class ?? '', + predefinedType: entity.predefinedType ?? entity.predefined_type ?? '', + name: entity.name ?? '', + description: entity.description ?? '', + globalId: entity.globalId ?? entity.global_id ?? '', + tag: entity.tag ?? '', + reason: entity.reason ?? '', + }) + + const escapeHtml = (value: string): string => { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + } + + /** + * Process entity tables for passed and failed entities + */ + const processEntityTables = (template: string, req: Record): string => { + let result = template + + // Process passed entities table + if (req.passed_entities && Array.isArray(req.passed_entities) && req.passed_entities.length > 0) { + const passedTableRegex = /\{\{#total_pass\}\}([\s\S]*?)\{\{\/total_pass\}\}/g + const passedTemplate = result.match(passedTableRegex)?.[0] + + if (passedTemplate) { + const tableContent = passedTemplate.replace(/\{\{#total_pass\}\}/, '').replace(/\{\{\/total_pass\}\}/, '') + const entityRows = req.passed_entities + .map((entity: Record) => { + const normalized = normalizeEntityForTable(entity) + return ` + ${escapeHtml(String(normalized.type))} + ${escapeHtml(String(normalized.predefinedType))} + ${escapeHtml(String(normalized.name))} + ${escapeHtml(String(normalized.description))} + ${escapeHtml(String(normalized.globalId))} + ${escapeHtml(String(normalized.tag))} + ` + }) + .join('') + + const tableHtml = tableContent.replace(/[\s\S]*?<\/tbody>/, `${entityRows}`) + + result = result.replace(passedTableRegex, tableHtml) + } + } else { + result = result.replace(/\{\{#total_pass\}\}[\s\S]*?\{\{\/total_pass\}\}/g, '') + } + + // Process failed entities table + if (req.failed_entities && Array.isArray(req.failed_entities) && req.failed_entities.length > 0) { + const failedTableRegex = /\{\{#total_fail\}\}([\s\S]*?)\{\{\/total_fail\}\}/g + const failedTemplate = result.match(failedTableRegex)?.[0] + + if (failedTemplate) { + const tableContent = failedTemplate.replace(/\{\{#total_fail\}\}/, '').replace(/\{\{\/total_fail\}\}/, '') + const entityRows = req.failed_entities + .map((entity: Record) => { + const normalized = normalizeEntityForTable(entity) + return ` + ${escapeHtml(String(normalized.type))} + ${escapeHtml(String(normalized.predefinedType))} + ${escapeHtml(String(normalized.name))} + ${escapeHtml(String(normalized.description))} + ${escapeHtml(String(normalized.reason))} + ${escapeHtml(String(normalized.globalId))} + ${escapeHtml(String(normalized.tag))} + ` + }) + .join('') + + const tableHtml = tableContent.replace(/[\s\S]*?<\/tbody>/, `${entityRows}`) + + result = result.replace(failedTableRegex, tableHtml) + } + } else { + result = result.replace(/\{\{#total_fail\}\}[\s\S]*?\{\{\/total_fail\}\}/g, '') + } + + return result + } + + /** + * Apply additional dynamic translations that couldn't be handled by template variables + */ + const applyDynamicTranslations = (html: string): string => { + const result = html + + // Apply any remaining pattern-based translations + // This is where we can add more sophisticated translation logic if needed + + return result + } + + return null // This component doesn't render anything visible +} + +export default HTMLTemplateRenderer diff --git a/src/components/ThemeToggle/ThemeToggle.css b/src/components/ThemeToggle/ThemeToggle.css index 53bb882..728262b 100644 --- a/src/components/ThemeToggle/ThemeToggle.css +++ b/src/components/ThemeToggle/ThemeToggle.css @@ -35,7 +35,8 @@ } .sun-icon, -.moon-icon { +.moon-icon, +.system-icon { position: absolute; top: 0; left: 0; @@ -56,6 +57,13 @@ color: #7aa2f7; /* Tokyo Night blue */ } +/* System icon defaults hidden; shown when wrapper has .system */ +.system-icon { + opacity: 0; + transform: rotate(-90deg) scale(0); + color: var(--text-primary); +} + .dark .sun-icon { opacity: 0; transform: rotate(90deg) scale(0); @@ -65,3 +73,15 @@ opacity: 1; transform: rotate(0) scale(1); } + +/* When explicitly in system mode, show only the system icon */ +.system .sun-icon, +.system .moon-icon { + opacity: 0; + transform: rotate(90deg) scale(0); +} + +.system .system-icon { + opacity: 1; + transform: rotate(0) scale(1); +} diff --git a/src/components/ThemeToggle/ThemeToggle.tsx b/src/components/ThemeToggle/ThemeToggle.tsx index ca826cc..813cdd2 100644 --- a/src/components/ThemeToggle/ThemeToggle.tsx +++ b/src/components/ThemeToggle/ThemeToggle.tsx @@ -1,20 +1,23 @@ import { useTheme } from '@context' -import { IconSun, IconMoon } from '@tabler/icons-react' +import { IconSun, IconMoon, IconDeviceDesktop } from '@tabler/icons-react' import './ThemeToggle.css' export const ThemeToggle = () => { - const { isDarkMode, toggleTheme } = useTheme() + const { mode, isDarkMode, setTheme } = useTheme() + + const cycleMode = () => { + if (mode === 'system') setTheme('light') + else if (mode === 'light') setTheme('dark') + else setTheme('system') + } return ( - diff --git a/src/components/Translation/Translation.ts b/src/components/Translation/Translation.ts index 38fe8b0..344ac58 100644 --- a/src/components/Translation/Translation.ts +++ b/src/components/Translation/Translation.ts @@ -4,13 +4,22 @@ import { initReactI18next } from 'react-i18next' import Backend from 'i18next-http-backend' import LanguageDetector from 'i18next-browser-languagedetector' +const supportedLngs = ['en', 'de', 'fr', 'it', 'rm'] + i18n .use(Backend) .use(LanguageDetector) .use(initReactI18next) .init({ fallbackLng: 'en', - debug: true, + supportedLngs, + load: 'languageOnly', + debug: false, + detection: { + order: ['localStorage', 'navigator', 'querystring'], + caches: ['localStorage'], + lookupLocalStorage: 'i18nextLng', + }, interpolation: { escapeValue: false, // not needed for react as it escapes by default }, diff --git a/src/components/UploadCard/UploadCard.tsx b/src/components/UploadCard/UploadCard.tsx index c5047eb..7667dbf 100644 --- a/src/components/UploadCard/UploadCard.tsx +++ b/src/components/UploadCard/UploadCard.tsx @@ -5,6 +5,7 @@ import { useEffect, useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { BcfData, downloadBcfReport } from '../../utils/bcfUtils' +import { ValidationResult } from '../../types/validation' import { processFile } from './processFile.ts' import { ErrorDisplay, @@ -15,7 +16,8 @@ import { UploadInstructions, } from './components' import { UploadCardTitle } from './UploadCardTitle.tsx' -import { useFileProcessor, useHtmlReport } from './hooks' +import { useFileProcessor } from './hooks' +import { useEnhancedHtmlReport } from './hooks/useEnhancedHtmlReport' import { FileError } from './hooks/useFileProcessor' export const UploadCard = () => { @@ -53,7 +55,7 @@ export const UploadCard = () => { reportFormats, }) - const { openHtmlReport } = useHtmlReport(templateContent, i18n) + const { openHtmlReport, downloadHtmlReport } = useEnhancedHtmlReport(templateContent) // Function to scroll to results const scrollToResults = () => { @@ -77,13 +79,16 @@ export const UploadCard = () => { useEffect(() => { // Load the HTML template - fetch('/report.html') - .then((response) => response.text()) - .then((content) => { - console.log('Template loaded, length:', content.length) + const loadTemplate = async () => { + try { + const response = await fetch('/report.html') + const content = await response.text() setTemplateContent(content) - }) - .catch((error) => console.error('Error loading template:', error)) + } catch (error) { + console.error('Failed to load template:', error) + } + } + loadTemplate() }, []) const handleBcfDownload = (result: { @@ -104,6 +109,15 @@ export const UploadCard = () => { } } + const handleHtmlDownload = async (result: ValidationResult) => { + try { + await downloadHtmlReport(result) + } catch (error) { + console.error('Failed to download HTML report:', error) + setUploadError(`Failed to download HTML report: ${error instanceof Error ? error.message : 'Unknown error'}`) + } + } + const handleClick = async () => { setUploadError(null) setProcessedResults([]) @@ -178,6 +192,7 @@ export const UploadCard = () => { processedResults={processedResults} reportFormats={reportFormats} onHtmlReport={openHtmlReport} + onHtmlDownload={handleHtmlDownload} onBcfDownload={handleBcfDownload} resultsRef={resultsRef} /> diff --git a/src/components/UploadCard/components/ProcessingConsole.tsx b/src/components/UploadCard/components/ProcessingConsole.tsx index acf0c71..c7a5e6c 100644 --- a/src/components/UploadCard/components/ProcessingConsole.tsx +++ b/src/components/UploadCard/components/ProcessingConsole.tsx @@ -1,5 +1,5 @@ import { Group, Paper, ScrollArea, Stack, Text } from '@mantine/core' -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' interface ProcessingConsoleProps { @@ -25,6 +25,8 @@ const consoleStyles = { export const ProcessingConsole = ({ isProcessing, logs }: ProcessingConsoleProps) => { const { t } = useTranslation() const [loadingDots, setLoadingDots] = useState('') + const bottomRef = useRef(null) + const [isHovered, setIsHovered] = useState(false) useEffect(() => { if (isProcessing) { @@ -35,50 +37,97 @@ export const ProcessingConsole = ({ isProcessing, logs }: ProcessingConsoleProps } }, [isProcessing]) + // Auto-scroll to the latest log entry whenever logs update or processing state changes + useEffect(() => { + if (!isHovered) return + + // Slight delay ensures DOM has rendered the new log before scrolling + const id = setTimeout(() => { + bottomRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }) + }, 0) + return () => clearTimeout(id) + }, [logs.length, isProcessing, isHovered]) + if (!isProcessing && logs.length === 0) { return null } return ( - - - - {t('console.loading.processingLogs', 'Processing Logs')} - - - {logs.length} {t('console.loading.entries', 'entries')} - - + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + role='region' + aria-label={t('console.loading.processingLogs', 'Processing Logs')} + aria-expanded={isHovered} + > + {isHovered && ( + + + {t('console.loading.processingLogs', 'Processing Logs')} + + + {logs.length} {t('console.loading.entries', 'entries')} + + + )} - - - {logs.map((log, index) => ( - - {log} - - ))} - {isProcessing && ( - - {loadingDots} - - )} - - + {isHovered ? ( + + + {logs.map((log, index) => ( + + {log} + + ))} + {isProcessing && ( + + {loadingDots} + + )} +
+ + + ) : ( + + {(logs[logs.length - 1] || (isProcessing ? '' : '')) + (isProcessing ? ` ${loadingDots}` : '')} + + )} ) } diff --git a/src/components/UploadCard/components/ResultsDisplay.tsx b/src/components/UploadCard/components/ResultsDisplay.tsx index f4975c5..d7d6811 100644 --- a/src/components/UploadCard/components/ResultsDisplay.tsx +++ b/src/components/UploadCard/components/ResultsDisplay.tsx @@ -1,4 +1,4 @@ -import { Alert, Button, Group, Text } from '@mantine/core' +import { Alert, Button, Group, Text, Tooltip } from '@mantine/core' import { IconDownload, IconFileText } from '@tabler/icons-react' import { useTranslation } from 'react-i18next' import { useEffect, RefObject } from 'react' @@ -13,6 +13,7 @@ interface ResultsDisplayProps { bcf: boolean } onHtmlReport: (result: ValidationResult, fileName: string) => void + onHtmlDownload: (result: ValidationResult) => Promise onBcfDownload: (result: { fileName: string result: { @@ -26,6 +27,7 @@ export const ResultsDisplay = ({ processedResults, reportFormats, onHtmlReport, + onHtmlDownload, onBcfDownload, resultsRef, }: ResultsDisplayProps) => { @@ -67,57 +69,93 @@ export const ResultsDisplay = ({ {processedResults.map((result, index) => ( {reportFormats.html && ( - + }} + > + View HTML - {result.fileName} + + + + + )} {reportFormats.bcf && (