Track xml:lang throughout parsing

Jonas SchΓ€fer created

Change summary

parsers/src/iq.rs                  |   1 
parsers/src/jingle.rs              |   2 
parsers/src/jingle_s5b.rs          |   1 
parsers/src/util/macro_tests.rs    |  62 ++++++++++++
parsers/src/util/macros.rs         |   1 
tokio-xmpp/src/xmlstream/common.rs |  26 ++++
xso-proc/src/compound.rs           |   4 
xso-proc/src/enums.rs              |   2 
xso-proc/src/field/child.rs        |   4 
xso-proc/src/field/element.rs      |   2 
xso-proc/src/field/lang.rs         |  72 ++++++++++++++
xso-proc/src/field/mod.rs          |   4 
xso-proc/src/lib.rs                |   1 
xso-proc/src/meta.rs               |  18 +++
xso-proc/src/state.rs              |  10 +
xso-proc/src/structs.rs            |   6 
xso/ChangeLog                      |   4 
xso/src/from_xml_doc.md            |  60 ++++++++++++
xso/src/fromxml.rs                 | 160 ++++++++++++++++++++++---------
xso/src/lib.rs                     |  73 +++++++++++---
xso/src/minidom_compat.rs          |  21 ++-
21 files changed, 446 insertions(+), 88 deletions(-)

Detailed changes

parsers/src/iq.rs πŸ”—

