Skip to main content

fml_xml/
read.rs

1//! XML → JSON of the logical model.
2
3use std::collections::HashMap;
4
5use roxmltree::{Document as XmlDocument, Node, ParsingOptions};
6use serde_json::{Map, Number, Value};
7
8use crate::datetime;
9use crate::model::{Element, Model, Representation, Root, TypeDef};
10use crate::{Error, Issue, IssueKind, Position, TYPE_KEY, XML_NS, XSI_NS};
11
12/// A document read from XML.
13#[derive(Debug, Clone)]
14pub struct Document {
15    /// The content as JSON of the logical model: property names are the model's element names,
16    /// attributes are primitives, the type `xsi:type` states is in `"@type"`.
17    pub json: Value,
18    /// The canonical URL of the root element's type.
19    pub root_type: String,
20    /// Where each element is in the XML, by its path in the JSON (`ClinicalDocument.component
21    /// .structuredBody.component[2].section`), the form the engine's trace uses.
22    pub positions: HashMap<String, Position>,
23    /// What could not be read into the model: unknown elements and attributes, values that do
24    /// not fit their type.
25    pub issues: Vec<Issue>,
26}
27
28impl Document {
29    /// The position of an element by its path.
30    pub fn position(&self, path: &str) -> Option<Position> {
31        self.positions.get(path).copied()
32    }
33}
34
35/// How to read a document.
36#[derive(Debug, Clone, Default)]
37pub struct ReadOptions<'a> {
38    /// The root: a canonical URL or type name, or either followed by `.element` for a fragment
39    /// whose root is an element declared inline (`StructuredBody.component`). Without it, the
40    /// type whose root element name matches.
41    pub root: Option<&'a str>,
42    /// Namespaces for a fragment that uses prefixes it does not declare, or no namespace at all:
43    /// `("", default)` and `(prefix, uri)` pairs, in scope around the document.
44    pub namespaces: Vec<(&'a str, &'a str)>,
45}
46
47/// Reads an XML document. `root_type` names the type of the root element (a canonical URL or a
48/// type name); without it, the type is the one whose root element name matches.
49pub fn read(xml: &str, model: &Model, root_type: Option<&str>) -> Result<Document, Error> {
50    read_with(
51        xml,
52        model,
53        &ReadOptions {
54            root: root_type,
55            namespaces: Vec::new(),
56        },
57    )
58}
59
60/// Reads an XML document with [`ReadOptions`].
61pub fn read_with(xml: &str, model: &Model, options: &ReadOptions<'_>) -> Result<Document, Error> {
62    let parsing = ParsingOptions {
63        allow_dtd: false,
64        ..ParsingOptions::default()
65    };
66    // A fragment is read inside an element that declares the namespaces; the XML declaration,
67    // which may only come first, is blanked so line numbers stay.
68    let (text, shift) = if options.namespaces.is_empty() {
69        (std::borrow::Cow::Borrowed(xml), 0)
70    } else {
71        let (wrapped, shift) = wrap_fragment(xml, &options.namespaces);
72        (std::borrow::Cow::Owned(wrapped), shift)
73    };
74    let doc = XmlDocument::parse_with_options(&text, parsing).map_err(|e| {
75        let pos = e.pos();
76        Error {
77            message: format!("not well-formed XML: {e}"),
78            position: Some(Position {
79                line: pos.row,
80                column: if pos.row == 1 {
81                    pos.col.saturating_sub(shift)
82                } else {
83                    pos.col
84                },
85            }),
86        }
87    })?;
88    let root = if shift > 0 {
89        doc.root_element()
90            .children()
91            .find(|n| n.is_element())
92            .ok_or_else(|| Error {
93                message: "the fragment has no element".to_owned(),
94                position: None,
95            })?
96    } else {
97        doc.root_element()
98    };
99    let resolved = root_of(model, root, options.root)?;
100    let expected_name = match resolved {
101        Root::Element(_, element) => Some(element.xml_name.as_str()),
102        Root::Type(ty) => ty.xml_name.as_deref(),
103    };
104    if let Some(expected) = expected_name
105        && expected != root.tag_name().name()
106    {
107        return Err(Error {
108            message: format!(
109                "the root element is `{}`, not `{}`",
110                root.tag_name().name(),
111                expected
112            ),
113            position: None,
114        });
115    }
116    let mut reader = Reader {
117        model,
118        doc: &doc,
119        shift,
120        positions: HashMap::new(),
121        issues: Vec::new(),
122    };
123    let path = resolved.path();
124    reader.at(root, &path);
125    let json = match resolved {
126        Root::Type(ty) => reader.complex(root, &Shape::of(ty), &path),
127        Root::Element(ty, element) => {
128            let context = Shape::of(ty);
129            reader
130                .element_value(root, element, &context, &path)
131                .unwrap_or(Value::Null)
132        }
133    };
134    Ok(Document {
135        json,
136        root_type: resolved.spec(),
137        positions: reader.positions,
138        issues: reader.issues,
139    })
140}
141
142/// Puts a fragment inside a `<fragment>` element that declares `namespaces`; the XML
143/// declaration, which may only come first, is blanked so line numbers stay. Returns the text and
144/// the columns the wrapper adds to the first line.
145pub(crate) fn wrap_fragment(xml: &str, namespaces: &[(&str, &str)]) -> (String, u32) {
146    let mut open = String::from("<fragment");
147    for (prefix, uri) in namespaces {
148        if prefix.is_empty() {
149            open.push_str(" xmlns=\"");
150        } else {
151            open.push_str(&format!(" xmlns:{prefix}=\""));
152        }
153        crate::write::escape_attribute(uri, &mut open);
154        open.push('"');
155    }
156    open.push('>');
157    let body = match xml.find("<?xml") {
158        Some(start) if xml[..start].trim().is_empty() => {
159            let end = xml[start..].find("?>").map_or(start, |e| start + e + 2);
160            let mut blanked = String::with_capacity(xml.len());
161            blanked.push_str(&xml[..start]);
162            blanked.extend(
163                xml[start..end]
164                    .chars()
165                    .map(|c| if c == '\n' { '\n' } else { ' ' }),
166            );
167            blanked.push_str(&xml[end..]);
168            blanked
169        }
170        _ => xml.to_owned(),
171    };
172    let shift = open.chars().count() as u32;
173    (format!("{open}{body}</fragment>"), shift)
174}
175
176fn root_of<'m>(
177    model: &'m Model,
178    root: Node<'_, '_>,
179    stated: Option<&str>,
180) -> Result<Root<'m>, Error> {
181    if let Some(stated) = stated {
182        if let Some(ty) = model.by_type_name(root.tag_name().namespace(), stated) {
183            return Ok(Root::Type(ty));
184        }
185        return model.root(stated).ok_or_else(|| Error {
186            message: format!("the model has no type or element `{stated}`"),
187            position: None,
188        });
189    }
190    let name = root.tag_name().name();
191    let candidates = model.roots(root.tag_name().namespace(), name);
192    match candidates.as_slice() {
193        [single] => Ok(Root::Type(single)),
194        [] => Err(Error {
195            message: format!(
196                "no type of the model has the root element `{name}`{}; say which type or element it is",
197                root.tag_name()
198                    .namespace()
199                    .map(|ns| format!(" in namespace `{ns}`"))
200                    .unwrap_or_default()
201            ),
202            position: None,
203        }),
204        several => Err(Error {
205            message: format!(
206                "the root element `{name}` can be any of {}; say which type it is",
207                several
208                    .iter()
209                    .map(|t| t.url.as_str())
210                    .collect::<Vec<_>>()
211                    .join(", ")
212            ),
213            position: None,
214        }),
215    }
216}
217
218/// The elements a node can hold: those of a type, or those declared inline on an element.
219struct Shape<'m> {
220    elements: &'m [Element],
221    /// The structure the elements belong to: type codes resolve relative to it.
222    owner: String,
223    namespace: Option<String>,
224}
225
226impl<'m> Shape<'m> {
227    fn of(ty: &'m TypeDef) -> Self {
228        Shape {
229            elements: &ty.elements,
230            owner: ty.url.clone(),
231            namespace: ty.namespace.clone(),
232        }
233    }
234
235    fn text_element(&self) -> Option<&'m Element> {
236        self.elements
237            .iter()
238            .find(|e| e.representation == Representation::Text)
239    }
240
241    fn choice_group(&self) -> Option<&'m Element> {
242        self.elements.iter().find(|e| e.choice_group)
243    }
244}
245
246/// Where an XML child element goes in the JSON.
247enum Slot<'m> {
248    Direct(&'m Element),
249    /// A member of a choice group: the group, then the member.
250    Grouped(&'m Element, &'m Element),
251}
252
253struct Reader<'m, 'd> {
254    model: &'m Model,
255    doc: &'d XmlDocument<'d>,
256    /// Columns the fragment wrapper adds to the first line.
257    shift: u32,
258    positions: HashMap<String, Position>,
259    issues: Vec<Issue>,
260}
261
262impl<'m> Reader<'m, '_> {
263    fn position(&self, node: Node<'_, '_>) -> Position {
264        let pos = self.doc.text_pos_at(node.range().start);
265        Position {
266            line: pos.row,
267            column: if pos.row == 1 {
268                pos.col.saturating_sub(self.shift)
269            } else {
270                pos.col
271            },
272        }
273    }
274
275    fn at(&mut self, node: Node<'_, '_>, path: &str) {
276        let position = self.position(node);
277        self.positions.insert(path.to_owned(), position);
278    }
279
280    fn issue(&mut self, kind: IssueKind, path: &str, node: Node<'_, '_>, message: String) {
281        let position = Some(self.position(node));
282        self.issues.push(Issue {
283            kind,
284            path: path.to_owned(),
285            position,
286            message,
287        });
288    }
289
290    /// An element's content: attributes, text and child elements.
291    fn complex(&mut self, node: Node<'_, '_>, shape: &Shape<'m>, path: &str) -> Value {
292        let mut object = Map::new();
293        self.attributes(node, shape, path, &mut object);
294        let text_element = shape.text_element();
295        let group = shape.choice_group();
296        let has_children = node.children().any(|c| c.is_element());
297        let mut text = String::new();
298        let mut text_seen = false;
299        for child in node.children() {
300            if child.is_text() {
301                let content = child.text().unwrap_or_default();
302                if let Some(group) = group
303                    && group.child("xmlText").is_some()
304                {
305                    if !content.trim().is_empty() {
306                        let index = push_many(&mut object, &group.name, {
307                            let mut item = Map::new();
308                            item.insert("xmlText".to_owned(), Value::String(content.to_owned()));
309                            Value::Object(item)
310                        });
311                        let _ = index;
312                    }
313                    continue;
314                }
315                if text_element.is_some() {
316                    text.push_str(content);
317                    text_seen = true;
318                } else if !content.trim().is_empty() {
319                    self.issue(
320                        IssueKind::UnexpectedText,
321                        path,
322                        child,
323                        format!("text `{}` where the model has no text", content.trim()),
324                    );
325                }
326                continue;
327            }
328            if !child.is_element() {
329                continue;
330            }
331            let expected_ns = node.tag_name().namespace();
332            let Some(slot) = self.slot(shape, child, expected_ns) else {
333                let name = qualified(child);
334                self.issue(
335                    IssueKind::UnknownElement,
336                    path,
337                    child,
338                    format!("element `{name}` is not in the model; left out"),
339                );
340                continue;
341            };
342            match slot {
343                Slot::Direct(element) => {
344                    let index = count(&object, &element.name);
345                    let child_path = if element.many {
346                        format!("{path}.{}[{index}]", element.name)
347                    } else {
348                        format!("{path}.{}", element.name)
349                    };
350                    if !element.many && object.contains_key(&element.name) {
351                        self.issue(
352                            IssueKind::TooMany,
353                            &child_path,
354                            child,
355                            format!("`{}` repeats but the model allows one", element.xml_name),
356                        );
357                        continue;
358                    }
359                    self.at(child, &child_path);
360                    let Some(value) = self.element_value(child, element, shape, &child_path) else {
361                        continue;
362                    };
363                    if element.many {
364                        push_many(&mut object, &element.name, value);
365                    } else {
366                        object.insert(element.name.clone(), value);
367                    }
368                }
369                Slot::Grouped(group, member) => {
370                    let index = count(&object, &group.name);
371                    let child_path = format!("{path}.{}[{index}].{}", group.name, member.name);
372                    self.at(child, &child_path);
373                    let inner = Shape {
374                        elements: &group.children,
375                        owner: shape.owner.clone(),
376                        namespace: shape.namespace.clone(),
377                    };
378                    let Some(value) = self.element_value(child, member, &inner, &child_path) else {
379                        continue;
380                    };
381                    let mut item = Map::new();
382                    item.insert(member.name.clone(), value);
383                    push_many(&mut object, &group.name, Value::Object(item));
384                }
385            }
386        }
387        if let Some(text_element) = text_element
388            && text_seen
389            && (!has_children || !text.trim().is_empty())
390        {
391            object.insert(text_element.name.clone(), Value::String(text));
392        }
393        Value::Object(object)
394    }
395
396    fn attributes(
397        &mut self,
398        node: Node<'_, '_>,
399        shape: &Shape<'m>,
400        path: &str,
401        object: &mut Map<String, Value>,
402    ) {
403        for attribute in node.attributes() {
404            let namespace = attribute.namespace();
405            if namespace == Some(XSI_NS) {
406                // `xsi:type` is read with the element; the schema location is not content.
407                continue;
408            }
409            let element = shape.elements.iter().find(|e| {
410                e.representation == Representation::Attribute
411                    && e.xml_name == attribute.name()
412                    && namespace
413                        == if e.explicit_namespace {
414                            e.namespace.as_deref()
415                        } else {
416                            None
417                        }
418            });
419            let Some(element) = element else {
420                let name = match namespace {
421                    Some(ns) if ns == XML_NS => format!("xml:{}", attribute.name()),
422                    Some(ns) => format!("{{{ns}}}{}", attribute.name()),
423                    None => attribute.name().to_owned(),
424                };
425                self.issue(
426                    IssueKind::UnknownAttribute,
427                    path,
428                    node,
429                    format!("attribute `{name}` is not in the model; left out"),
430                );
431                continue;
432            };
433            let attr_path = format!("{path}.{}", element.name);
434            let value = if element.many {
435                Value::Array(
436                    attribute
437                        .value()
438                        .split_whitespace()
439                        .map(|token| self.primitive(token, element, &attr_path, node))
440                        .collect(),
441                )
442            } else {
443                self.primitive(attribute.value(), element, &attr_path, node)
444            };
445            object.insert(element.name.clone(), value);
446        }
447    }
448
449    /// An attribute value as the JSON primitive of its type.
450    fn primitive(
451        &mut self,
452        text: &str,
453        element: &Element,
454        path: &str,
455        node: Node<'_, '_>,
456    ) -> Value {
457        if element.date_format {
458            return match datetime::from_compact(text.trim()) {
459                Some(converted) => Value::String(converted),
460                None => {
461                    self.issue(
462                        IssueKind::InvalidValue,
463                        path,
464                        node,
465                        format!("`{text}` is not a timestamp (YYYYMMDDHHMMSS.UUUU[+|-ZZzz]); kept as it is"),
466                    );
467                    Value::String(text.to_owned())
468                }
469            };
470        }
471        let code = element.single_type().unwrap_or_default();
472        let invalid = |reader: &mut Self, what: &str| {
473            reader.issue(
474                IssueKind::InvalidValue,
475                path,
476                node,
477                format!("`{text}` is not a valid {what}; kept as text"),
478            );
479            Value::String(text.to_owned())
480        };
481        match code {
482            "boolean" => match text.trim() {
483                "true" => Value::Bool(true),
484                "false" => Value::Bool(false),
485                _ => invalid(self, "boolean"),
486            },
487            "integer" | "positiveInt" | "unsignedInt" | "integer64" => {
488                match text.trim().parse::<i64>() {
489                    Ok(n) => Value::Number(n.into()),
490                    Err(_) => invalid(self, "integer"),
491                }
492            }
493            "decimal" => match decimal(text.trim()) {
494                Some(n) => Value::Number(n),
495                None => invalid(self, "decimal"),
496            },
497            _ => Value::String(text.to_owned()),
498        }
499    }
500
501    /// Finds the element a child element is.
502    fn slot(
503        &self,
504        shape: &Shape<'m>,
505        child: Node<'_, '_>,
506        parent_ns: Option<&str>,
507    ) -> Option<Slot<'m>> {
508        let name = child.tag_name().name();
509        let namespace = child.tag_name().namespace();
510        let fits = |element: &Element| {
511            element.representation != Representation::Attribute
512                && element.representation != Representation::Text
513                && !element.choice_group
514                && element.xml_name == name
515                && namespace == element.namespace.as_deref().or(parent_ns)
516        };
517        for element in shape.elements {
518            if element.choice_group {
519                if let Some(member) = element.children.iter().find(|m| fits(m)) {
520                    return Some(Slot::Grouped(element, member));
521                }
522            } else if fits(element) {
523                return Some(Slot::Direct(element));
524            }
525        }
526        None
527    }
528
529    /// The value of a child element: markup as a string, otherwise an object of its type.
530    fn element_value(
531        &mut self,
532        node: Node<'_, '_>,
533        element: &'m Element,
534        context: &Shape<'m>,
535        path: &str,
536    ) -> Option<Value> {
537        if element.representation == Representation::Markup {
538            let mut out = String::new();
539            write_markup(node, node.tag_name().namespace(), &mut Vec::new(), &mut out);
540            return Some(Value::String(out));
541        }
542        let model: &'m Model = self.model;
543        let declared: Option<&'m TypeDef> = element
544            .single_type()
545            .and_then(|code| model.type_of(code, &context.owner))
546            .map(|t| &**t);
547        let stated = node
548            .attributes()
549            .find(|a| a.namespace() == Some(XSI_NS) && a.name() == "type")
550            .map(|a| a.value().to_owned());
551        let mut actual: Option<&'m TypeDef> = match &stated {
552            Some(qname) => {
553                let (prefix, local) = match qname.split_once(':') {
554                    Some((prefix, local)) => (Some(prefix), local),
555                    None => (None, qname.as_str()),
556                };
557                let namespace = node.lookup_namespace_uri(prefix);
558                match model.by_type_name(namespace, local) {
559                    Some(found) => Some(&**found),
560                    None => {
561                        self.issue(
562                            IssueKind::UnknownType,
563                            path,
564                            node,
565                            format!("xsi:type `{qname}` is not a type of the model"),
566                        );
567                        None
568                    }
569                }
570            }
571            None => None,
572        };
573        if actual.is_none()
574            && let Some(default) = &element.default_type
575        {
576            actual = model.type_of(default, &context.owner).map(|t| &**t);
577        }
578        // Inline elements describe the content unless xsi:type names another type.
579        if actual.is_none() && !element.children.is_empty() {
580            let shape = Shape {
581                elements: &element.children,
582                owner: context.owner.clone(),
583                namespace: element.namespace.clone(),
584            };
585            return Some(self.complex(node, &shape, path));
586        }
587        if actual.is_none() {
588            actual = declared;
589        }
590        let Some(actual) = actual else {
591            let candidates: Vec<&str> = element
592                .types
593                .iter()
594                .map(|t| t.rsplit('/').next().unwrap_or(t))
595                .collect();
596            self.issue(
597                IssueKind::MissingType,
598                path,
599                node,
600                format!(
601                    "`{}` can be {}; without xsi:type its content cannot be read",
602                    element.xml_name,
603                    candidates.join(", ")
604                ),
605            );
606            return None;
607        };
608        if let Some(stated_type) = declared
609            && !model.derives_from(actual, &stated_type.url)
610        {
611            self.issue(
612                IssueKind::UnknownType,
613                path,
614                node,
615                format!("xsi:type `{}` is not a `{}`", actual.name, stated_type.name),
616            );
617        }
618        let shape = Shape::of(actual);
619        let mut value = self.complex(node, &shape, path);
620        // The type is named when the XML states it, the element is polymorphic, or the value is
621        // a subtype of the element's type.
622        let polymorphic = element.types.len() > 1;
623        let differs = declared.is_none_or(|d| d.url != actual.url);
624        if (stated.is_some() || polymorphic || differs)
625            && let Value::Object(object) = &mut value
626        {
627            let mut typed = Map::with_capacity(object.len() + 1);
628            typed.insert(TYPE_KEY.to_owned(), Value::String(actual.name.clone()));
629            typed.extend(std::mem::take(object));
630            *object = typed;
631        }
632        Some(value)
633    }
634}
635
636/// A decimal as a JSON number: an integer when written without a fraction (`12`), otherwise a
637/// float. JSON numbers do not keep trailing zeros (`12.50` reads as `12.5`).
638fn decimal(text: &str) -> Option<Number> {
639    if !is_decimal_text(text) {
640        return None;
641    }
642    if !text.contains('.')
643        && let Ok(n) = text.parse::<i64>()
644    {
645        return Some(n.into());
646    }
647    text.parse::<f64>()
648        .ok()
649        .filter(|n| n.is_finite())
650        .and_then(Number::from_f64)
651}
652
653/// Whether text is a decimal in XML Schema's lexical space (no exponent, no `inf`).
654fn is_decimal_text(text: &str) -> bool {
655    let digits = text.strip_prefix(['+', '-']).unwrap_or(text);
656    !digits.is_empty()
657        && digits.bytes().all(|b| b.is_ascii_digit() || b == b'.')
658        && digits.bytes().filter(|&b| b == b'.').count() <= 1
659        && digits.bytes().any(|b| b.is_ascii_digit())
660}
661
662fn count(object: &Map<String, Value>, name: &str) -> usize {
663    match object.get(name) {
664        Some(Value::Array(items)) => items.len(),
665        Some(_) => 1,
666        None => 0,
667    }
668}
669
670fn push_many(object: &mut Map<String, Value>, name: &str, value: Value) -> usize {
671    let entry = object
672        .entry(name.to_owned())
673        .or_insert_with(|| Value::Array(Vec::new()));
674    match entry {
675        Value::Array(items) => {
676            items.push(value);
677            items.len() - 1
678        }
679        other => {
680            let previous = std::mem::take(other);
681            *other = Value::Array(vec![previous, value]);
682            1
683        }
684    }
685}
686
687fn qualified(node: Node<'_, '_>) -> String {
688    match node.tag_name().namespace() {
689        Some(ns) => format!("{{{ns}}}{}", node.tag_name().name()),
690        None => node.tag_name().name().to_owned(),
691    }
692}
693
694/// Writes an element and its content as markup. Elements in `default_ns` are written without a
695/// prefix; others get the prefix the document used, declared where first needed.
696fn write_markup(
697    node: Node<'_, '_>,
698    default_ns: Option<&str>,
699    declared: &mut Vec<(String, String)>,
700    out: &mut String,
701) {
702    let mark = declared.len();
703    let mut declarations = String::new();
704    let tag = name_in(
705        node,
706        node.tag_name().namespace(),
707        node.tag_name().name(),
708        default_ns,
709        declared,
710        &mut declarations,
711        false,
712    );
713    out.push('<');
714    out.push_str(&tag);
715    let mut attributes = String::new();
716    for attribute in node.attributes() {
717        let name = name_in(
718            node,
719            attribute.namespace(),
720            attribute.name(),
721            default_ns,
722            declared,
723            &mut declarations,
724            true,
725        );
726        attributes.push(' ');
727        attributes.push_str(&name);
728        attributes.push_str("=\"");
729        crate::write::escape_attribute(attribute.value(), &mut attributes);
730        attributes.push('"');
731    }
732    out.push_str(&declarations);
733    out.push_str(&attributes);
734    let mut children = node
735        .children()
736        .filter(|c| c.is_element() || c.is_text())
737        .peekable();
738    if children.peek().is_none() {
739        out.push_str("/>");
740    } else {
741        out.push('>');
742        for child in children {
743            if child.is_text() {
744                crate::write::escape_text(child.text().unwrap_or_default(), out);
745            } else {
746                write_markup(child, default_ns, declared, out);
747            }
748        }
749        out.push_str("</");
750        out.push_str(&tag);
751        out.push('>');
752    }
753    declared.truncate(mark);
754}
755
756/// The name to write for an element or attribute in `namespace`, adding a namespace declaration
757/// when the prefix is not declared yet.
758fn name_in(
759    node: Node<'_, '_>,
760    namespace: Option<&str>,
761    local: &str,
762    default_ns: Option<&str>,
763    declared: &mut Vec<(String, String)>,
764    declarations: &mut String,
765    attribute: bool,
766) -> String {
767    let Some(namespace) = namespace else {
768        if !attribute && default_ns.is_some() {
769            // An element in no namespace inside a default namespace.
770            if !declarations.contains("xmlns=\"\"") {
771                declarations.push_str(" xmlns=\"\"");
772            }
773        }
774        return local.to_owned();
775    };
776    if namespace == XML_NS {
777        return format!("xml:{local}");
778    }
779    if !attribute && Some(namespace) == default_ns {
780        return local.to_owned();
781    }
782    if let Some((prefix, _)) = declared.iter().rev().find(|(_, ns)| ns == namespace) {
783        return format!("{prefix}:{local}");
784    }
785    let prefix = node
786        .lookup_prefix(namespace)
787        .filter(|p| !p.is_empty())
788        .map(str::to_owned)
789        .unwrap_or_else(|| format!("ns{}", declared.len() + 1));
790    declarations.push_str(&format!(" xmlns:{prefix}=\""));
791    crate::write::escape_attribute(namespace, declarations);
792    declarations.push('"');
793    declared.push((prefix.clone(), namespace.to_owned()));
794    format!("{prefix}:{local}")
795}