diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json
index 44decbe..0c20fb8 100644
--- a/public/locales/de/translation.json
+++ b/public/locales/de/translation.json
@@ -127,6 +127,8 @@
"name": "Name",
"description": "Beschreibung",
"warning": "Warnung",
+ "specNotApplyToVersion": "Spezifikation gilt nicht für diese IFC-Version",
+ "failureReason": "Fehlergrund",
"globalId": "Globale ID",
"tag": "Kennzeichnung",
"reportBy": "Bericht von",
@@ -156,25 +158,56 @@
"elementsPassedPrefix": "Elemente erfüllt",
"applicability": "Anwendbarkeit",
"all": "Alle",
- "data": "Daten"
+ "data": "Daten",
+ "allEntityData": "Alle {{entity}} Daten"
},
"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"
+ "dataRequired": "{{property}} Daten müssen {{value}} sein und im Eigenschaftssatz {{propertySet}}",
+ "enumRequired": "{{field}} muss einer der folgenden Werte sein: {{values}}",
+ "propertyInSet": "Eigenschaft {{property}} im Satz {{propertySet}}",
+ "propertyWithValue": "Eigenschaft {{property}} im Satz {{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 +215,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..b65fbe2 100644
--- a/public/locales/en/translation.json
+++ b/public/locales/en/translation.json
@@ -135,6 +135,8 @@
"name": "Name",
"description": "Description",
"warning": "Warning",
+ "specNotApplyToVersion": "specification does not apply to this IFC version",
+ "failureReason": "Failure Reason",
"globalId": "GlobalId",
"tag": "Tag",
"reportBy": "Report by",
@@ -190,6 +192,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..49d7a8c 100644
--- a/public/locales/fr/translation.json
+++ b/public/locales/fr/translation.json
@@ -159,6 +159,11 @@
"all": "Tous",
"data": "Données"
},
+ "specificationsPassedPrefix": "Spécifications réussies",
+ "requirementsPassedPrefix": "Exigences respectées",
+ "applicability": "Applicabilité",
+ "specNotApplyToVersion": "la spécification ne s'applique pas à cette version IFC",
+ "failureReason": "Raison de l'échec",
"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}} ..."
diff --git a/public/locales/it/translation.json b/public/locales/it/translation.json
index 18bf448..fc3f71f 100644
--- a/public/locales/it/translation.json
+++ b/public/locales/it/translation.json
@@ -159,6 +159,11 @@
"all": "Tutti",
"data": "Dati"
},
+ "specificationsPassedPrefix": "Specifiche superate",
+ "requirementsPassedPrefix": "Requisiti soddisfatti",
+ "applicability": "Applicabilità",
+ "specNotApplyToVersion": "la specifica non si applica a questa versione IFC",
+ "failureReason": "Motivo del fallimento",
"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}} ..."
diff --git a/public/locales/rm/translation.json b/public/locales/rm/translation.json
index 14692fe..44e336f 100644
--- a/public/locales/rm/translation.json
+++ b/public/locales/rm/translation.json
@@ -159,6 +159,11 @@
"all": "Tut",
"data": "Datas"
},
+ "specificationsPassedPrefix": "Specificaziuns reussidas",
+ "requirementsPassedPrefix": "Pretensiuns satisfatgas",
+ "applicability": "Applicabilitad",
+ "specNotApplyToVersion": "la specificaziun na s'applitgescha betg a questa versiun IFC",
+ "failureReason": "Motiv dal falliment",
"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}} ..."
diff --git a/public/pyodideWorkerClean.js b/public/pyodideWorkerClean.js
new file mode 100644
index 0000000..46e1157
--- /dev/null
+++ b/public/pyodideWorkerClean.js
@@ -0,0 +1,254 @@
+/* global importScripts */
+importScripts('https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.js')
+
+let pyodide = null
+
+// 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.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}`),
+ })
+ throw error
+ }
+}
+
+const WORKER_VERSION = '3.0.0-clean'
+console.log('pyodideWorkerClean version:', WORKER_VERSION)
+
+self.onmessage = async function (e) {
+ const { arrayBuffer, idsContent, fileName, language, generateBcf, idsFilename } = e.data
+
+ 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'])
+
+ // Bypass the Emscripten version compatibility check for wheels
+ self.postMessage({
+ type: 'progress',
+ message: getConsoleMessage('console.loading.micropipPatch', 'Patching micropip for compatibility...'),
+ })
+
+ await pyodide.runPythonAsync(`
+import micropip
+from micropip._micropip import WheelInfo
+WheelInfo.check_compatible = lambda self: None
+ `)
+
+ // Install IfcOpenShell and IfcTester
+ 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
+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...'),
+ })
+
+ pyodide.FS.writeFile('/input.ifc', new Uint8Array(arrayBuffer))
+ if (idsContent) {
+ pyodide.FS.writeFile('/input.ids', idsContent)
+ }
+
+ // Run validation using native IfcTester
+ self.postMessage({
+ type: 'progress',
+ message: getConsoleMessage('console.loading.validation', 'Running IFC validation...'),
+ })
+
+ const validationResult = await pyodide.runPythonAsync(`
+import json
+import os
+import ifcopenshell
+from ifctester import ids, reporter
+from datetime import datetime
+
+# Open IFC file
+print("Opening IFC file: /input.ifc")
+ifc = ifcopenshell.open('/input.ifc')
+
+# Initialize basic results structure
+results = {
+ "title": "IDS Validation Report",
+ "filename": "${fileName}",
+ "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ "language_code": "${language}",
+ "status": True,
+ "validation_status": "success"
+}
+
+# Check if IDS file exists and validate
+if os.path.exists('/input.ids'):
+ print("Loading IDS specification: /input.ids")
+ ids_spec = ids.open('/input.ids')
+
+ 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"] = "${fileName}"
+ results["language_code"] = "${language}"
+ 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"] = []
+
+# 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,
+ })
+ }
+ }
+}
diff --git a/public/python/model_checker.py b/public/python/model_checker.py
index 96ea454..8ed96e7 100644
--- a/public/python/model_checker.py
+++ b/public/python/model_checker.py
@@ -6,48 +6,9 @@
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"
- }
- }
-}
+import json
+import base64
+from datetime import datetime
def check_dependencies():
"""Check and install required dependencies."""
@@ -70,83 +31,137 @@ def 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.
+def extract_requirement_data(requirement):
+ """Extract structured data from IDS requirement for JSON output"""
+ req_data = {
+ "type": requirement.__class__.__name__,
+ "cardinality": getattr(requirement, 'cardinality', 'required'),
+ "status": "pass" if not getattr(requirement, 'failed_entities', []) else "fail"
+ }
- Args:
- html_content: HTML content to translate
- lang: Language code (e.g., 'en', 'de')
+ # Extract requirement details based on type
+ if hasattr(requirement, 'name') and requirement.name:
+ req_data["name"] = str(requirement.name)
+ if hasattr(requirement, 'value') and requirement.value:
+ req_data["value"] = str(requirement.value)
+ if hasattr(requirement, 'propertySet') and requirement.propertySet:
+ req_data["propertySet"] = str(requirement.propertySet)
+ if hasattr(requirement, 'baseName') and requirement.baseName:
+ req_data["baseName"] = str(requirement.baseName)
+ if hasattr(requirement, 'system') and requirement.system:
+ req_data["system"] = str(requirement.system)
+ if hasattr(requirement, 'instructions') and requirement.instructions:
+ req_data["instructions"] = str(requirement.instructions)
- 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]
+ # Handle restrictions and patterns
+ if hasattr(requirement, 'restriction'):
+ restriction = requirement.restriction
+ if hasattr(restriction, 'enumeration') and restriction.enumeration:
+ req_data["enumeration"] = [str(val) for val in restriction.enumeration]
+ if hasattr(restriction, 'pattern') and restriction.pattern:
+ req_data["pattern"] = str(restriction.pattern)
+ if hasattr(restriction, 'bounds') and restriction.bounds:
+ req_data["bounds"] = str(restriction.bounds)
+
+ # Extract failed/passed entities for detailed reporting
+ if hasattr(requirement, 'failed_entities'):
+ req_data["failed_entities"] = []
+ for entity in requirement.failed_entities[:10]: # Limit to first 10
+ entity_data = {
+ "global_id": getattr(entity, 'GlobalId', ''),
+ "class": entity.__class__.__name__,
+ "predefined_type": getattr(entity, 'PredefinedType', 'NOTDEFINED'),
+ "name": getattr(entity, 'Name', ''),
+ "tag": getattr(entity, 'Tag', ''),
+ "description": getattr(entity, 'Description', ''),
+ "reason": '' # TODO: Add failure reason if available
+ }
+ req_data["failed_entities"].append(entity_data)
+
+ if hasattr(requirement, 'passed_entities'):
+ req_data["passed_entities"] = []
+ for entity in requirement.passed_entities[:10]: # Limit to first 10
+ entity_data = {
+ "global_id": getattr(entity, 'GlobalId', ''),
+ "class": entity.__class__.__name__,
+ "predefined_type": getattr(entity, 'PredefinedType', 'NOTDEFINED'),
+ "name": getattr(entity, 'Name', ''),
+ "tag": getattr(entity, 'Tag', ''),
+ "description": getattr(entity, 'Description', ''),
+ "reason": ''
+ }
+ req_data["passed_entities"].append(entity_data)
+
+ return req_data
+
+def extract_applicability_data(applicability):
+ """Extract applicability data for JSON output"""
+ if not applicability:
+ return None
- # Use regex with word boundaries to avoid partial replacements
- html_content = re.sub(r'\b' + english_term + r'\b', translation, html_content)
+ app_data = {}
- print(f"HTML report translation to {lang} completed")
- return html_content
+ if hasattr(applicability, 'entity') and applicability.entity:
+ app_data["entity"] = {
+ "name": str(applicability.entity.name) if applicability.entity.name else None,
+ "predefinedType": str(applicability.entity.predefinedType) if hasattr(applicability.entity, 'predefinedType') and applicability.entity.predefinedType else None
+ }
+
+ if hasattr(applicability, 'classification') and applicability.classification:
+ app_data["classification"] = []
+ classifications = applicability.classification if isinstance(applicability.classification, list) else [applicability.classification]
+ for cls in classifications:
+ cls_data = {
+ "system": str(cls.system) if cls.system else None,
+ "value": str(cls.value) if cls.value else None
+ }
+ app_data["classification"].append(cls_data)
+
+ if hasattr(applicability, 'attribute') and applicability.attribute:
+ app_data["attribute"] = []
+ attributes = applicability.attribute if isinstance(applicability.attribute, list) else [applicability.attribute]
+ for attr in attributes:
+ attr_data = {
+ "name": str(attr.name) if attr.name else None,
+ "value": str(attr.value) if attr.value else None
+ }
+ app_data["attribute"].append(attr_data)
+
+ if hasattr(applicability, 'property') and applicability.property:
+ app_data["property"] = []
+ properties = applicability.property if isinstance(applicability.property, list) else [applicability.property]
+ for prop in properties:
+ prop_data = {
+ "propertySet": str(prop.propertySet) if prop.propertySet else None,
+ "baseName": str(prop.baseName) if prop.baseName else None,
+ "value": str(prop.value) if prop.value else None
+ }
+ app_data["property"].append(prop_data)
+
+ if hasattr(applicability, 'material') and applicability.material:
+ app_data["material"] = []
+ materials = applicability.material if isinstance(applicability.material, list) else [applicability.material]
+ for mat in materials:
+ mat_data = {
+ "value": str(mat.value) if mat.value else None
+ }
+ app_data["material"].append(mat_data)
+
+ return app_data if app_data else None
-def test_ifc(ifc_path: str, ids_path: str, report_path: str = "report.html", lang: str = "en"):
+def validate_ifc_ids_json(ifc_path: str, ids_path: str, lang: str = "en"):
"""
- Test an IFC file against an IDS specification and generate reports in multiple formats.
+ Validate IFC against IDS and return structured JSON data for frontend processing.
+ This generates the same data that would be used in HTML templates but in JSON format.
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
-
+ ids_path: Path to the IDS specification file
+ lang: Language code for report (used for metadata only)
+
Returns:
- Dictionary with validation status and paths to generated reports
+ JSON string with structured validation results
"""
- 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):
@@ -166,83 +181,231 @@ def test_ifc(ifc_path: str, ids_path: str, report_path: str = "report.html", lan
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)
+ # Build structured results that match the HTML template structure
+ results = {
+ # Template metadata
+ "title": f"IDS Validation Report",
+ "filename": os.path.basename(ifc_path),
+ "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ "language_code": lang,
+ "_lang": lang,
+
+ # Overall summary statistics
+ "total_specifications": len(ids_spec.specifications),
+ "total_specifications_pass": 0,
+ "total_specifications_fail": 0,
+ "total_requirements": 0,
+ "total_requirements_pass": 0,
+ "total_requirements_fail": 0,
+ "total_checks": 0,
+ "total_checks_pass": 0,
+ "total_checks_fail": 0,
+ "total_applicable": 0,
+ "total_applicable_pass": 0,
+ "total_applicable_fail": 0,
+
+ # Individual specifications
+ "specifications": []
+ }
- # 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()
+ # Process each specification to match HTML template structure
+ for spec in ids_spec.specifications:
+ spec_data = {
+ "name": spec.name,
+ "description": getattr(spec, 'description', ''),
+ "instructions": getattr(spec, 'instructions', ''),
+ "identifier": getattr(spec, 'identifier', ''),
+ "ifcVersion": getattr(spec, 'ifcVersion', []),
+ "is_ifc_version": True, # Assume valid for now
+
+ # Status and statistics
+ "status": not bool(getattr(spec, 'failed_entities', [])),
+ "status_text": "PASS" if not getattr(spec, 'failed_entities', []) else "FAIL",
+ "total_checks": 0,
+ "total_checks_pass": 0,
+ "total_checks_fail": 0,
+ "percent_checks_pass": 0,
+ "total_applicable": 0,
+ "total_applicable_pass": 0,
+ "total_applicable_fail": 0,
+
+ # Applicability and requirements
+ "applicability": [],
+ "requirements": []
+ }
+
+ # Extract applicability
+ if hasattr(spec, 'applicability') and spec.applicability:
+ app_data = extract_applicability_data(spec.applicability)
+ if app_data:
+ # Convert to display format for template
+ app_display = []
+ if app_data.get('entity'):
+ entity_text = f"All {app_data['entity']['name']} data"
+ if app_data['entity']['predefinedType']:
+ entity_text += f" with PredefinedType {app_data['entity']['predefinedType']}"
+ app_display.append(entity_text)
+
+ for prop in app_data.get('property', []):
+ prop_text = f"Property {prop['baseName']} in set {prop['propertySet']}"
+ if prop['value']:
+ prop_text += f" with value {prop['value']}"
+ app_display.append(prop_text)
+
+ spec_data["applicability"] = app_display
+
+ # Extract requirements
+ if hasattr(spec, 'requirements') and spec.requirements:
+ for req in spec.requirements:
+ req_data = extract_requirement_data(req)
+
+ # Build requirement display data that matches template structure
+ req_display = {
+ "description": format_requirement_description(req_data),
+ "status": req_data["status"] == "pass",
+ "total_checks": len(req_data.get("failed_entities", [])) + len(req_data.get("passed_entities", [])),
+ "total_pass": len(req_data.get("passed_entities", [])),
+ "total_fail": len(req_data.get("failed_entities", [])),
+ "passed_entities": req_data.get("passed_entities", []),
+ "failed_entities": req_data.get("failed_entities", []),
+ "has_omitted_passes": len(req_data.get("passed_entities", [])) > 10,
+ "has_omitted_failures": len(req_data.get("failed_entities", [])) > 10
+ }
+
+ spec_data["requirements"].append(req_display)
+
+ # Update counters
+ spec_data["total_checks"] += req_display["total_checks"]
+ spec_data["total_checks_pass"] += req_display["total_pass"]
+ spec_data["total_checks_fail"] += req_display["total_fail"]
- translated_html = translate_html_report(html_content, lang)
+ # Calculate percentages
+ if spec_data["total_checks"] > 0:
+ spec_data["percent_checks_pass"] = round((spec_data["total_checks_pass"] / spec_data["total_checks"]) * 100)
- with open(report_path, "w", encoding="utf-8") as f:
- f.write(translated_html)
+ results["specifications"].append(spec_data)
+
+ # Update overall counters
+ if spec_data["status"]:
+ results["total_specifications_pass"] += 1
+ else:
+ results["total_specifications_fail"] += 1
+
+ results["total_checks"] += spec_data["total_checks"]
+ results["total_checks_pass"] += spec_data["total_checks_pass"]
+ results["total_checks_fail"] += spec_data["total_checks_fail"]
- results["html_path"] = os.path.abspath(report_path)
+ # Calculate overall percentages
+ if results["total_checks"] > 0:
+ results["percent_checks_pass"] = round((results["total_checks_pass"] / results["total_checks"]) * 100)
+ else:
+ results["percent_checks_pass"] = 0
+
+ results["status"] = results["total_specifications_fail"] == 0
+ results["status_text"] = "PASS" if results["status"] else "FAIL"
- # 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)
+ # Generate BCF data
+ print("Generating BCF data...")
+ try:
+ bcf_reporter = reporter.Bcf(ids_spec)
+ bcf_reporter.report()
+
+ # Create BCF in memory
+ import tempfile
+ with tempfile.NamedTemporaryFile(suffix='.bcf', delete=False) as tmp_file:
+ bcf_reporter.to_file(tmp_file.name)
+ with open(tmp_file.name, 'rb') as f:
+ bcf_bytes = f.read()
+ results["bcf_data"] = {
+ "zip_content": base64.b64encode(bcf_bytes).decode('utf-8'),
+ "filename": f"{os.path.splitext(os.path.basename(ifc_path))[0]}.bcf"
+ }
+ os.unlink(tmp_file.name) # Clean up
+ except Exception as bcf_error:
+ print(f"BCF generation failed: {bcf_error}")
+ results["bcf_data"] = None
- # 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 json.dumps(results, ensure_ascii=False, indent=2)
- # 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
+ error_result = {
+ "error": str(e),
+ "status": "error",
+ "title": "Validation Error",
+ "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ }
+ return json.dumps(error_result, ensure_ascii=False)
+
+def format_requirement_description(req_data):
+ """Format requirement data into human-readable description"""
+ req_type = req_data.get("type", "")
+
+ if "Property" in req_type:
+ if req_data.get("value"):
+ return f"{req_data.get('baseName', 'Property')} shall be {req_data.get('value')} in the dataset {req_data.get('propertySet', 'PropertySet')}"
+ else:
+ return f"{req_data.get('baseName', 'Property')} shall be provided in the dataset {req_data.get('propertySet', 'PropertySet')}"
+
+ elif "Entity" in req_type:
+ if req_data.get("enumeration"):
+ return f"The {req_data.get('name', 'Name')} shall be {{{{'enumeration': {req_data.get('enumeration', [])}}}}}"
+ else:
+ return f"The {req_data.get('name', 'Name')} shall be {req_data.get('value', '')}"
+
+ elif "Attribute" in req_type:
+ return f"The {req_data.get('name', 'Attribute')} shall be {req_data.get('value', '')}"
+
+ elif "Classification" in req_type:
+ return f"Classification {req_data.get('value', '')} in system {req_data.get('system', '')}"
+
+ elif "Material" in req_type:
+ return f"Material shall be {req_data.get('value', '')}"
+
+ else:
+ return f"{req_type}: {req_data.get('name', '')} = {req_data.get('value', '')}"
def main():
- # Set up argument parser
+ # 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")
+ parser.add_argument("--ids", "-s", help="Path to the IDS specification file")
+ parser.add_argument("--lang", "-l", default="en", help="Language for the report (default: en)")
+ parser.add_argument("--json", action="store_true", help="Output JSON instead of running interactively")
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
+ # If JSON mode, return structured data for the worker
+ if args.json and args.ifc and args.ids:
+ try:
+ result_json = validate_ifc_ids_json(args.ifc, args.ids, args.lang)
+ print(result_json) # This will be captured by the worker
+ except Exception as e:
+ error_json = json.dumps({"error": str(e)}, ensure_ascii=False)
+ print(error_json)
+ return
+
+ # Interactive mode for testing
+ 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.lang:
+ args.lang = input("Enter language code (default: en): ") or "en"
+
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}")
+ result_json = validate_ifc_ids_json(args.ifc, args.ids, args.lang)
+ result = json.loads(result_json)
+
+ if "error" in result:
+ print(f"Error: {result['error']}")
+ sys.exit(1)
+ else:
+ print(f"\nValidation {result['status_text']}!")
+ print(f"Total specifications: {result['total_specifications']}")
+ print(f"Passed: {result['total_specifications_pass']}")
+ print(f"Failed: {result['total_specifications_fail']}")
+ print(f"Total checks: {result['total_checks']}")
+ print(f"Success rate: {result['percent_checks_pass']}%")
+
except Exception as e:
print(f"Error: {str(e)}")
sys.exit(1)
diff --git a/public/report-viewer.html b/public/report-viewer.html
new file mode 100644
index 0000000..ac74b84
--- /dev/null
+++ b/public/report-viewer.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+ Report Viewer
+
+
+
+ Loading report...
+
+
+
+
+
diff --git a/public/report.html b/public/report.html
index a6cc7d7..7593727 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}}
@@ -301,28 +329,28 @@ {{name}}
{{^total_checks}}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
+ {{t.warning}}: {{t.specNotApplyToVersion}}
{{/is_ifc_version}}
- Applicability
+ {{t.applicability}}
{{#applicability}}
@@ -330,20 +358,17 @@ {{name}}
{{/applicability}}
- Requirements
+ {{t.requirements}}
{{#requirements}}
-
+ {{#total_checks}}
{{description}}
- {{#total_ckecks}}
-
- {{/total_ckecks}} {{#total_pass}}
+ {{#total_pass}}
@@ -384,9 +409,8 @@ {{name}}
{{t.predefinedType}} |
{{t.name}} |
{{t.description}} |
- {{t.warning}} |
{{t.globalId}} |
- {{t.tag}} |
+ {{t.failureReason}} |
@@ -396,23 +420,26 @@ {{name}}
{{predefined_type}} |
{{name}} |
{{description}} |
- {{reason}} |
{{global_id}} |
- {{tag}} |
+ {{reason}} |
{{#extra_of_type}}
- | {{t.moreOfSameType}} |
+ {{t.moreOfSameType}} |
{{/extra_of_type}} {{/failed_entities}} {{#has_omitted_failures}}
- | {{t.moreElementsNotShown}} |
+ {{t.moreElementsNotShown}} |
{{/has_omitted_failures}}
{{/total_fail}}
+ {{/total_checks}}
+ {{^total_checks}}
+ {{description}}
+ {{/total_checks}}
{{/requirements}}
@@ -422,8 +449,11 @@
{{name}}
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')}
-
+
+
+
{t('open-source')}
diff --git a/src/components/IDSReport/HTMLTemplateRenderer.tsx b/src/components/IDSReport/HTMLTemplateRenderer.tsx
new file mode 100644
index 0000000..76b1d12
--- /dev/null
+++ b/src/components/IDSReport/HTMLTemplateRenderer.tsx
@@ -0,0 +1,377 @@
+import React, { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { IDSTranslationService, ValidationResult } from '../../services/IDSTranslationService'
+
+interface HTMLTemplateRendererProps {
+ validationResults: ValidationResult
+ onReportGenerated: (htmlContent: string) => void
+}
+
+/**
+ * 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))
+ }, [])
+
+ // Generate the report when template and results are ready
+ useEffect(() => {
+ if (templateContent && validationResults) {
+ generateReport()
+ }
+ }, [templateContent, validationResults, i18n.language])
+
+ const generateReport = () => {
+ if (!templateContent) return
+
+ // Create translation service
+ const translationService = new IDSTranslationService(t)
+
+ // Translate the validation results
+ const translatedResults = translationService.translateValidationResults(validationResults)
+
+ // Prepare template data with translations
+ const 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'),
+ 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 ...',
+ ),
+ },
+ }
+
+ // 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, translationService)
+
+ // Callback with the generated HTML
+ onReportGenerated(htmlContent)
+ }
+
+ /**
+ * Replace Mustache-style template variables with actual values
+ */
+ const replaceMustacheVariables = (template: string, data: any): 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')
+ result = result.replace(regex, data[varName] || '')
+ })
+
+ // 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.moreOfSameType',
+ 't.moreElementsNotShown',
+ ]
+
+ translationVars.forEach((varName) => {
+ const regex = new RegExp(`\\{\\{${varName}\\}\\}`, 'g')
+ const value = getNestedValue(data, varName)
+ result = result.replace(regex, 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: any, path: string): any => {
+ return path.split('.').reduce((current, key) => current?.[key], obj)
+ }
+
+ /**
+ * Process conditional sections in the template
+ */
+ const processConditionalSections = (template: string, data: any): 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 (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 = (template: string, data: any): 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) {
+ const specContent = specTemplate.replace(/\{\{#specifications\}\}/, '').replace(/\{\{\/specifications\}\}/, '')
+
+ const specHtml = data.specifications
+ .map((spec: any) => {
+ 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')
+ specSection = specSection.replace(regex, spec[varName] || '')
+ })
+
+ // Handle applicability loop
+ if (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, app)).join('')
+
+ specSection = specSection.replace(appLoopRegex, appHtml)
+ }
+ }
+
+ // Handle requirements loop
+ if (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: any) => {
+ let reqSection = reqContent
+
+ // Replace requirement variables
+ const reqVars = ['description', 'total_checks', 'total_pass', 'total_fail']
+ reqVars.forEach((varName) => {
+ const regex = new RegExp(`\\{\\{${varName}\\}\\}`, 'g')
+ reqSection = reqSection.replace(regex, req[varName] || '')
+ })
+
+ // 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
+ }
+
+ /**
+ * Process entity tables for passed and failed entities
+ */
+ const processEntityTables = (template: string, req: any): string => {
+ let result = template
+
+ // Process passed entities table
+ if (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: any) =>
+ `
+ | ${entity.type} |
+ ${entity.predefinedType || ''} |
+ ${entity.name} |
+ ${entity.description} |
+ ${entity.globalId} |
+ ${entity.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 && 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: any) =>
+ `
+ | ${entity.type} |
+ ${entity.predefinedType || ''} |
+ ${entity.name} |
+ ${entity.description} |
+ Warning message here |
+ ${entity.globalId} |
+ ${entity.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, translationService: IDSTranslationService): string => {
+ let 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 (
-