Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions packages/core/src/openbim/IDSSpecifications/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import { ModelIdMap } from "../../fragments";
import { ModelIdMapUtils } from "../../utils";
// import { createPropertyFacets } from "./src/importers/property";
import { XML } from "../../utils/xml";

/**
* Component that manages Information Delivery Specification (IDS) data. It provides functionality for importing, exporting, and manipulating IDS data. 📕 [Tutorial](https://docs.thatopen.com/Tutorials/Components/Core/IDSSpecifications). 📘 [API](https://docs.thatopen.com/api/@thatopen/components/classes/IDSSpecifications).
Expand All @@ -36,7 +37,9 @@ export class IDSSpecifications extends Component {
numberParseOptions: { leadingZeros: true, hex: true },
parseAttributeValue: true,
preserveOrder: false,
processEntities: false,
// Decode & < > " ' so values compare equal to the
// model's text. fast-xml-parser applies its expansion limits by default.
processEntities: true,
removeNSPrefix: true,
trimValues: true,
});
Expand Down Expand Up @@ -240,14 +243,14 @@ export class IDSSpecifications extends Component {
const xml = `<ids xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://standards.buildingsmart.org/IDS http://standards.buildingsmart.org/IDS/1.0/ids.xsd" xmlns:ids="http://standards.buildingsmart.org/IDS">
<!-- Made with That Open Engine ${Components.release} (https://github.com/thatopen/engine_components) -->
<info>
<title>${info.title}</title>
${info.copyright ? `<copyright>${info.copyright}</copyright>` : ""}
${info.version ? `<version>${info.version}</version>` : ""}
${info.description ? `<description>${info.description}</description>` : ""}
${info.author ? `<author>${info.author}</author>` : ""}
<title>${XML.escape(info.title)}</title>
${info.copyright ? `<copyright>${XML.escape(info.copyright)}</copyright>` : ""}
${info.version ? `<version>${XML.escape(info.version)}</version>` : ""}
${info.description ? `<description>${XML.escape(info.description)}</description>` : ""}
${info.author ? `<author>${XML.escape(info.author)}</author>` : ""}
${info.date ? `<date>${info.date.toISOString().split("T")[0]}</date>` : ""}
${info.purpose ? `<purpose>${info.purpose}</purpose>` : ""}
${info.milestone ? `<milestone>${info.milestone}</milestone>` : ""}
${info.purpose ? `<purpose>${XML.escape(info.purpose)}</purpose>` : ""}
${info.milestone ? `<milestone>${XML.escape(info.milestone)}</milestone>` : ""}
</info>
<specifications>
${[..._specifications].map((spec) => spec.serialize()).join("\n")}
Expand Down
11 changes: 7 additions & 4 deletions packages/core/src/openbim/IDSSpecifications/src/Specification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { UUID } from "../../../utils";
import { IDSFacet } from "./facets";
import { IDSSpecifications } from "..";
import { ModelIdMap } from "../../../fragments";
import { XML } from "../../../utils/xml";

/**
* Represents a single specification from the Information Delivery Specification (IDS) standard.
Expand Down Expand Up @@ -87,15 +88,17 @@ export class IDSSpecification implements IDSSpecificationData {
* @returns The XML representation of the IDSSpecification.
*/
serialize() {
const name = `name="${this.name}"`;
const identifier = this.identifier ? `identifier="${this.identifier}"` : "";
const name = `name="${XML.escape(this.name)}"`;
const identifier = this.identifier
? `identifier="${XML.escape(this.identifier)}"`
: "";

const description = this.description
? `description="${this.description}"`
? `description="${XML.escape(this.description)}"`
: "";

const instructions = this.instructions
? `instructions="${this.instructions}"`
? `instructions="${XML.escape(this.instructions)}"`
: "";

const xml = `<specification ifcVersion="${[...this.ifcVersion].join(" ")}" ${name} ${identifier} ${description} ${instructions}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IDSFacetParameterName, IDSFacetParameter } from "../types";
import { XML } from "../../../../utils/xml";

export const getParameterXML = (
name: IDSFacetParameterName,
Expand All @@ -8,20 +9,20 @@ export const getParameterXML = (
if (!parameter) return parameterXML;

if (parameter.type === "simple") {
parameterXML = `<simpleValue>${parameter.parameter}</simpleValue>`;
parameterXML = `<simpleValue>${XML.escape(parameter.parameter)}</simpleValue>`;
}

if (parameter.type === "enumeration") {
const value = parameter.parameter;
parameterXML = `<xs:restriction base="xs:string">
${value.map((v) => `<xs:enumeration value="${v}" />`).join("\n")}
${value.map((v) => `<xs:enumeration value="${XML.escape(v)}" />`).join("\n")}
</xs:restriction>`;
}

if (parameter.type === "pattern") {
const value = parameter.parameter;
parameterXML = `<xs:restriction base="xs:string">
<xs:pattern value="${value}" />
<xs:pattern value="${XML.escape(value)}" />
</xs:restriction>`;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ModelIdDataMap,
ModelIdMap,
} from "../../../../fragments";
import { XML } from "../../../../utils/xml";

// https://github.com/buildingSMART/IDS/blob/development/Documentation/UserManual/attribute-facet.md

Expand All @@ -27,7 +28,7 @@ export class IDSAttribute extends IDSFacet {
if (type === "requirement") {
attributes += `cardinality="${this.cardinality}"`;
attributes += this.instructions
? `instructions="${this.instructions}"`
? `instructions="${XML.escape(this.instructions)}"`
: "";
}
return `<attribute ${attributes}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IDSFacetParameter, IDSItemCheckResult } from "../types";
import { IDSFacet } from "./Facet";
import { getParameterXML } from "../exporters/parameter";
import { ModelIdDataMap, ModelIdMap } from "../../../../fragments";
import { XML } from "../../../../utils/xml";

