use crate::error::XmlError;
use crate::XmlCodec;
use ifc_model::{Model, Value};
use std::fmt::Write as _;
const XSI_NAMESPACE: &str = "http://www.w3.org/2001/XMLSchema-instance";
fn reject_non_finite_reals(model: &Model) -> Result<(), XmlError> {
for (id, entity) in model.iter() {
for (slot, value) in entity.attributes.iter().enumerate() {
if let Some(non_finite) = first_non_finite_real(value) {
let path = format!("/ifcXML/{}[@id='i{}']/a{}", entity.type_name, id.0, slot);
return Err(XmlError::InvalidScalar {
kind: "real".into(),
value: non_finite.to_string(),
}
.at(path));
}
}
}
Ok(())
}
fn first_non_finite_real(value: &Value) -> Option<f64> {
match value {
Value::Real(value) if !value.is_finite() => Some(*value),
Value::List(values) => values.iter().find_map(first_non_finite_real),
Value::Typed { value, .. } => first_non_finite_real(value),
_ => None,
}
}
pub fn write(codec: &XmlCodec, model: &Model) -> Result<Vec<u8>, XmlError> {
reject_non_finite_reals(model)?;
let mut out = String::with_capacity(model.len() * 96);
let schema_token = model.header().schema.first().cloned().unwrap_or_default();
let namespace = if let Some(profile) = codec.profile() {
if schema_token != profile.schema_token() {
return Err(XmlError::Profile {
expected: profile.schema_token(),
found: (!schema_token.is_empty()).then_some(schema_token),
});
}
profile.namespace()
} else {
"http://www.buildingsmart-tech.org/ifcXML/IFC4/final"
};
out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
write!(
out,
"<ifcXML xmlns=\"{namespace}\" xmlns:xsi=\"{XSI_NAMESPACE}\""
)
.map_err(fmt_err)?;
write_attr(&mut out, "schema", &schema_token);
out.push_str(">\n");
write_header(&mut out, model);
for (id, entity) in model.iter() {
let names = attribute_names(codec, &entity.type_name, entity.attributes.len());
write!(out, " <{}", entity.type_name).map_err(fmt_err)?;
write!(out, " id=\"i{}\"", id.0).map_err(fmt_err)?;
let mut children: Vec<(usize, &Value)> = Vec::new();
for (i, value) in entity.attributes.iter().enumerate() {
match scalar_text(value) {
Some(text) => write_attr(&mut out, &names[i], &text),
None => children.push((i, value)),
}
}
if children.is_empty() {
out.push_str("/>\n");
} else {
out.push_str(">\n");
for (i, value) in children {
write_child(&mut out, &names[i], value, 2)?;
}
writeln!(out, " </{}>", entity.type_name).map_err(fmt_err)?;
}
}
out.push_str("</ifcXML>\n");
Ok(out.into_bytes())
}
fn fmt_err(e: std::fmt::Error) -> XmlError {
XmlError::Write(e.to_string())
}
fn format_real(r: f64) -> String {
if r == r.trunc() && r.is_finite() && r.abs() < 1e15 {
format!("{r:.1}")
} else {
let s = format!("{r}");
if s.contains('.') || s.contains('e') || s.contains('E') {
s
} else {
format!("{s}.0")
}
}
}
fn attribute_names(codec: &XmlCodec, type_name: &str, count: usize) -> Vec<String> {
#[cfg(feature = "schema")]
if let Some(schema) = codec.schema() {
let names = schema.attribute_names(type_name);
if names.len() >= count {
return names.iter().take(count).map(|s| s.to_string()).collect();
}
}
let _ = (codec, type_name);
(0..count).map(|i| format!("a{i}")).collect()
}
fn write_header(out: &mut String, model: &Model) {
let h = model.header();
out.push_str(" <header>\n");
push_element(out, "name", &h.name);
push_element(out, "time_stamp", &h.time_stamp);
push_element(out, "preprocessor_version", &h.preprocessor_version);
push_element(out, "originating_system", &h.originating_system);
push_element(out, "authorization", &h.authorization);
for a in &h.author {
push_element(out, "author", a);
}
for o in &h.organization {
push_element(out, "organization", o);
}
for d in &h.description {
push_element(out, "description", d);
}
out.push_str(" </header>\n");
}
fn push_element(out: &mut String, tag: &str, text: &str) {
if text.is_empty() {
return;
}
out.push_str(" <");
out.push_str(tag);
out.push('>');
escape_into(out, text);
out.push_str("</");
out.push_str(tag);
out.push_str(">\n");
}
fn scalar_text(value: &Value) -> Option<String> {
match value {
Value::Text(s) => (!looks_numeric(s)).then(|| s.to_string()),
Value::Integer(i) => Some(i.to_string()),
Value::Real(r) => Some(format_real(*r)),
Value::Ref(id) => Some(format!("i{}", id.0)),
Value::Null | Value::Derived | Value::Enum(_) => None,
Value::Bool(_) | Value::LogicalUnknown | Value::Binary(_) => None,
Value::List(_) | Value::Typed { .. } => None,
}
}
fn looks_numeric(text: &str) -> bool {
if text
.strip_prefix('i')
.is_some_and(|r| r.parse::<u64>().is_ok())
{
return true;
}
text.parse::<i64>().is_ok() || text.parse::<f64>().is_ok()
}
fn write_child(out: &mut String, name: &str, value: &Value, depth: usize) -> Result<(), XmlError> {
let pad = " ".repeat(depth);
match value {
Value::Null => {
writeln!(out, "{pad}<{name} xsi:nil=\"true\"/>").map_err(fmt_err)?;
}
Value::Derived => {
writeln!(out, "{pad}<{name} derived=\"true\"/>").map_err(fmt_err)?;
}
Value::Enum(e) => {
write!(out, "{pad}<{name} kind=\"enum\">").map_err(fmt_err)?;
escape_into(out, e);
writeln!(out, "</{name}>").map_err(fmt_err)?;
}
Value::Binary(b) => {
write!(out, "{pad}<{name} kind=\"binary\">").map_err(fmt_err)?;
escape_into(out, b);
writeln!(out, "</{name}>").map_err(fmt_err)?;
}
Value::Bool(b) => {
let text = if *b { "true" } else { "false" };
writeln!(out, "{pad}<{name} kind=\"logical\">{text}</{name}>").map_err(fmt_err)?;
}
Value::LogicalUnknown => {
writeln!(out, "{pad}<{name} kind=\"logical\">unknown</{name}>").map_err(fmt_err)?;
}
Value::Typed { type_name, value } => {
writeln!(out, "{pad}<{name} kind=\"typed\" type=\"{type_name}\">").map_err(fmt_err)?;
write_child(out, "value", value, depth + 1)?;
writeln!(out, "{pad}</{name}>").map_err(fmt_err)?;
}
Value::List(items) => {
writeln!(out, "{pad}<{name} kind=\"list\">").map_err(fmt_err)?;
for item in items {
write_child(out, "item", item, depth + 1)?;
}
writeln!(out, "{pad}</{name}>").map_err(fmt_err)?;
}
scalar => {
let (kind, text) = match scalar {
Value::Text(s) => ("string", s.to_string()),
Value::Integer(i) => ("integer", i.to_string()),
Value::Real(r) => ("real", format_real(*r)),
Value::Ref(id) => ("ref", format!("i{}", id.0)),
other => ("string", scalar_text(other).unwrap_or_default()),
};
write!(out, "{pad}<{name} kind=\"{kind}\">").map_err(fmt_err)?;
escape_into(out, &text);
writeln!(out, "</{name}>").map_err(fmt_err)?;
}
}
Ok(())
}
fn write_attr(out: &mut String, name: &str, value: &str) {
out.push(' ');
out.push_str(name);
out.push_str("=\"");
escape_into(out, value);
out.push('"');
}
fn escape_into(out: &mut String, text: &str) {
for c in text.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
c => out.push(c),
}
}
}