# IfcOpenShell - IFC toolkit and geometry engine # Copyright (C) 2022, 2023 @Andrej730 # # This file is part of IfcOpenShell. # # IfcOpenShell is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # IfcOpenShell is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with IfcOpenShell. If not, see . import copy import json from pathlib import Path from typing import Optional, TypedDict, Union from typing_extensions import NotRequired import ifcopenshell import ifcopenshell.ifcopenshell_wrapper as ifcopenshell_wrapper import ifcopenshell.util.attribute import ifcopenshell.util.schema try: import glob import re import shutil import urllib.parse import warnings import zipfile import requests from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning from lxml import etree from markdown import markdown except: pass # Only necessary if you're using it to generate the docs database BASE_MODULE_PATH = Path(__file__).parent IFC2x3_DOCS_LOCATION = BASE_MODULE_PATH / "Ifc2.3.0.1" IFC4_DOCS_LOCATION = BASE_MODULE_PATH / "Ifc4.0.2.1" IFC4x3_HTML_LOCATION = BASE_MODULE_PATH / "IFC4.3-html" IFC4x3_DEV_LOCATION = BASE_MODULE_PATH / "IFC4.3.x-development" IFC4x3_SPEC_URL_TEMPLATE = "https://ifc43-docs.standards.buildingsmart.org/IFC/RELEASE/IFC4x3/HTML/lexical/%s.htm" class BaseData(TypedDict): description: str spec_url: str class EntityData(BaseData): attributes: NotRequired[dict[str, str]] predefined_types: NotRequired[dict[str, str]] class PsetData(TypedDict): # Apparently some psets in ifc4 are missing spec url / description. description: NotRequired[str] spec_url: NotRequired[str] properties: dict[str, str] class PropertyData(TypedDict): description: str # in IFC4x3 there is no children[] for properties children: NotRequired[dict[str, "PropertyData"]] class ClassesSuggestions(TypedDict): name: str predefined_type: NotRequired[str] class SchemaData(TypedDict): entities: dict[str, EntityData] types: dict[str, BaseData] properties: dict[str, PsetData] classes_suggestions: dict[str, ClassesSuggestions] SUPPORTED_SCHEMA = ifcopenshell.util.schema.IFC_SCHEMA SCHEMA_FILES: dict[SUPPORTED_SCHEMA, dict[str, Path]] = { "IFC2X3": { "entities": BASE_MODULE_PATH / "schema/ifc2x3_entities.json", "properties": BASE_MODULE_PATH / "schema/ifc2x3_properties.json", "types": BASE_MODULE_PATH / "schema/ifc2x3_types.json", "classes_suggestions": BASE_MODULE_PATH / "schema/ifc_classes_suggestions.json", }, "IFC4": { "entities": BASE_MODULE_PATH / "schema/ifc4_entities.json", "properties": BASE_MODULE_PATH / "schema/ifc4_properties.json", "types": BASE_MODULE_PATH / "schema/ifc4_types.json", "classes_suggestions": BASE_MODULE_PATH / "schema/ifc_classes_suggestions.json", }, "IFC4X3": { "entities": BASE_MODULE_PATH / "schema/ifc4x3_entities.json", "properties": BASE_MODULE_PATH / "schema/ifc4x3_properties.json", "types": BASE_MODULE_PATH / "schema/ifc4x3_types.json", "classes_suggestions": BASE_MODULE_PATH / "schema/ifc_classes_suggestions.json", }, } db: dict[SUPPORTED_SCHEMA, SchemaData] = None schema_by_name: dict[SUPPORTED_SCHEMA, Optional[ifcopenshell_wrapper.schema_definition]] = { "IFC2X3": None, "IFC4": None, "IFC4X3": None, } def get_db(version: ifcopenshell.util.schema.IFC_SCHEMA) -> Union[SchemaData, None]: global db if not db: db = {ifc_version: dict() for ifc_version in SCHEMA_FILES} for ifc_version in SCHEMA_FILES: for data_type in SCHEMA_FILES[ifc_version]: schema_path = SCHEMA_FILES[ifc_version][data_type] if not schema_path.is_file(): print(f"Schema file {schema_path} wasn't found.") files_missing = True continue with open(schema_path, "r") as fi: db[ifc_version][data_type] = json.load(fi) version = ifcopenshell.util.schema.get_fallback_schema(version) return db.get(version) def get_schema_by_name(version: str) -> ifcopenshell_wrapper.schema_definition: global schema_by_name version = ifcopenshell.util.schema.get_fallback_schema(version) if not schema_by_name[version]: schema_by_name[version] = ifcopenshell.schema_by_name(version) return schema_by_name[version] def get_class_suggestions( version: ifcopenshell.util.schema.IFC_SCHEMA, class_name: str, ) -> Union[ClassesSuggestions, None]: db = get_db(version) if not db: return class_suggestions = db["classes_suggestions"].get(class_name) return class_suggestions def get_entity_doc( version: ifcopenshell.util.schema.IFC_SCHEMA, entity_name: str, recursive: bool = True, ) -> Union[EntityData, None]: db = get_db(version) if db: entity = copy.deepcopy(db["entities"].get(entity_name)) if not recursive: return entity ifc_schema = get_schema_by_name(version) ifc_entity = ifc_schema.declaration_by_name(entity_name) ifc_supertype = ifc_entity.supertype() if ifc_supertype: parent_entity = get_entity_doc(version, ifc_supertype.name(), recursive=True) if "attributes" not in entity: entity["attributes"] = dict() for parent_attr in parent_entity.get("attributes", []): entity["attributes"][parent_attr] = parent_entity["attributes"][parent_attr] return entity def get_attribute_doc( version: ifcopenshell.util.schema.IFC_SCHEMA, entity: str, attribute: str, recursive=True, ) -> Union[str, None]: db = get_db(version) if db: entity_ = get_entity_doc(version, entity, recursive) if entity_ and "attributes" in entity_: return entity_["attributes"].get(attribute) def get_predefined_type_doc( version: ifcopenshell.util.schema.IFC_SCHEMA, entity: str, predefined_type: str, ) -> Union[str, None]: db = get_db(version) if db: entity_ = db["entities"].get(entity) if entity_: return entity_.get("predefined_types", {}).get(predefined_type) def get_property_set_doc(version: ifcopenshell.util.schema.IFC_SCHEMA, pset: str) -> Union[PsetData, None]: db = get_db(version) if db: return db["properties"].get(pset) def get_property_doc(version: ifcopenshell.util.schema.IFC_SCHEMA, pset: str, prop: str) -> Union[str, None]: db = get_db(version) if db: pset_ = db["properties"].get(pset) if pset_: return pset_["properties"].get(prop) def get_type_doc(version: ifcopenshell.util.schema.IFC_SCHEMA, ifc_type: str) -> Union[BaseData, None]: db = get_db(version) if db: return db["types"].get(ifc_type) # TODO: there are still some discrepancies between this method # and the specs website because of the asymmetry # More: https://github.com/buildingSMART/IFC4.3.x-development/issues/582 def get_inverse_attributes(el): inverse_attrs = [] for a in el.all_inverse_attributes(): attribute_type = a.attribute_reference().type_of_attribute() # unpacking aggregation types while isinstance(attribute_type, ifcopenshell.ifcopenshell_wrapper.aggregation_type): attribute_type = attribute_type.type_of_element() attribute_type = attribute_type.declared_type() # recursively looking for entities inside the selections types_to_process = [attribute_type] entity_attr_types = [] while types_to_process: for attr_type in types_to_process.copy(): if isinstance(attr_type, ifcopenshell.ifcopenshell_wrapper.select_type): types_to_process.extend([t for t in attr_type.select_list()]) else: entity_attr_types.append(attr_type.name()) types_to_process.remove(attr_type) if el.name() in entity_attr_types: inverse_attrs.append(a) return inverse_attrs class DocExtractor: def clean_highlighted_words(self, text: str) -> str: text = re.sub(r"\b_([a-zA-Z0-9]+)_\b", r"\1", text) text = re.sub(r"\*\*([a-zA-Z0-9]+)\*\*", r"\1", text) return text def clean_description(self, description): description = description.replace("\n", " ") description = description.replace("\u00a0", " ") description = description.split("HISTORY:", 1)[0] description = description.strip() return description def extract_full_description(self, html: str) -> str: """Extract the full definition text from markdown-derived HTML. Entity/type documentation often introduces a bulleted list mid-definition (e.g. "... may include:" followed by a `