@@ -229,6 +229,7 @@ impl ::xso::FromXml for Iq {
     fn from_events(
         qname: ::xso::exports::rxml::QName,
         attrs: ::xso::exports::rxml::AttrMap,
+        _ctx: &::xso::Context<'_>,
     ) -> Result<Self::Builder, ::xso::error::FromEventsError> {
         if qname.0 != crate::ns::DEFAULT_NS || qname.1 != "iq" {
             return Err(::xso::error::FromEventsError::Mismatch { name: qname, attrs });

parsers/src/jingle.rs πŸ”—

@@ -203,6 +203,7 @@ impl ::xso::FromXml for Description {
     fn from_events(
         qname: ::xso::exports::rxml::QName,
         attrs: ::xso::exports::rxml::AttrMap,
+        _ctx: &::xso::Context<'_>,
     ) -> Result<Self::Builder, ::xso::error::FromEventsError> {
         if qname.1 != "description" {
             return Err(::xso::error::FromEventsError::Mismatch { name: qname, attrs });
@@ -263,6 +264,7 @@ impl ::xso::FromXml for Transport {
     fn from_events(
         qname: ::xso::exports::rxml::QName,
         attrs: ::xso::exports::rxml::AttrMap,
+        _ctx: &::xso::Context<'_>,
     ) -> Result<Self::Builder, ::xso::error::FromEventsError> {
         if qname.1 != "transport" {
             return Err(::xso::error::FromEventsError::Mismatch { name: qname, attrs });

parsers/src/jingle_s5b.rs πŸ”—

@@ -290,6 +290,7 @@ impl ::xso::FromXml for Transport {
     fn from_events(
         qname: ::xso::exports::rxml::QName,
         attrs: ::xso::exports::rxml::AttrMap,
+        _ctx: &::xso::Context<'_>,
     ) -> Result<Self::Builder, ::xso::error::FromEventsError> {
         if qname.0 != crate::ns::JINGLE_S5B || qname.1 != "transport" {
             return Err(::xso::error::FromEventsError::Mismatch { name: qname, attrs });

parsers/src/util/macro_tests.rs πŸ”—

@@ -2489,3 +2489,65 @@ fn text_vs_attribute_ordering_compile_bug_roundtrip_with_parent() {
         "<thread xmlns='urn:example:ns1' parent='bar'>foo</thread>",
     );
 }
+
+#[derive(FromXml, AsXml, PartialEq, Debug, Clone)]
+#[xml(namespace = NS1, name = "elem")]
+struct Language {
+    #[xml(child(default))]
+    child: core::option::Option<Box<Language>>,
+
+    #[xml(lang)]
+    lang: core::option::Option<String>,
+}
+
+#[test]
+fn language_roundtrip_absent() {
+    #[allow(unused_imports)]
+    use core::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<Language>("<elem xmlns='urn:example:ns1'/>");
+}
+
+#[test]
+fn language_roundtrip_present() {
+    #[allow(unused_imports)]
+    use core::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    roundtrip_full::<Language>("<elem xmlns='urn:example:ns1' xml:lang='foo'/>");
+}
+
+#[test]
+fn language_roundtrip_nested_parse() {
+    // cannot write a round-trip test for this, because on emission,
+    // `#[xml(language)]` fields with a non-None value will always emit
+    // the `xml:lang` attribute to ensure semantic correctness.
+    #[allow(unused_imports)]
+    use core::{
+        option::Option::{None, Some},
+        result::Result::{Err, Ok},
+    };
+    match parse_str::<Language>(
+        "<elem xmlns='urn:example:ns1' xml:lang='foo'><elem><elem xml:lang='bar'/></elem></elem>",
+    ) {
+        Ok(Language { child, lang }) => {
+            assert_eq!(lang.as_deref(), Some("foo"));
+
+            let Some(Language { child, lang }) = child.map(|x| *x) else {
+                panic!("missing child");
+            };
+            assert_eq!(lang.as_deref(), Some("foo"));
+
+            let Some(Language { child, lang }) = child.map(|x| *x) else {
+                panic!("missing grand child");
+            };
+            assert_eq!(lang.as_deref(), Some("bar"));
+
+            assert!(child.is_none());
+        }
+        other => panic!("unexpected parse result: {:?}", other),
+    }
+}

parsers/src/util/macros.rs πŸ”—

@@ -262,6 +262,7 @@ macro_rules! generate_attribute_enum {
             fn from_events(
                 qname: ::xso::exports::rxml::QName,
                 attrs: ::xso::exports::rxml::AttrMap,
+                _ctx: &::xso::Context<'_>,
             ) -> Result<Self::Builder, ::xso::error::FromEventsError> {
                 if qname.0 != crate::ns::$ns || qname.1 != $name {
                     return Err(::xso::error::FromEventsError::Mismatch {

tokio-xmpp/src/xmlstream/common.rs πŸ”—

@@ -23,7 +23,9 @@ use tokio::{
 };
 
 use xso::{
-    exports::rxml::{self, writer::TrackNamespace, xml_ncname, Event, Namespace},
+    exports::rxml::{
+        self, writer::TrackNamespace, xml_lang::XmlLangStack, xml_ncname, Event, Namespace,
+    },
     AsXml, FromEventsBuilder, FromXml, Item,
 };
 
@@ -165,6 +167,9 @@ pin_project_lite::pin_project! {
         #[pin]
         parser: rxml::AsyncReader<CaptureBufRead<Io>>,
 
+        // Tracker for `xml:lang` data.
+        lang_stack: XmlLangStack,
+
         // The writer used for serialising data.
         writer: rxml::writer::Encoder<rxml::writer::SimpleNamespaces>,
 
@@ -254,6 +259,7 @@ impl<Io: AsyncBufRead + AsyncWrite> RawXmlStream<Io> {
         Self {
             parser: rxml::AsyncReader::wrap(io, parser),
             writer: Self::new_writer(stream_ns),
+            lang_stack: XmlLangStack::new(),
             timeouts: TimeoutState::new(timeouts),
             tx_buffer_logged: 0,
             stream_ns,
@@ -270,6 +276,7 @@ impl<Io: AsyncBufRead + AsyncWrite> RawXmlStream<Io> {
     pub(super) fn reset_state(self: Pin<&mut Self>) {
         let this = self.project();
         *this.parser.parser_pinned() = rxml::Parser::default();
+        *this.lang_stack = XmlLangStack::new();
         *this.writer = Self::new_writer(this.stream_ns);
     }
 
@@ -293,6 +300,7 @@ impl<Io: AsyncBufRead + AsyncWrite> RawXmlStream<Io> {
         let parser = rxml::AsyncReader::wrap(io, p);
         RawXmlStream {
             parser,
+            lang_stack: XmlLangStack::new(),
             timeouts: self.timeouts,
             writer: self.writer,
             tx_buffer: self.tx_buffer,
@@ -360,6 +368,10 @@ impl<Io: AsyncBufRead> Stream for RawXmlStream<Io> {
                     match v.transpose() {
                         // Skip the XML declaration, nobody wants to hear about that.
                         Some(Ok(rxml::Event::XmlDeclaration(_, _))) => continue,
+                        Some(Ok(event)) => {
+                            this.lang_stack.handle_event(&event);
+                            return Poll::Ready(Some(Ok(event)));
+                        }
                         other => return Poll::Ready(other.map(|x| x.map_err(RawError::Io))),
                     }
                 }
@@ -635,9 +647,13 @@ impl<T: FromXml> ReadXsoState<T> {
                             }
                         }
                         Ok(Some(rxml::Event::StartElement(_, name, attrs))) => {
+                            let source_tmp = source.as_mut();
+                            let ctx = xso::Context::new(source_tmp.lang_stack.current());
                             *self = ReadXsoState::Parsing(
-                                <Result<T, xso::error::Error> as FromXml>::from_events(name, attrs)
-                                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?,
+                                <Result<T, xso::error::Error> as FromXml>::from_events(
+                                    name, attrs, &ctx,
+                                )
+                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?,
                             );
                         }
                         // Amounts to EOF, as we expect to start on the stream level.
@@ -691,7 +707,9 @@ impl<T: FromXml> ReadXsoState<T> {
                         }
                     };
 
-                    match builder.feed(ev) {
+                    let source_tmp = source.as_mut();
+                    let ctx = xso::Context::new(source_tmp.lang_stack.current());
+                    match builder.feed(ev, &ctx) {
                         Err(err) => {
                             *self = ReadXsoState::Done;
                             source.as_mut().stream_pinned().discard_capture();

xso-proc/src/compound.rs πŸ”—

@@ -318,7 +318,7 @@ impl Compound {
                         substate_data,
                         &builder,
                     ).with_mut(substate_data).with_impl(quote! {
-                        match #feed(&mut #substate_data, ev)? {
+                        match #feed(&mut #substate_data, ev, ctx)? {
                             ::core::option::Option::Some(#substate_result) => {
                                 #collect
                                 ::core::result::Result::Ok(::core::ops::ControlFlow::Break(Self::#default_state_ident {
@@ -467,7 +467,7 @@ impl Compound {
             substate_data,
             &discard_builder_ty,
         ).with_mut(substate_data).with_impl(quote! {
-            match #discard_feed(&mut #substate_data, ev)? {
+            match #discard_feed(&mut #substate_data, ev, ctx)? {
                 ::core::option::Option::Some(#substate_result) => {
                     ::core::result::Result::Ok(::core::ops::ControlFlow::Break(Self::#default_state_ident {
                         #builder_data_ident,

xso-proc/src/enums.rs πŸ”—

@@ -547,7 +547,7 @@ impl ItemDef for EnumDef {
         Ok(FromXmlParts {
             defs,
             from_events_body: quote! {
-                #builder_ty_ident::new(#name_ident, #attrs_ident)
+                #builder_ty_ident::new(#name_ident, #attrs_ident, ctx)
             },
             builder_ty_ident: builder_ty_ident.clone(),
         })

xso-proc/src/field/child.rs πŸ”—

@@ -68,7 +68,7 @@ impl Field for ChildField {
 
                 let from_events = from_events_fn(element_ty.clone());
 
-                let matcher = quote! { #from_events(name, attrs) };
+                let matcher = quote! { #from_events(name, attrs, ctx) };
                 let builder = from_xml_builder_ty(element_ty.clone());
 
                 (
@@ -307,7 +307,7 @@ impl ExtractDef {
         )?;
         let from_xml_builder_ty = ty_from_ident(from_xml_builder_ty_ident.clone()).into();
 
-        let matcher = quote! { #state_ty_ident::new(name, attrs).map(|x| #from_xml_builder_ty_ident(::core::option::Option::Some(x))) };
+        let matcher = quote! { #state_ty_ident::new(name, attrs, ctx).map(|x| #from_xml_builder_ty_ident(::core::option::Option::Some(x))) };
 
         let inner_ty = self.parts.to_single_or_tuple_ty();
 

xso-proc/src/field/element.rs πŸ”—

@@ -80,7 +80,7 @@ impl Field for ElementField {
                         if #field_access.is_some() {
                             ::core::result::Result::Err(::xso::error::FromEventsError::Mismatch { name, attrs })
                         } else {
-                            #from_events(name, attrs)
+                            #from_events(name, attrs, ctx)
                         }
                     }),
                     builder,

xso-proc/src/field/lang.rs πŸ”—

@@ -0,0 +1,72 @@
+// Copyright (c) 2025 Jonas SchΓ€fer <jonas@zombofant.net>
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+//! This module concerns the processing of inherited `xml:lang` values.
+//!
+//! In particular, it provides the `#[xml(lang)]` implementation.
+
+use proc_macro2::Span;
+use quote::quote;
+use syn::*;
+
+use crate::error_message::ParentRef;
+use crate::scope::{AsItemsScope, FromEventsScope};
+use crate::types::{as_optional_xml_text_fn, option_ty, string_ty};
+
+use super::{Field, FieldBuilderPart, FieldIteratorPart, FieldTempInit};
+
+/// The field maps to a potentially inherited `xml:lang` value.
+pub(super) struct LangField;
+
+impl Field for LangField {
+    fn make_builder_part(
+        &self,
+        _scope: &FromEventsScope,
+        _container_name: &ParentRef,
+        _member: &Member,
+        _ty: &Type,
+    ) -> Result<FieldBuilderPart> {
+        let string_ty = string_ty(Span::call_site());
+        let ty = option_ty(string_ty.clone());
+
+        Ok(FieldBuilderPart::Init {
+            value: FieldTempInit {
+                ty,
+                init: quote! {
+                    ctx.language().map(#string_ty::from).into()
+                },
+            },
+        })
+    }
+
+    fn make_iterator_part(
+        &self,
+        _scope: &AsItemsScope,
+        _container_name: &ParentRef,
+        bound_name: &Ident,
+        _member: &Member,
+        ty: &Type,
+    ) -> Result<FieldIteratorPart> {
+        let as_optional_xml_text = as_optional_xml_text_fn(ty.clone());
+
+        Ok(FieldIteratorPart::Header {
+            generator: quote! {
+                #as_optional_xml_text(#bound_name)?.map(|#bound_name|
+                    ::xso::Item::Attribute(
+                        ::xso::exports::rxml::Namespace::XML,
+                        // SAFETY: `lang` is a known-good NcName
+                        unsafe {
+                            ::xso::exports::alloc::borrow::Cow::Borrowed(
+                                ::xso::exports::rxml::NcNameStr::from_str_unchecked("lang"),
+                            )
+                        },
+                        #bound_name,
+                    )
+                )
+            },
+        })
+    }
+}

xso-proc/src/field/mod.rs πŸ”—

@@ -21,6 +21,7 @@ mod child;
 #[cfg(feature = "minidom")]
 mod element;
 mod flag;
+mod lang;
 mod text;
 
 use self::attribute::AttributeField;
@@ -28,6 +29,7 @@ use self::child::{ChildField, ExtractDef};
 #[cfg(feature = "minidom")]
 use self::element::ElementField;
 use self::flag::FlagField;
+use self::lang::LangField;
 use self::text::TextField;
 
 /// Code slices necessary for declaring and initializing a temporary variable
@@ -444,6 +446,8 @@ fn new_field(
                 xml_name,
             }))
         }
+
+        XmlFieldMeta::Language { span: _ } => Ok(Box::new(LangField)),
     }
 }
 

xso-proc/src/lib.rs πŸ”—

@@ -83,6 +83,7 @@ fn from_xml_impl(input: Item) -> Result<TokenStream> {
             fn from_events(
                 name: ::xso::exports::rxml::QName,
                 attrs: ::xso::exports::rxml::AttrMap,
+                ctx: &::xso::Context<'_>,
             ) -> ::core::result::Result<Self::Builder, ::xso::error::FromEventsError> {
                 #from_events_body
             }

xso-proc/src/meta.rs πŸ”—

@@ -874,6 +874,14 @@ pub(crate) enum XmlFieldMeta {
         /// The namespace/name keys.
         qname: QNameRef,
     },
+
+    /// `#[xml(lang)]`
+    Language {
+        /// The span of the `#[xml(lang)]` meta from which this was parsed.
+        ///
+        /// This is useful for error messages.
+        span: Span,
+    },
 }
 
 impl XmlFieldMeta {
@@ -1182,6 +1190,13 @@ impl XmlFieldMeta {
         })
     }
 
+    /// Parse a `#[xml(lang)]` meta.
+    fn lang_from_meta(meta: ParseNestedMeta<'_>) -> Result<Self> {
+        Ok(Self::Language {
+            span: meta.path.span(),
+        })
+    }
+
     /// Parse [`Self`] from a nestd meta, switching on the identifier
     /// of that nested meta.
     fn parse_from_meta(meta: ParseNestedMeta<'_>) -> Result<Self> {
@@ -1197,6 +1212,8 @@ impl XmlFieldMeta {
             Self::element_from_meta(meta)
         } else if meta.path.is_ident("flag") {
             Self::flag_from_meta(meta)
+        } else if meta.path.is_ident("lang") {
+            Self::lang_from_meta(meta)
         } else {
             Err(Error::new_spanned(meta.path, "unsupported field meta"))
         }
@@ -1280,6 +1297,7 @@ impl XmlFieldMeta {
             Self::Extract { ref span, .. } => *span,
             Self::Element { ref span, .. } => *span,
             Self::Flag { ref span, .. } => *span,
+            Self::Language { ref span, .. } => *span,
         }
     }
 

xso-proc/src/state.rs πŸ”—

@@ -509,7 +509,7 @@ impl FromEventsStateMachine {
             }
 
             impl #state_ty_ident {
-                fn advance(mut self, ev: ::xso::exports::rxml::Event) -> ::core::result::Result<::core::ops::ControlFlow<Self, #output_ty>, ::xso::error::Error> {
+                fn advance(mut self, ev: ::xso::exports::rxml::Event, ctx: &::xso::Context<'_>) -> ::core::result::Result<::core::ops::ControlFlow<Self, #output_ty>, ::xso::error::Error> {
                     match self {
                         #advance_match_arms
                     }.and_then(|__ok| {
@@ -527,8 +527,9 @@ impl FromEventsStateMachine {
                 fn new(
                     name: ::xso::exports::rxml::QName,
                     attrs: ::xso::exports::rxml::AttrMap,
+                    ctx: &::xso::Context<'_>,
                 ) -> ::core::result::Result<Self, ::xso::error::FromEventsError> {
-                    #state_ty_ident::new(name, attrs).map(|ok| Self(::core::option::Option::Some(ok)))
+                    #state_ty_ident::new(name, attrs, ctx).map(|ok| Self(::core::option::Option::Some(ok)))
                 }
             }
 
@@ -539,9 +540,9 @@ impl FromEventsStateMachine {
             impl ::xso::FromEventsBuilder for #builder_ty_ident {
                 type Output = #output_ty;
 
-                fn feed(&mut self, ev: ::xso::exports::rxml::Event) -> ::core::result::Result<::core::option::Option<Self::Output>, ::xso::error::Error> {
+                fn feed(&mut self, ev: ::xso::exports::rxml::Event, ctx: &::xso::Context<'_>) -> ::core::result::Result<::core::option::Option<Self::Output>, ::xso::error::Error> {
                     let inner = self.0.take().expect("feed called after completion");
-                    match inner.advance(ev)? {
+                    match inner.advance(ev, ctx)? {
                         ::core::ops::ControlFlow::Continue(mut value) => {
                             #validate_call
                             ::core::result::Result::Ok(::core::option::Option::Some(value))
@@ -558,6 +559,7 @@ impl FromEventsStateMachine {
                 fn new(
                     name: ::xso::exports::rxml::QName,
                     mut attrs: ::xso::exports::rxml::AttrMap,
+                    ctx: &::xso::Context<'_>,
                 ) -> ::core::result::Result<Self, ::xso::error::FromEventsError> {
                     #init_body
                     { let _ = &mut attrs; }

xso-proc/src/structs.rs πŸ”—

@@ -200,7 +200,7 @@ impl StructInner {
                             &from_xml_builder_ty,
                         )
                             .with_impl(quote! {
-                                match #feed_fn(&mut #builder_data_ident, ev)? {
+                                match #feed_fn(&mut #builder_data_ident, ev, ctx)? {
                                     ::core::option::Option::Some(result) => {
                                         ::core::result::Result::Ok(::core::ops::ControlFlow::Continue(#output_cons))
                                     }
@@ -213,7 +213,7 @@ impl StructInner {
                             })
                     ],
                     init: quote! {
-                        #from_events_fn(name, attrs).map(|#builder_data_ident| Self::#state_name { #builder_data_ident })
+                        #from_events_fn(name, attrs, ctx).map(|#builder_data_ident| Self::#state_name { #builder_data_ident })
                     },
                 })
             }
@@ -392,7 +392,7 @@ impl ItemDef for StructDef {
         Ok(FromXmlParts {
             defs,
             from_events_body: quote! {
-                #builder_ty_ident::new(#name_ident, #attrs_ident)
+                #builder_ty_ident::new(#name_ident, #attrs_ident, ctx)
             },
             builder_ty_ident: builder_ty_ident.clone(),
         })

xso/ChangeLog πŸ”—

@@ -17,6 +17,8 @@ Version NEXT:
         equivalent. This is because `xml:lang` is special as per
         XML 1.0 Β§ 2.12 and it is generally preferable to allow it to occur
         anywhere in the document.
+      - The `FromXml::from_events` and `FromEventsBuilder::feed` functions
+        gained an additional argument to support `xml:lang` inheritance.
     * Added
       - Support for child elements in derive macros. Child elements may also
         be wrapped in Option or Box or in containers like Vec or HashSet.
@@ -46,6 +48,8 @@ Version NEXT:
       - Implement `AsXml` and `FromXml` for serde_json::Value` behind
         `serde_json` feature.
       - Support for a post-deserialization callback function call. (!553)
+      - Support for correctly inheriting `xml:lang` values throughout the
+        XML document.
     * Changes
       - Generated AsXml iterator and FromXml builder types are now
         doc(hidden), to not clutter hand-written documentation with auto

xso/src/from_xml_doc.md πŸ”—

@@ -28,6 +28,7 @@ assert_eq!(foo, Foo);
     3. [`element` meta](#element-meta)
     4. [`extract` meta](#extract-meta)
     5. [`flag` meta](#flag-meta)
+    6. [`lang` meta](#lang-meta)
     6. [`text` meta](#text-meta)
 
 ## Attributes
@@ -627,6 +628,7 @@ error is emitted.
 When parsing, any contents within the child element generate a parse error.
 
 #### Example
+
 ```rust
 # use xso::FromXml;
 #[derive(FromXml, Debug, PartialEq)]
@@ -647,6 +649,64 @@ assert_eq!(foo, Foo {
 });
 ```
 
+### `lang` meta
+
+The `lang` meta allows to access the (potentially inherited) logical
+`xml:lang` value as defined in
+[XML 1.0 Β§ 2.12](https://www.w3.org/TR/REC-xml/#sec-lang-tag).
+
+This meta supports no arguments and can only be used on fields of type
+`Option<String>`.
+
+This meta should not be used alongsite `#[xml(attribute)]` meta which match
+the `xml:lang` attribute. Doing so causes unspecified behavior during
+serialisation: only one of the values will be in the output, but it is
+unspecified which of the two. This is the same as when having two
+`#[xml(attribute)]` field which match the same attribute. (Due to indirections
+when refering to `static` items for attribute namespaces and names, it is not
+possible to check this at compile-time.)
+
+Unlike `#[xml(attribute = "xml:lang")]`, the `#[xml(lang)]` meta takes
+inheritance into account.
+
+**Note:** Using this meta is not roundtrip-safe. `rxml` will always emit its
+value on serialisation, even if it was inherited during deserialisation.
+
+#### Example
+
+```rust
+# use xso::FromXml;
+#[derive(FromXml, Debug, PartialEq)]
+#[xml(namespace = "urn:example", name = "bar")]
+struct Bar {
+    #[xml(lang)]
+    lang: Option<String>,
+};
+
+#[derive(FromXml, Debug, PartialEq)]
+#[xml(namespace = "urn:example", name = "foo")]
+struct Foo {
+    #[xml(child)]
+    child: Bar,
+};
+
+// `xml:lang` gets inherited from <foo/> to <bar/>
+let foo: Foo = xso::from_bytes(b"<foo xmlns='urn:example' xml:lang='en'><bar/></foo>").unwrap();
+assert_eq!(foo, Foo {
+    child: Bar {
+        lang: Some("en".to_owned()),
+    },
+});
+
+// `xml:lang` gets set/overwritten in <bar/>
+let foo: Foo = xso::from_bytes(b"<foo xmlns='urn:example' xml:lang='en'><bar xml:lang='de'/></foo>").unwrap();
+assert_eq!(foo, Foo {
+    child: Bar {
+        lang: Some("de".to_owned()),
+    },
+});
+```
+
 ### `text` meta
 
 The `text` meta causes the field to be mapped to the text content of the

xso/src/fromxml.rs πŸ”—

@@ -15,7 +15,7 @@
 use alloc::boxed::Box;
 
 use crate::error::{Error, FromEventsError};
-use crate::{FromEventsBuilder, FromXml};
+use crate::{Context, FromEventsBuilder, FromXml};
 
 /// Helper struct to construct an `Option<T>` from XML events.
 pub struct OptionBuilder<T: FromEventsBuilder>(T);
@@ -23,8 +23,8 @@ pub struct OptionBuilder<T: FromEventsBuilder>(T);
 impl<T: FromEventsBuilder> FromEventsBuilder for OptionBuilder<T> {
     type Output = Option<T::Output>;
 
-    fn feed(&mut self, ev: rxml::Event) -> Result<Option<Self::Output>, Error> {
-        self.0.feed(ev).map(|ok| ok.map(Some))
+    fn feed(&mut self, ev: rxml::Event, ctx: &Context<'_>) -> Result<Option<Self::Output>, Error> {
+        self.0.feed(ev, ctx).map(|ok| ok.map(Some))
     }
 }
 
@@ -40,8 +40,9 @@ impl<T: FromXml> FromXml for Option<T> {
     fn from_events(
         name: rxml::QName,
         attrs: rxml::AttrMap,
+        ctx: &Context<'_>,
     ) -> Result<Self::Builder, FromEventsError> {
-        Ok(OptionBuilder(T::from_events(name, attrs)?))
+        Ok(OptionBuilder(T::from_events(name, attrs, ctx)?))
     }
 }
 
@@ -51,8 +52,8 @@ pub struct BoxBuilder<T: FromEventsBuilder>(Box<T>);
 impl<T: FromEventsBuilder> FromEventsBuilder for BoxBuilder<T> {
     type Output = Box<T::Output>;
 
-    fn feed(&mut self, ev: rxml::Event) -> Result<Option<Self::Output>, Error> {
-        self.0.feed(ev).map(|ok| ok.map(Box::new))
+    fn feed(&mut self, ev: rxml::Event, ctx: &Context<'_>) -> Result<Option<Self::Output>, Error> {
+        self.0.feed(ev, ctx).map(|ok| ok.map(Box::new))
     }
 }
 
@@ -63,8 +64,9 @@ impl<T: FromXml> FromXml for Box<T> {
     fn from_events(
         name: rxml::QName,
         attrs: rxml::AttrMap,
+        ctx: &Context<'_>,
     ) -> Result<Self::Builder, FromEventsError> {
-        Ok(BoxBuilder(Box::new(T::from_events(name, attrs)?)))
+        Ok(BoxBuilder(Box::new(T::from_events(name, attrs, ctx)?)))
     }
 }
 
@@ -92,7 +94,7 @@ pub struct FallibleBuilder<T: FromEventsBuilder, E>(FallibleBuilderInner<T, E>);
 impl<T: FromEventsBuilder, E: From<Error>> FromEventsBuilder for FallibleBuilder<T, E> {
     type Output = Result<T::Output, E>;
 
-    fn feed(&mut self, ev: rxml::Event) -> Result<Option<Self::Output>, Error> {
+    fn feed(&mut self, ev: rxml::Event, ctx: &Context<'_>) -> Result<Option<Self::Output>, Error> {
         match self.0 {
             FallibleBuilderInner::Processing {
                 ref mut depth,
@@ -130,7 +132,7 @@ impl<T: FromEventsBuilder, E: From<Error>> FromEventsBuilder for FallibleBuilder
                     rxml::Event::XmlDeclaration(..) | rxml::Event::Text(..) => Some(*depth),
                 };
 
-                match builder.feed(ev) {
+                match builder.feed(ev, ctx) {
                     Ok(Some(v)) => {
                         self.0 = FallibleBuilderInner::Done;
                         return Ok(Some(Ok(v)));
@@ -214,8 +216,9 @@ impl<T: FromXml, E: From<Error>> FromXml for Result<T, E> {
     fn from_events(
         name: rxml::QName,
         attrs: rxml::AttrMap,
+        ctx: &Context<'_>,
     ) -> Result<Self::Builder, FromEventsError> {
-        match T::from_events(name, attrs) {
+        match T::from_events(name, attrs, ctx) {
             Ok(builder) => Ok(FallibleBuilder(FallibleBuilderInner::Processing {
                 depth: 0,
                 builder,
@@ -248,7 +251,7 @@ impl Discard {
 impl FromEventsBuilder for Discard {
     type Output = ();
 
-    fn feed(&mut self, ev: rxml::Event) -> Result<Option<Self::Output>, Error> {
+    fn feed(&mut self, ev: rxml::Event, _ctx: &Context<'_>) -> Result<Option<Self::Output>, Error> {
         match ev {
             rxml::Event::StartElement(..) => {
                 self.depth = match self.depth.checked_add(1) {
@@ -284,7 +287,7 @@ pub struct EmptyBuilder {
 impl FromEventsBuilder for EmptyBuilder {
     type Output = ();
 
-    fn feed(&mut self, ev: rxml::Event) -> Result<Option<Self::Output>, Error> {
+    fn feed(&mut self, ev: rxml::Event, _ctx: &Context<'_>) -> Result<Option<Self::Output>, Error> {
         match ev {
             rxml::Event::EndElement(..) => Ok(Some(())),
             rxml::Event::StartElement(..) => Err(Error::Other(self.childerr)),
@@ -336,7 +339,11 @@ mod tests {
             impl FromEventsBuilder for $name {
                 type Output = $output;
 
-                fn feed(&mut self, _: Event) -> Result<Option<Self::Output>, Error> {
+                fn feed(
+                    &mut self,
+                    _: Event,
+                    _: &Context<'_>,
+                ) -> Result<Option<Self::Output>, Error> {
                     unreachable!();
                 }
             }
@@ -355,6 +362,7 @@ mod tests {
         fn from_events(
             name: rxml::QName,
             attrs: rxml::AttrMap,
+            _ctx: &Context<'_>,
         ) -> Result<Self::Builder, FromEventsError> {
             Err(FromEventsError::Mismatch { name, attrs })
         }
@@ -366,7 +374,11 @@ mod tests {
     impl FromXml for InitialError {
         type Builder = InitialErrorBuilder;
 
-        fn from_events(_: rxml::QName, _: rxml::AttrMap) -> Result<Self::Builder, FromEventsError> {
+        fn from_events(
+            _: rxml::QName,
+            _: rxml::AttrMap,
+            _: &Context<'_>,
+        ) -> Result<Self::Builder, FromEventsError> {
             Err(FromEventsError::Invalid(Error::Other("some error")))
         }
     }
@@ -377,7 +389,7 @@ mod tests {
     impl FromEventsBuilder for FailOnContentBuilder {
         type Output = FailOnContent;
 
-        fn feed(&mut self, _: Event) -> Result<Option<Self::Output>, Error> {
+        fn feed(&mut self, _: Event, _: &Context<'_>) -> Result<Option<Self::Output>, Error> {
             Err(Error::Other("content error"))
         }
     }
@@ -388,7 +400,11 @@ mod tests {
     impl FromXml for FailOnContent {
         type Builder = FailOnContentBuilder;
 
-        fn from_events(_: rxml::QName, _: rxml::AttrMap) -> Result<Self::Builder, FromEventsError> {
+        fn from_events(
+            _: rxml::QName,
+            _: rxml::AttrMap,
+            _: &Context<'_>,
+        ) -> Result<Self::Builder, FromEventsError> {
             Ok(FailOnContentBuilder)
         }
     }
@@ -403,7 +419,7 @@ mod tests {
 
     #[test]
     fn fallible_builder_mismatch_passthrough() {
-        match Result::<AlwaysMismatch, Error>::from_events(qname(), attrs()) {
+        match Result::<AlwaysMismatch, Error>::from_events(qname(), attrs(), &Context::empty()) {
             Err(FromEventsError::Mismatch { .. }) => (),
             other => panic!("unexpected result: {:?}", other),
         }
@@ -411,15 +427,19 @@ mod tests {
 
     #[test]
     fn fallible_builder_initial_error_capture() {
-        let mut builder = match Result::<InitialError, Error>::from_events(qname(), attrs()) {
+        let ctx = Context::empty();
+        let mut builder = match Result::<InitialError, Error>::from_events(qname(), attrs(), &ctx) {
             Ok(v) => v,
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::Text(EventMetrics::zero(), "hello world!".to_owned())) {
+        match builder.feed(
+            Event::Text(EventMetrics::zero(), "hello world!".to_owned()),
+            &ctx,
+        ) {
             Ok(None) => (),
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::EndElement(EventMetrics::zero())) {
+        match builder.feed(Event::EndElement(EventMetrics::zero()), &ctx) {
             Ok(Some(Err(Error::Other("some error")))) => (),
             other => panic!("unexpected result: {:?}", other),
         };
@@ -427,47 +447,66 @@ mod tests {
 
     #[test]
     fn fallible_builder_initial_error_capture_allows_nested_stuff() {
-        let mut builder = match Result::<InitialError, Error>::from_events(qname(), attrs()) {
+        let ctx = Context::empty();
+        let mut builder = match Result::<InitialError, Error>::from_events(qname(), attrs(), &ctx) {
             Ok(v) => v,
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::StartElement(EventMetrics::zero(), qname(), attrs())) {
+        match builder.feed(
+            Event::StartElement(EventMetrics::zero(), qname(), attrs()),
+            &ctx,
+        ) {
             Ok(None) => (),
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::Text(EventMetrics::zero(), "hello world!".to_owned())) {
+        match builder.feed(
+            Event::Text(EventMetrics::zero(), "hello world!".to_owned()),
+            &ctx,
+        ) {
             Ok(None) => (),
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::EndElement(EventMetrics::zero())) {
+        match builder.feed(Event::EndElement(EventMetrics::zero()), &ctx) {
             Ok(None) => (),
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::Text(EventMetrics::zero(), "hello world!".to_owned())) {
+        match builder.feed(
+            Event::Text(EventMetrics::zero(), "hello world!".to_owned()),
+            &ctx,
+        ) {
             Ok(None) => (),
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::StartElement(EventMetrics::zero(), qname(), attrs())) {
+        match builder.feed(
+            Event::StartElement(EventMetrics::zero(), qname(), attrs()),
+            &ctx,
+        ) {
             Ok(None) => (),
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::StartElement(EventMetrics::zero(), qname(), attrs())) {
+        match builder.feed(
+            Event::StartElement(EventMetrics::zero(), qname(), attrs()),
+            &ctx,
+        ) {
             Ok(None) => (),
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::Text(EventMetrics::zero(), "hello world!".to_owned())) {
+        match builder.feed(
+            Event::Text(EventMetrics::zero(), "hello world!".to_owned()),
+            &ctx,
+        ) {
             Ok(None) => (),
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::EndElement(EventMetrics::zero())) {
+        match builder.feed(Event::EndElement(EventMetrics::zero()), &ctx) {
             Ok(None) => (),
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::EndElement(EventMetrics::zero())) {
+        match builder.feed(Event::EndElement(EventMetrics::zero()), &ctx) {
             Ok(None) => (),
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::EndElement(EventMetrics::zero())) {
+        match builder.feed(Event::EndElement(EventMetrics::zero()), &ctx) {
             Ok(Some(Err(Error::Other("some error")))) => (),
             other => panic!("unexpected result: {:?}", other),
         };
@@ -475,11 +514,13 @@ mod tests {
 
     #[test]
     fn fallible_builder_content_error_capture() {
-        let mut builder = match Result::<FailOnContent, Error>::from_events(qname(), attrs()) {
+        let ctx = Context::empty();
+        let mut builder = match Result::<FailOnContent, Error>::from_events(qname(), attrs(), &ctx)
+        {
             Ok(v) => v,
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::EndElement(EventMetrics::zero())) {
+        match builder.feed(Event::EndElement(EventMetrics::zero()), &ctx) {
             Ok(Some(Err(Error::Other("content error")))) => (),
             other => panic!("unexpected result: {:?}", other),
         };
@@ -487,15 +528,20 @@ mod tests {
 
     #[test]
     fn fallible_builder_content_error_capture_with_more_content() {
-        let mut builder = match Result::<FailOnContent, Error>::from_events(qname(), attrs()) {
+        let ctx = Context::empty();
+        let mut builder = match Result::<FailOnContent, Error>::from_events(qname(), attrs(), &ctx)
+        {
             Ok(v) => v,
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::Text(EventMetrics::zero(), "hello world!".to_owned())) {
+        match builder.feed(
+            Event::Text(EventMetrics::zero(), "hello world!".to_owned()),
+            &ctx,
+        ) {
             Ok(None) => (),
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::EndElement(EventMetrics::zero())) {
+        match builder.feed(Event::EndElement(EventMetrics::zero()), &ctx) {
             Ok(Some(Err(Error::Other("content error")))) => (),
             other => panic!("unexpected result: {:?}", other),
         };
@@ -503,47 +549,67 @@ mod tests {
 
     #[test]
     fn fallible_builder_content_error_capture_with_nested_content() {
-        let mut builder = match Result::<FailOnContent, Error>::from_events(qname(), attrs()) {
+        let ctx = Context::empty();
+        let mut builder = match Result::<FailOnContent, Error>::from_events(qname(), attrs(), &ctx)
+        {
             Ok(v) => v,
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::StartElement(EventMetrics::zero(), qname(), attrs())) {
+        match builder.feed(
+            Event::StartElement(EventMetrics::zero(), qname(), attrs()),
+            &ctx,
+        ) {
             Ok(None) => (),
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::Text(EventMetrics::zero(), "hello world!".to_owned())) {
+        match builder.feed(
+            Event::Text(EventMetrics::zero(), "hello world!".to_owned()),
+            &ctx,
+        ) {
             Ok(None) => (),
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::EndElement(EventMetrics::zero())) {
+        match builder.feed(Event::EndElement(EventMetrics::zero()), &ctx) {
             Ok(None) => (),
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::Text(EventMetrics::zero(), "hello world!".to_owned())) {
+        match builder.feed(
+            Event::Text(EventMetrics::zero(), "hello world!".to_owned()),
+            &ctx,
+        ) {
             Ok(None) => (),
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::StartElement(EventMetrics::zero(), qname(), attrs())) {
+        match builder.feed(
+            Event::StartElement(EventMetrics::zero(), qname(), attrs()),
+            &ctx,
+        ) {
             Ok(None) => (),
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::StartElement(EventMetrics::zero(), qname(), attrs())) {
+        match builder.feed(
+            Event::StartElement(EventMetrics::zero(), qname(), attrs()),
+            &ctx,
+        ) {
             Ok(None) => (),
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::Text(EventMetrics::zero(), "hello world!".to_owned())) {
+        match builder.feed(
+            Event::Text(EventMetrics::zero(), "hello world!".to_owned()),
+            &ctx,
+        ) {
             Ok(None) => (),
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::EndElement(EventMetrics::zero())) {
+        match builder.feed(Event::EndElement(EventMetrics::zero()), &ctx) {
             Ok(None) => (),
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::EndElement(EventMetrics::zero())) {
+        match builder.feed(Event::EndElement(EventMetrics::zero()), &ctx) {
             Ok(None) => (),
             other => panic!("unexpected result: {:?}", other),
         };
-        match builder.feed(Event::EndElement(EventMetrics::zero())) {
+        match builder.feed(Event::EndElement(EventMetrics::zero()), &ctx) {
             Ok(Some(Err(Error::Other("content error")))) => (),
             other => panic!("unexpected result: {:?}", other),
         };

xso/src/lib.rs πŸ”—

@@ -128,6 +128,33 @@ pub trait AsXml {
     fn as_xml_iter(&self) -> Result<Self::ItemIter<'_>, self::error::Error>;
 }
 
+/// Additional parsing context supplied to [`FromEventsBuilder`]
+/// implementations.
+pub struct Context<'x> {
+    language: Option<&'x str>,
+}
+
+impl<'x> Context<'x> {
+    /// A context suitable for the beginning of the document.
+    pub fn empty() -> Self {
+        Self { language: None }
+    }
+
+    /// Create a new context.
+    ///
+    /// - `language` must be the effective value of the `xml:lang` value at
+    ///   the end of the current event.
+    pub fn new(language: Option<&'x str>) -> Self {
+        Self { language }
+    }
+
+    /// Return the `xml:lang` value in effect at the end of the event which
+    /// is currently being processed.
+    pub fn language(&self) -> Option<&str> {
+        self.language.as_deref()
+    }
+}
+
 /// Trait for a temporary object allowing to construct a struct from
 /// [`rxml::Event`] items.
 ///
@@ -151,7 +178,11 @@ pub trait FromEventsBuilder {
     /// Feeding more events after an error may result in panics, errors or
     /// inconsistent result data, though it may never result in unsound or
     /// unsafe behaviour.
-    fn feed(&mut self, ev: rxml::Event) -> Result<Option<Self::Output>, self::error::Error>;
+    fn feed(
+        &mut self,
+        ev: rxml::Event,
+        ctx: &Context<'_>,
+    ) -> Result<Option<Self::Output>, self::error::Error>;
 }
 
 /// Trait allowing to construct a struct from a stream of
@@ -188,6 +219,7 @@ pub trait FromXml {
     fn from_events(
         name: rxml::QName,
         attrs: rxml::AttrMap,
+        ctx: &Context<'_>,
     ) -> Result<Self::Builder, self::error::FromEventsError>;
 }
 
@@ -423,13 +455,15 @@ impl UnknownChildPolicy {
 /// Attempt to transform a type implementing [`AsXml`] into another
 /// type which implements [`FromXml`].
 pub fn transform<T: FromXml, F: AsXml>(from: &F) -> Result<T, self::error::Error> {
+    let mut languages = rxml::xml_lang::XmlLangStack::new();
     let mut iter = self::rxml_util::ItemToEvent::new(from.as_xml_iter()?);
     let (qname, attrs) = match iter.next() {
         Some(Ok(rxml::Event::StartElement(_, qname, attrs))) => (qname, attrs),
         Some(Err(e)) => return Err(e),
         _ => panic!("into_event_iter did not start with StartElement event!"),
     };
-    let mut sink = match T::from_events(qname, attrs) {
+    languages.push_from_attrs(&attrs);
+    let mut sink = match T::from_events(qname, attrs, &Context::new(languages.current())) {
         Ok(v) => v,
         Err(self::error::FromEventsError::Mismatch { .. }) => {
             return Err(self::error::Error::TypeMismatch)
@@ -438,7 +472,8 @@ pub fn transform<T: FromXml, F: AsXml>(from: &F) -> Result<T, self::error::Error
     };
     for event in iter {
         let event = event?;
-        if let Some(v) = sink.feed(event)? {
+        languages.handle_event(&event);
+        if let Some(v) = sink.feed(event, &Context::new(languages.current()))? {
             return Ok(v);
         }
     }
@@ -455,8 +490,11 @@ pub fn transform<T: FromXml, F: AsXml>(from: &F) -> Result<T, self::error::Error
 pub fn try_from_element<T: FromXml>(
     from: minidom::Element,
 ) -> Result<T, self::error::FromElementError> {
+    let mut languages = rxml::xml_lang::XmlLangStack::new();
     let (qname, attrs) = minidom_compat::make_start_ev_parts(&from)?;
-    let mut sink = match T::from_events(qname, attrs) {
+
+    languages.push_from_attrs(&attrs);
+    let mut sink = match T::from_events(qname, attrs, &Context::new(languages.current())) {
         Ok(v) => v,
         Err(self::error::FromEventsError::Mismatch { .. }) => {
             return Err(self::error::FromElementError::Mismatch(from))
@@ -486,7 +524,8 @@ pub fn try_from_element<T: FromXml>(
     let iter = self::rxml_util::ItemToEvent::new(iter);
     for event in iter {
         let event = event?;
-        if let Some(v) = sink.feed(event)? {
+        languages.handle_event(&event);
+        if let Some(v) = sink.feed(event, &Context::new(languages.current()))? {
             return Ok(v);
         }
     }
@@ -508,8 +547,8 @@ fn map_nonio_error<T>(r: Result<T, io::Error>) -> Result<T, self::error::Error>
 }
 
 #[cfg(feature = "std")]
-fn read_start_event<I: io::BufRead>(
-    r: &mut rxml::Reader<I>,
+fn read_start_event(
+    r: &mut impl Iterator<Item = io::Result<rxml::Event>>,
 ) -> Result<(rxml::QName, rxml::AttrMap), self::error::Error> {
     for ev in r {
         match map_nonio_error(ev)? {
@@ -531,17 +570,17 @@ fn read_start_event<I: io::BufRead>(
 /// containing XML data.
 #[cfg(feature = "std")]
 pub fn from_bytes<T: FromXml>(mut buf: &[u8]) -> Result<T, self::error::Error> {
-    let mut reader = rxml::Reader::new(&mut buf);
+    let mut reader = rxml::XmlLangTracker::wrap(rxml::Reader::new(&mut buf));
     let (name, attrs) = read_start_event(&mut reader)?;
-    let mut builder = match T::from_events(name, attrs) {
+    let mut builder = match T::from_events(name, attrs, &Context::new(reader.language())) {
         Ok(v) => v,
         Err(self::error::FromEventsError::Mismatch { .. }) => {
             return Err(self::error::Error::TypeMismatch)
         }
         Err(self::error::FromEventsError::Invalid(e)) => return Err(e),
     };
-    for ev in reader {
-        if let Some(v) = builder.feed(map_nonio_error(ev)?)? {
+    while let Some(ev) = reader.next() {
+        if let Some(v) = builder.feed(map_nonio_error(ev)?, &Context::new(reader.language()))? {
             return Ok(v);
         }
     }
@@ -549,8 +588,8 @@ pub fn from_bytes<T: FromXml>(mut buf: &[u8]) -> Result<T, self::error::Error> {
 }
 
 #[cfg(feature = "std")]
-fn read_start_event_io<I: io::BufRead>(
-    r: &mut rxml::Reader<I>,
+fn read_start_event_io(
+    r: &mut impl Iterator<Item = io::Result<rxml::Event>>,
 ) -> io::Result<(rxml::QName, rxml::AttrMap)> {
     for ev in r {
         match ev? {
@@ -575,9 +614,9 @@ fn read_start_event_io<I: io::BufRead>(
 /// Attempt to parse a type implementing [`FromXml`] from a reader.
 #[cfg(feature = "std")]
 pub fn from_reader<T: FromXml, R: io::BufRead>(r: R) -> io::Result<T> {
-    let mut reader = rxml::Reader::new(r);
+    let mut reader = rxml::XmlLangTracker::wrap(rxml::Reader::new(r));
     let (name, attrs) = read_start_event_io(&mut reader)?;
-    let mut builder = match T::from_events(name, attrs) {
+    let mut builder = match T::from_events(name, attrs, &Context::new(reader.language())) {
         Ok(v) => v,
         Err(self::error::FromEventsError::Mismatch { .. }) => {
             return Err(self::error::Error::TypeMismatch)
@@ -587,9 +626,9 @@ pub fn from_reader<T: FromXml, R: io::BufRead>(r: R) -> io::Result<T> {
             return Err(e).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
         }
     };
-    for ev in reader {
+    while let Some(ev) = reader.next() {
         if let Some(v) = builder
-            .feed(ev?)
+            .feed(ev?, &Context::new(reader.language()))
             .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
         {
             return Ok(v);

xso/src/minidom_compat.rs πŸ”—

@@ -24,7 +24,7 @@ use rxml::{
 use crate::{
     error::{Error, FromEventsError},
     rxml_util::{EventToItem, Item},
-    AsXml, FromEventsBuilder, FromXml,
+    AsXml, Context, FromEventsBuilder, FromXml,
 };
 
 /// State machine for converting a minidom Element into rxml events.
@@ -351,13 +351,13 @@ impl ElementFromEvents {
 impl FromEventsBuilder for ElementFromEvents {
     type Output = minidom::Element;
 
-    fn feed(&mut self, ev: Event) -> Result<Option<Self::Output>, Error> {
+    fn feed(&mut self, ev: Event, ctx: &Context<'_>) -> Result<Option<Self::Output>, Error> {
         let inner = self
             .inner
             .as_mut()
             .expect("feed() called after it finished");
         if let Some(nested) = self.nested.as_mut() {
-            match nested.feed(ev)? {
+            match nested.feed(ev, ctx)? {
                 Some(v) => {
                     inner.append_child(v);
                     self.nested = None;
@@ -369,7 +369,7 @@ impl FromEventsBuilder for ElementFromEvents {
         match ev {
             Event::XmlDeclaration(_, _) => Ok(None),
             Event::StartElement(_, qname, attrs) => {
-                let nested = match Element::from_events(qname, attrs) {
+                let nested = match Element::from_events(qname, attrs, ctx) {
                     Ok(v) => v,
                     Err(FromEventsError::Invalid(e)) => return Err(e),
                     Err(FromEventsError::Mismatch { .. }) => {
@@ -394,6 +394,7 @@ impl FromXml for Element {
     fn from_events(
         qname: rxml::QName,
         attrs: rxml::AttrMap,
+        _ctx: &Context<'_>,
     ) -> Result<Self::Builder, FromEventsError> {
         Ok(Self::Builder::new(qname, attrs))
     }
@@ -418,7 +419,13 @@ where
     pub fn new(qname: rxml::QName, attrs: rxml::AttrMap) -> Result<Self, FromEventsError> {
         Ok(Self {
             _phantom: PhantomData,
-            inner: Element::from_events(qname, attrs)?,
+            inner: Element::from_events(
+                qname,
+                attrs,
+                // FromEventsViaElement does not support passing through
+                // `xml:lang` inheritance, so we don't pass any context.
+                &Context::empty(),
+            )?,
         })
     }
 }
@@ -429,8 +436,8 @@ where
 {
     type Output = T;
 
-    fn feed(&mut self, ev: Event) -> Result<Option<Self::Output>, Error> {
-        match self.inner.feed(ev) {
+    fn feed(&mut self, ev: Event, ctx: &Context<'_>) -> Result<Option<Self::Output>, Error> {
+        match self.inner.feed(ev, ctx) {
             Ok(Some(v)) => Ok(Some(v.try_into()?)),
             Ok(None) => Ok(None),
             Err(e) => Err(e),