Skip to main content

openbim_ids/
write.rs

1//! Writing IDS 1.0 documents.
2//!
3//! [`to_string`] and [`to_writer`] turn the typed [`Ids`] model into an IDS
4//! 1.0 document that validates against `ids.xsd` 1.0.0, or refuse it with a
5//! [`WriteError`] naming the offending part of the model. Nothing is written
6//! that the schema would reject: the writer checks the whole model before a
7//! byte reaches the output.
8//!
9//! # What the output looks like
10//!
11//! - The root declares the IDS namespace as default, `xs` for XML Schema and
12//!   `xsi` with the 1.0 `xsi:schemaLocation`, so the reader records the
13//!   revision as [`Detected::Declared`].
14//! - Elements follow the order `ids.xsd` requires. Requirement facets keep
15//!   the model's order, since the schema lets them interleave.
16//! - A [`Value`] is written as `<simpleValue>` or `<xs:restriction>`, exactly
17//!   as it holds it.
18//! - A requirement facet always states its `@cardinality`; the applicability
19//!   writes `minOccurs`/`maxOccurs` only where they differ from the schema
20//!   default of `1`.
21//! - Optional elements and attributes are omitted when `None`.
22//! - Two-space indentation and `\n` line ends. Character data is never
23//!   indented, so a value with surrounding whitespace reads back unchanged.
24//!
25//! # What is refused
26//!
27//! Beyond what the schema forbids, the writer refuses two schema-valid shapes
28//! that express nothing: an applicability without facets, which would make
29//! every object applicable by accident, and an `<xs:restriction>` without
30//! facets, which accepts any value while looking like a constraint.
31//!
32//! Reading what this module writes gives back the model it was given; see
33//! [`to_string`].
34
35use core::fmt;
36
37use openbim_core::Detected;
38
39use crate::model::{
40    Applicability, Entity, Facet, Info, Requirement, Requirements, Restriction, Specification,
41    Value,
42};
43use crate::read::{is_author, is_xs_date, XSD_NAMESPACE, XSD_TYPES};
44use crate::{Ids, IdsVersion, Occurrence, NAMESPACE};
45
46const XSI_NAMESPACE: &str = "http://www.w3.org/2001/XMLSchema-instance";
47
48/// Where the IDS 1.0 schema is published, as `xsi:schemaLocation` names it.
49pub const SCHEMA_LOCATION: &str = "http://standards.buildingsmart.org/IDS/1.0/ids.xsd";
50
51/// Why a model was refused, and where in it.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct WriteError {
54    kind: WriteErrorKind,
55    location: String,
56}
57
58impl WriteError {
59    /// What was wrong.
60    #[must_use]
61    pub fn kind(&self) -> &WriteErrorKind {
62        &self.kind
63    }
64
65    /// The path to the offending part of the model, such as
66    /// `specifications[0]/requirements/facets[2]/value`. Indices are
67    /// 0-based positions in the model's vectors.
68    #[must_use]
69    pub fn location(&self) -> &str {
70        &self.location
71    }
72}
73
74impl fmt::Display for WriteError {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        if self.location.is_empty() {
77            write!(f, "{}", self.kind)
78        } else {
79            write!(f, "{}: {}", self.location, self.kind)
80        }
81    }
82}
83
84impl std::error::Error for WriteError {}
85
86/// The kinds of [`WriteError`].
87#[derive(Debug, Clone, PartialEq, Eq)]
88#[non_exhaustive]
89pub enum WriteErrorKind {
90    /// The model is not IDS 1.0, or its version is a conflict.
91    UnsupportedVersion(Detected<IdsVersion>),
92    /// The schema requires at least one specification.
93    NoSpecifications,
94    /// A specification lists no IFC release; `@ifcVersion` needs one.
95    NoIfcVersions,
96    /// An applicability without facets.
97    EmptyApplicability,
98    /// Applicability facets out of the order the schema requires: at most one
99    /// `entity`, then `partOf`, `classification`, `attribute`, `property`,
100    /// `material`.
101    ApplicabilityOrder {
102        /// The facet that stands where it may not.
103        found: &'static str,
104    },
105    /// A requirement attribute the schema does not allow on this facet.
106    AttributeNotAllowed {
107        /// The facet's element name.
108        facet: &'static str,
109        /// The attribute.
110        attribute: &'static str,
111    },
112    /// An occurrence that this facet cannot state: an `entity` requirement is
113    /// always required, a `partOf` requirement cannot be optional.
114    OccurrenceNotAllowed {
115        /// The facet's element name.
116        facet: &'static str,
117        /// The refused occurrence.
118        occurrence: Occurrence,
119    },
120    /// An `<xs:restriction>` without facets.
121    EmptyRestriction,
122    /// A value outside the lexical space the schema allows.
123    InvalidValue {
124        /// The value.
125        found: String,
126        /// What the schema allows.
127        expected: &'static str,
128    },
129    /// A character that XML 1.0 cannot represent, even escaped.
130    InvalidCharacter(char),
131    /// Writing to the output failed.
132    Io {
133        /// The I/O error kind.
134        kind: std::io::ErrorKind,
135        /// The I/O error message.
136        message: String,
137    },
138}
139
140impl fmt::Display for WriteErrorKind {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        match self {
143            WriteErrorKind::UnsupportedVersion(detected) => match detected {
144                Detected::Conflict { declared, observed } => write!(
145                    f,
146                    "the version is a conflict (declared {declared}, observed {observed}); only 1.0 is written"
147                ),
148                Detected::Declared(version) | Detected::Inferred(version) => {
149                    write!(f, "the model is IDS {version}; only 1.0 is written")
150                }
151            },
152            WriteErrorKind::NoSpecifications => {
153                f.write_str("IDS 1.0 requires at least one specification")
154            }
155            WriteErrorKind::NoIfcVersions => {
156                f.write_str("a specification must list at least one IFC release")
157            }
158            WriteErrorKind::EmptyApplicability => {
159                f.write_str("an applicability without facets matches every object")
160            }
161            WriteErrorKind::ApplicabilityOrder { found } => write!(
162                f,
163                "<{found}> is out of order; applicability takes at most one entity, then partOf, \
164                 classification, attribute, property and material facets"
165            ),
166            WriteErrorKind::AttributeNotAllowed { facet, attribute } => {
167                write!(f, "a <{facet}> requirement cannot carry @{attribute}")
168            }
169            WriteErrorKind::OccurrenceNotAllowed { facet, occurrence } => {
170                write!(f, "a <{facet}> requirement cannot be {occurrence}")
171            }
172            WriteErrorKind::EmptyRestriction => {
173                f.write_str("an xs:restriction without facets constrains nothing")
174            }
175            WriteErrorKind::InvalidValue { found, expected } => {
176                write!(f, "{found:?} is not {expected}")
177            }
178            WriteErrorKind::InvalidCharacter(c) => {
179                write!(f, "U+{:04X} cannot be represented in XML 1.0", u32::from(*c))
180            }
181            WriteErrorKind::Io { message, .. } => write!(f, "writing failed: {message}"),
182        }
183    }
184}
185
186/// Writes an IDS 1.0 document.
187///
188/// For every model `x` that [`read`](crate::read) returns,
189/// `from_str(&to_string(&x)?)` is `x` again, and the text validates against
190/// `ids.xsd` 1.0.0.
191///
192/// ```
193/// use openbim_ids::{Entity, Ids, IfcVersion, Info, Specification};
194///
195/// let mut spec = Specification::new("Walls", [IfcVersion::Ifc4]);
196/// spec.applicability.facets.push(Entity::new("IFCWALL").into());
197/// let mut ids = Ids::new(Info::new("Example"));
198/// ids.specifications.push(spec);
199///
200/// let xml = openbim_ids::to_string(&ids).unwrap();
201/// assert_eq!(openbim_ids::from_str(&xml).unwrap(), ids);
202/// ```
203///
204/// # Errors
205///
206/// Returns a [`WriteError`] when the model is not IDS 1.0 or holds something
207/// IDS 1.0 cannot express; see the [module documentation](self).
208pub fn to_string(ids: &Ids) -> Result<String, WriteError> {
209    let mut writer = Writer::default();
210    writer.ids(ids)?;
211    Ok(writer.out)
212}
213
214/// Writes an IDS 1.0 document to `output` as UTF-8.
215///
216/// The whole document is checked before anything is written, so a refused
217/// model leaves `output` untouched.
218///
219/// # Errors
220///
221/// The errors of [`to_string`], and [`WriteErrorKind::Io`] when `output`
222/// fails.
223pub fn to_writer(ids: &Ids, mut output: impl std::io::Write) -> Result<(), WriteError> {
224    let text = to_string(ids)?;
225    output
226        .write_all(text.as_bytes())
227        .and_then(|()| output.flush())
228        .map_err(|error| WriteError {
229            kind: WriteErrorKind::Io {
230                kind: error.kind(),
231                message: error.to_string(),
232            },
233            location: String::new(),
234        })
235}
236
237type Result<T, E = WriteError> = core::result::Result<T, E>;
238
239#[derive(Default)]
240struct Writer {
241    out: String,
242    depth: usize,
243    path: Vec<String>,
244}
245
246/// An attribute to write, `None` meaning absent.
247type Attr<'a> = (&'static str, Option<&'a str>);
248
249impl Writer {
250    fn error(&self, kind: WriteErrorKind) -> WriteError {
251        WriteError {
252            kind,
253            location: self.path.join("/"),
254        }
255    }
256
257    fn error_at(&self, step: &str, kind: WriteErrorKind) -> WriteError {
258        let mut error = self.error(kind);
259        if !error.location.is_empty() {
260            error.location.push('/');
261        }
262        error.location.push_str(step);
263        error
264    }
265
266    fn at<T>(&mut self, step: String, f: impl FnOnce(&mut Self) -> Result<T>) -> Result<T> {
267        self.path.push(step);
268        let result = f(self)?;
269        self.path.pop();
270        Ok(result)
271    }
272
273    fn indent(&mut self) {
274        for _ in 0..self.depth {
275            self.out.push_str("  ");
276        }
277    }
278
279    fn open_tag(&mut self, name: &str, attributes: &[Attr<'_>]) -> Result<()> {
280        self.indent();
281        self.out.push('<');
282        self.out.push_str(name);
283        for (attribute, value) in attributes {
284            if let Some(value) = value {
285                self.path.push(format!("@{attribute}"));
286                check_chars(value).map_err(|c| self.error(WriteErrorKind::InvalidCharacter(c)))?;
287                self.path.pop();
288                self.out.push(' ');
289                self.out.push_str(attribute);
290                self.out.push_str("=\"");
291                escape(&mut self.out, value, true);
292                self.out.push('"');
293            }
294        }
295        Ok(())
296    }
297
298    /// `<name attrs>` followed by children written by `f`, then `</name>`.
299    fn element(
300        &mut self,
301        name: &str,
302        attributes: &[Attr<'_>],
303        f: impl FnOnce(&mut Self) -> Result<()>,
304    ) -> Result<()> {
305        self.open_tag(name, attributes)?;
306        self.out.push_str(">\n");
307        self.depth += 1;
308        f(self)?;
309        self.depth -= 1;
310        self.indent();
311        self.out.push_str("</");
312        self.out.push_str(name);
313        self.out.push_str(">\n");
314        Ok(())
315    }
316
317    fn empty(&mut self, name: &str, attributes: &[Attr<'_>]) -> Result<()> {
318        self.open_tag(name, attributes)?;
319        self.out.push_str("/>\n");
320        Ok(())
321    }
322
323    fn text(&mut self, name: &'static str, text: &str) -> Result<()> {
324        self.at(name.to_owned(), |w| {
325            check_chars(text).map_err(|c| w.error(WriteErrorKind::InvalidCharacter(c)))?;
326            w.open_tag(name, &[])?;
327            w.out.push('>');
328            escape(&mut w.out, text, false);
329            w.out.push_str("</");
330            w.out.push_str(name);
331            w.out.push_str(">\n");
332            Ok(())
333        })
334    }
335
336    fn optional_text(&mut self, name: &'static str, text: Option<&str>) -> Result<()> {
337        text.map_or(Ok(()), |text| self.text(name, text))
338    }
339
340    fn ids(&mut self, ids: &Ids) -> Result<()> {
341        if ids.version.resolved() != Some(IdsVersion::Ids1_0) {
342            return Err(self.error(WriteErrorKind::UnsupportedVersion(ids.version.clone())));
343        }
344        if ids.specifications.is_empty() {
345            return Err(self.error(WriteErrorKind::NoSpecifications));
346        }
347        self.out
348            .push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
349        let location = format!("{NAMESPACE} {SCHEMA_LOCATION}");
350        self.element(
351            "ids",
352            &[
353                ("xmlns", Some(NAMESPACE)),
354                ("xmlns:xs", Some(XSD_NAMESPACE)),
355                ("xmlns:xsi", Some(XSI_NAMESPACE)),
356                ("xsi:schemaLocation", Some(&location)),
357            ],
358            |w| {
359                w.at("info".into(), |w| w.info(&ids.info))?;
360                w.element("specifications", &[], |w| {
361                    for (index, specification) in ids.specifications.iter().enumerate() {
362                        w.at(format!("specifications[{index}]"), |w| {
363                            w.specification(specification)
364                        })?;
365                    }
366                    Ok(())
367                })
368            },
369        )
370    }
371
372    fn info(&mut self, info: &Info) -> Result<()> {
373        if let Some(author) = &info.author {
374            if !is_author(author) {
375                return Err(self.error_at(
376                    "author",
377                    WriteErrorKind::InvalidValue {
378                        found: author.clone(),
379                        expected: "an e-mail address (pattern [^@]+@[^\\.]+\\..+)",
380                    },
381                ));
382            }
383        }
384        if let Some(date) = &info.date {
385            if !is_xs_date(date) {
386                return Err(self.error_at(
387                    "date",
388                    WriteErrorKind::InvalidValue {
389                        found: date.clone(),
390                        expected: "an xs:date such as 2024-06-01",
391                    },
392                ));
393            }
394        }
395        self.element("info", &[], |w| {
396            w.text("title", &info.title)?;
397            w.optional_text("copyright", info.copyright.as_deref())?;
398            w.optional_text("version", info.version.as_deref())?;
399            w.optional_text("description", info.description.as_deref())?;
400            w.optional_text("author", info.author.as_deref())?;
401            w.optional_text("date", info.date.as_deref())?;
402            w.optional_text("purpose", info.purpose.as_deref())?;
403            w.optional_text("milestone", info.milestone.as_deref())
404        })
405    }
406
407    fn specification(&mut self, specification: &Specification) -> Result<()> {
408        if specification.ifc_versions.is_empty() {
409            return Err(self.error(WriteErrorKind::NoIfcVersions));
410        }
411        let versions = specification
412            .ifc_versions
413            .iter()
414            .map(|v| v.as_str())
415            .collect::<Vec<_>>()
416            .join(" ");
417        self.element(
418            "specification",
419            &[
420                ("name", Some(&specification.name)),
421                ("ifcVersion", Some(&versions)),
422                ("identifier", specification.identifier.as_deref()),
423                ("description", specification.description.as_deref()),
424                ("instructions", specification.instructions.as_deref()),
425            ],
426            |w| {
427                w.at("applicability".into(), |w| {
428                    w.applicability(&specification.applicability)
429                })?;
430                if let Some(requirements) = &specification.requirements {
431                    w.at("requirements".into(), |w| w.requirements(requirements))?;
432                }
433                Ok(())
434            },
435        )
436    }
437
438    fn applicability(&mut self, applicability: &Applicability) -> Result<()> {
439        if applicability.facets.is_empty() {
440            return Err(self.error(WriteErrorKind::EmptyApplicability));
441        }
442        let mut rank = 0;
443        for (index, facet) in applicability.facets.iter().enumerate() {
444            let this = applicability_rank(facet);
445            // Only the entity may not repeat; every other kind is unbounded.
446            if this < rank || (index > 0 && this == 0) {
447                return Err(self.error_at(
448                    &format!("facets[{index}]"),
449                    WriteErrorKind::ApplicabilityOrder {
450                        found: facet.kind(),
451                    },
452                ));
453            }
454            rank = this;
455        }
456        let min = (applicability.min_occurs != 1).then(|| applicability.min_occurs.to_string());
457        let max = match applicability.max_occurs {
458            Some(1) => None,
459            Some(max) => Some(max.to_string()),
460            None => Some("unbounded".to_owned()),
461        };
462        self.element(
463            "applicability",
464            &[("minOccurs", min.as_deref()), ("maxOccurs", max.as_deref())],
465            |w| {
466                for (index, facet) in applicability.facets.iter().enumerate() {
467                    w.at(format!("facets[{index}]"), |w| w.facet(facet, None))?;
468                }
469                Ok(())
470            },
471        )
472    }
473
474    fn requirements(&mut self, requirements: &Requirements) -> Result<()> {
475        let attributes = [("description", requirements.description.as_deref())];
476        if requirements.facets.is_empty() {
477            return self.empty("requirements", &attributes);
478        }
479        self.element("requirements", &attributes, |w| {
480            for (index, requirement) in requirements.facets.iter().enumerate() {
481                w.at(format!("facets[{index}]"), |w| {
482                    w.facet(&requirement.facet, Some(requirement))
483                })?;
484            }
485            Ok(())
486        })
487    }
488
489    /// Writes a facet; `requirement` carries the requirement-only attributes.
490    fn facet(&mut self, facet: &Facet, requirement: Option<&Requirement>) -> Result<()> {
491        let kind = facet.kind();
492        let mut cardinality = None;
493        let mut uri = None;
494        let mut instructions = None;
495        if let Some(requirement) = requirement {
496            let occurrence = requirement.occurrence;
497            let refused = match facet {
498                Facet::Entity(_) => occurrence != Occurrence::Required,
499                Facet::PartOf(_) => occurrence == Occurrence::Optional,
500                _ => false,
501            };
502            if refused {
503                return Err(self.error(WriteErrorKind::OccurrenceNotAllowed {
504                    facet: kind,
505                    occurrence,
506                }));
507            }
508            if !matches!(facet, Facet::Entity(_)) {
509                cardinality = Some(occurrence.as_cardinality());
510            }
511            if requirement.uri.is_some() {
512                if !matches!(
513                    facet,
514                    Facet::Classification(_) | Facet::Property(_) | Facet::Material(_)
515                ) {
516                    return Err(self.error(WriteErrorKind::AttributeNotAllowed {
517                        facet: kind,
518                        attribute: "uri",
519                    }));
520                }
521                uri = requirement.uri.as_deref();
522            }
523            instructions = requirement.instructions.as_deref();
524        }
525        match facet {
526            Facet::Entity(entity) => {
527                self.element("entity", &[("instructions", instructions)], |w| {
528                    w.entity_content(entity)
529                })
530            }
531            Facet::PartOf(part_of) => self.element(
532                "partOf",
533                &[
534                    ("relation", part_of.relation.map(|r| r.as_str())),
535                    ("cardinality", cardinality),
536                    ("instructions", instructions),
537                ],
538                |w| {
539                    w.at("entity".into(), |w| {
540                        w.element("entity", &[], |w| w.entity_content(&part_of.entity))
541                    })
542                },
543            ),
544            Facet::Classification(classification) => self.element(
545                "classification",
546                &[
547                    ("uri", uri),
548                    ("cardinality", cardinality),
549                    ("instructions", instructions),
550                ],
551                |w| {
552                    w.optional_value("value", classification.value.as_ref())?;
553                    w.value("system", &classification.system)
554                },
555            ),
556            Facet::Attribute(attribute) => self.element(
557                "attribute",
558                &[("cardinality", cardinality), ("instructions", instructions)],
559                |w| {
560                    w.value("name", &attribute.name)?;
561                    w.optional_value("value", attribute.value.as_ref())
562                },
563            ),
564            Facet::Property(property) => {
565                if let Some(data_type) = &property.data_type {
566                    if data_type.is_empty() || !data_type.bytes().all(|b| b.is_ascii_uppercase()) {
567                        return Err(self.error_at(
568                            "@dataType",
569                            WriteErrorKind::InvalidValue {
570                                found: data_type.clone(),
571                                expected: "an upper-case name matching [A-Z]+",
572                            },
573                        ));
574                    }
575                }
576                self.element(
577                    "property",
578                    &[
579                        ("dataType", property.data_type.as_deref()),
580                        ("uri", uri),
581                        ("cardinality", cardinality),
582                        ("instructions", instructions),
583                    ],
584                    |w| {
585                        w.value("propertySet", &property.property_set)?;
586                        w.value("baseName", &property.base_name)?;
587                        w.optional_value("value", property.value.as_ref())
588                    },
589                )
590            }
591            Facet::Material(material) => {
592                let attributes = [
593                    ("uri", uri),
594                    ("cardinality", cardinality),
595                    ("instructions", instructions),
596                ];
597                match &material.value {
598                    Some(value) => {
599                        self.element("material", &attributes, |w| w.value("value", value))
600                    }
601                    None => self.empty("material", &attributes),
602                }
603            }
604        }
605    }
606
607    fn entity_content(&mut self, entity: &Entity) -> Result<()> {
608        self.value("name", &entity.name)?;
609        self.optional_value("predefinedType", entity.predefined_type.as_ref())
610    }
611
612    fn optional_value(&mut self, name: &'static str, value: Option<&Value>) -> Result<()> {
613        value.map_or(Ok(()), |value| self.value(name, value))
614    }
615
616    fn value(&mut self, name: &'static str, value: &Value) -> Result<()> {
617        self.at(name.to_owned(), |w| {
618            w.element(name, &[], |w| match value {
619                Value::Simple(text) => w.text("simpleValue", text),
620                Value::Restriction(restriction) => {
621                    w.at("xs:restriction".into(), |w| w.restriction(restriction))
622                }
623            })
624        })
625    }
626
627    fn restriction(&mut self, restriction: &Restriction) -> Result<()> {
628        if !XSD_TYPES.contains(&restriction.base.as_str()) {
629            return Err(self.error_at(
630                "@base",
631                WriteErrorKind::InvalidValue {
632                    found: restriction.base.clone(),
633                    expected:
634                        "the local name of an XML Schema built-in simple type, such as string",
635                },
636            ));
637        }
638        if restriction.total_digits == Some(0) {
639            return Err(self.error_at(
640                "xs:totalDigits",
641                WriteErrorKind::InvalidValue {
642                    found: "0".into(),
643                    expected: "a positive integer",
644                },
645            ));
646        }
647        let counts = [
648            ("xs:length", restriction.length),
649            ("xs:minLength", restriction.min_length),
650            ("xs:maxLength", restriction.max_length),
651            ("xs:totalDigits", restriction.total_digits),
652            ("xs:fractionDigits", restriction.fraction_digits),
653        ];
654        let bounds = [
655            ("xs:minInclusive", restriction.min_inclusive.as_deref()),
656            ("xs:maxInclusive", restriction.max_inclusive.as_deref()),
657            ("xs:minExclusive", restriction.min_exclusive.as_deref()),
658            ("xs:maxExclusive", restriction.max_exclusive.as_deref()),
659        ];
660        let has_facets = !restriction.enumeration.is_empty()
661            || !restriction.patterns.is_empty()
662            || counts.iter().any(|(_, v)| v.is_some())
663            || bounds.iter().any(|(_, v)| v.is_some());
664        if !has_facets {
665            return Err(self.error(WriteErrorKind::EmptyRestriction));
666        }
667        let base = format!("xs:{}", restriction.base);
668        self.element("xs:restriction", &[("base", Some(&base))], |w| {
669            for value in &restriction.enumeration {
670                w.at("xs:enumeration".into(), |w| {
671                    w.empty("xs:enumeration", &[("value", Some(value))])
672                })?;
673            }
674            for value in &restriction.patterns {
675                w.at("xs:pattern".into(), |w| {
676                    w.empty("xs:pattern", &[("value", Some(value))])
677                })?;
678            }
679            for (name, value) in bounds {
680                if let Some(value) = value {
681                    w.at(name.into(), |w| w.empty(name, &[("value", Some(value))]))?;
682                }
683            }
684            for (name, value) in counts {
685                if let Some(value) = value {
686                    w.empty(name, &[("value", Some(&value.to_string()))])?;
687                }
688            }
689            Ok(())
690        })
691    }
692}
693
694/// The position of a facet kind in the applicability sequence.
695fn applicability_rank(facet: &Facet) -> u8 {
696    match facet {
697        Facet::Entity(_) => 0,
698        Facet::PartOf(_) => 1,
699        Facet::Classification(_) => 2,
700        Facet::Attribute(_) => 3,
701        Facet::Property(_) => 4,
702        Facet::Material(_) => 5,
703    }
704}
705
706/// The first character XML 1.0 cannot carry, if any.
707fn check_chars(text: &str) -> Result<(), char> {
708    match text.chars().find(|&c| {
709        !matches!(c, '\t' | '\n' | '\r' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..)
710    }) {
711        Some(c) => Err(c),
712        None => Ok(()),
713    }
714}
715
716/// Escapes `text` so that a reader returns it unchanged.
717///
718/// Attribute values additionally escape the quote and the whitespace that
719/// attribute-value normalization would turn into spaces; character data
720/// escapes `\r`, which end-of-line handling would drop.
721fn escape(out: &mut String, text: &str, attribute: bool) {
722    for c in text.chars() {
723        match c {
724            '&' => out.push_str("&amp;"),
725            '<' => out.push_str("&lt;"),
726            '>' => out.push_str("&gt;"),
727            '\r' => out.push_str("&#13;"),
728            '"' if attribute => out.push_str("&quot;"),
729            '\t' if attribute => out.push_str("&#9;"),
730            '\n' if attribute => out.push_str("&#10;"),
731            c => out.push(c),
732        }
733    }
734}