// https://github.com/buildingSMART/IDS/blob/development/Documentation/UserManual/classification-facet.md

Expand All @@ -25,7 +26,7 @@ export class IDSClassification extends IDSFacet {
attributes += `cardinality="${this.cardinality}"`;
attributes += this.uri ? `uri=${this.uri}` : "";
attributes += this.instructions
? `instructions="${this.instructions}"`
? `instructions="${XML.escape(this.instructions)}"`
: "";
}
return `<classification ${attributes}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ModelIdMap,
} from "../../../../fragments";
import { ModelIdMapUtils } from "../../../../utils";
import { XML } from "../../../../utils/xml";

// https://github.com/buildingSMART/IDS/blob/development/Documentation/UserManual/entity-facet.md

Expand All @@ -29,7 +30,7 @@ export class IDSEntity extends IDSFacet {
if (type === "requirement") {
attributes += `cardinality="${this.cardinality}"`;
attributes += this.instructions
? `instructions="${this.instructions}"`
? `instructions="${XML.escape(this.instructions)}"`
: "";
}
return `<entity ${attributes}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getParameterXML } from "../exporters/parameter";
import { IDSCheck, IDSFacetParameter, IDSItemCheckResult } from "../types";
import { IDSFacet } from "./Facet";
import { ModelIdMapUtils } from "../../../../utils";
import { XML } from "../../../../utils/xml";

// https://github.com/buildingSMART/IDS/blob/development/Documentation/UserManual/material-facet.md

Expand Down Expand Up @@ -59,7 +60,7 @@ export class IDSMaterial extends IDSFacet {
attributes += `cardinality="${this.cardinality}"`;
attributes += this.uri ? `uri=${this.uri}` : "";
attributes += this.instructions
? `instructions="${this.instructions}"`
? `instructions="${XML.escape(this.instructions)}"`
: "";
}
return `<material ${attributes}>
Expand Down
18 changes: 13 additions & 5 deletions packages/core/src/openbim/IDSSpecifications/src/facets/Property.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ModelIdMap,
} from "../../../../fragments";
import { ModelIdMapUtils } from "../../../../utils";
import { XML } from "../../../../utils/xml";

// https://github.com/buildingSMART/IDS/blob/development/Documentation/UserManual/property-facet.md

Expand Down Expand Up @@ -46,7 +47,7 @@ export class IDSProperty extends IDSFacet {
attributes += `cardinality="${this.cardinality}"`;
attributes += this.uri ? `uri=${this.uri}` : "";
attributes += this.instructions
? `instructions="${this.instructions}"`
? `instructions="${XML.escape(this.instructions)}"`
: "";
}
return `<property ${dataTypeXML} ${attributes}>
Expand Down Expand Up @@ -84,7 +85,10 @@ export class IDSProperty extends IDSFacet {
// Matching sets, by localId: their name and the property names that
// matched. A set attached to a type (IfcTypeObject.HasPropertySets)
// has no DefinesOccurrence, so its elements are found from the type.
const matchedSets = new Map<number, { name: string; props: Set<string> }>();
const matchedSets = new Map<
number,
{ name: string; props: Set<string> }
>();

for (const set of data) {
const definesOccurrence = set.DefinesOccurrence ?? set.DefinesOcurrence;
Expand Down Expand Up @@ -220,7 +224,8 @@ export class IDSProperty extends IDSFacet {
for (const type of types) {
if (!Array.isArray(type.HasPropertySets)) continue;
const sets = type.HasPropertySets.flatMap((set) => {
const id = set._localId && "value" in set._localId ? set._localId.value : null;
const id =
set._localId && "value" in set._localId ? set._localId.value : null;
const matched = matchedSets.get(id as number);
return matched ? [matched] : [];
});
Expand Down Expand Up @@ -260,12 +265,15 @@ export class IDSProperty extends IDSFacet {
const props = list ? set[list] : undefined;
if (!Array.isArray(props)) return false;
return props.some(
(prop) => prop.Name && "value" in prop.Name && prop.Name.value === propName,
(prop) =>
prop.Name && "value" in prop.Name && prop.Name.value === propName,
);
});
const inherits = inherited
.get(id)!
.some((set) => [...set.props].some((prop) => !overrides(set.name, prop)));
.some((set) =>
[...set.props].some((prop) => !overrides(set.name, prop)),
);
if (inherits) applies.push(id);
}
ModelIdMapUtils.append(collector, modelId, ...applies);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { afterAll, describe, expect, it } from "vitest";
import type { Components } from "../../../index";
import { loadSourceBundle } from "../../../../tests/source-bundle";

// Needs the real `Components`: see `loadSourceBundle`.
const OBC = await loadSourceBundle();

const ids = `<?xml version="1.0" encoding="UTF-8"?>
<ids:ids xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:ids="http://standards.buildingsmart.org/IDS">
<ids:info><ids:title>Entities</ids:title></ids:info>
<ids:specifications>
<ids:specification name="R&amp;D walls" ifcVersion="IFC4">
<ids:applicability minOccurs="0" maxOccurs="unbounded">
<ids:entity><ids:name><ids:simpleValue>IFCWALL</ids:simpleValue></ids:name></ids:entity>
</ids:applicability>
<ids:requirements>
<ids:property dataType="IFCLABEL">
<ids:propertySet><ids:simpleValue>Pset_R&amp;D</ids:simpleValue></ids:propertySet>
<ids:baseName><ids:simpleValue>Owner</ids:simpleValue></ids:baseName>
<ids:value><ids:simpleValue>Smith &amp; Sons &lt;Ltd&gt;</ids:simpleValue></ids:value>
</ids:property>
</ids:requirements>
</ids:specification>
</ids:specifications>
</ids:ids>`;

describe("IDSSpecifications.load", () => {
const components: Components = new OBC.Components();
afterAll(() => components.dispose());

it("decodes XML entities in names and facet values", () => {
const [spec] = components.get(OBC.IDSSpecifications).load(ids);
expect(spec.name).toBe("R&D walls");
const [property] = [...spec.requirements] as InstanceType<
typeof OBC.IDSProperty
>[];
expect(property.propertySet.parameter).toBe("Pset_R&D");
expect(property.value?.parameter).toBe("Smith & Sons <Ltd>");
});

it("round-trips them through export", () => {
const specs = components.get(OBC.IDSSpecifications);
specs.list.clear();
specs.load(ids);
const xml = specs.export({ title: "Entities" });
specs.list.clear();
const [spec] = specs.load(xml);
expect(spec.name).toBe("R&D walls");
const [property] = [...spec.requirements] as InstanceType<
typeof OBC.IDSProperty
>[];
expect(property.value?.parameter).toBe("Smith & Sons <Ltd>");
});
});
13 changes: 13 additions & 0 deletions packages/core/src/utils/xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ export class XML {
trimValues: true,
});

/**
* Escapes a value for XML text or a double-quoted attribute, so a value
* holding `&`, `<`, `>` or quotes is written as data, not markup.
*/
static escape(value: unknown) {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}

static builder = new XMLBuilder({
attributeNamePrefix: "$",
ignoreAttributes: false,
Expand Down
Loading