diff --git a/packages/mrml-core/src/prelude/parser/mod.rs b/packages/mrml-core/src/prelude/parser/mod.rs index c0e538ad..9f368d55 100644 --- a/packages/mrml-core/src/prelude/parser/mod.rs +++ b/packages/mrml-core/src/prelude/parser/mod.rs @@ -1,6 +1,7 @@ use std::marker::PhantomData; use htmlparser::{StrSpan, Tokenizer}; +use indexmap::map::Entry; use self::loader::IncludeLoaderError; use super::hash::Map; @@ -394,10 +395,14 @@ pub(crate) fn parse_attributes_map( ) -> Result>, Error> { let mut result = Map::new(); while let Some(attr) = cursor.next_attribute()? { - result.insert( - attr.qualified_name(), - attr.value.map(|inner| inner.to_string()), - ); + match result.entry(attr.qualified_name()) { + Entry::Vacant(slot) => { + slot.insert(attr.value.map(|inner| inner.to_string())); + } + Entry::Occupied(_) => { + cursor.add_warning(WarningKind::DuplicateAttribute, attr.span); + } + } } Ok(result) } @@ -630,3 +635,32 @@ macro_rules! should_not_async_parse { }); }; } + +#[cfg(test)] +mod tests { + use super::{MrmlCursor, MrmlParser, ParserOptions, WarningKind}; + use crate::mj_text::MjText; + + #[test] + fn should_warn_and_keep_first_value_on_duplicate_attribute() { + let raw = r#"hi"#; + let opts = ParserOptions::default(); + let parser = MrmlParser::new(&opts); + let mut cursor = MrmlCursor::new(raw); + let element: MjText = parser.parse_root(&mut cursor).unwrap(); + + assert_eq!( + element + .attributes + .get("font-size") + .and_then(|v| v.as_deref()), + Some("12px") + ); + + let warnings = cursor.warnings(); + assert_eq!(warnings.len(), 1); + assert_eq!(warnings[0].kind, WarningKind::DuplicateAttribute); + // The span should point at the second occurrence, not the first. + assert_eq!(warnings[0].span.start, raw.rfind("font-size").unwrap()); + } +} diff --git a/packages/mrml-core/src/prelude/parser/output.rs b/packages/mrml-core/src/prelude/parser/output.rs index 00f9cada..d9f35e49 100644 --- a/packages/mrml-core/src/prelude/parser/output.rs +++ b/packages/mrml-core/src/prelude/parser/output.rs @@ -6,6 +6,7 @@ pub struct ParseOutput { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum WarningKind { UnexpectedAttribute, + DuplicateAttribute, InlineStyleUnsupported, } @@ -13,6 +14,7 @@ impl WarningKind { pub const fn as_str(&self) -> &'static str { match self { Self::UnexpectedAttribute => "unexpected-attribute", + Self::DuplicateAttribute => "duplicate-attribute", Self::InlineStyleUnsupported => "inline-style-unsupported", } } @@ -22,6 +24,7 @@ impl std::fmt::Display for WarningKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::UnexpectedAttribute => f.write_str("unexpected attribute"), + Self::DuplicateAttribute => f.write_str("duplicate attribute"), Self::InlineStyleUnsupported => { f.write_str("inlining styles is not supported in this build") } diff --git a/packages/mrml-wasm/src/parser/mod.rs b/packages/mrml-wasm/src/parser/mod.rs index 46e43679..835d1d31 100644 --- a/packages/mrml-wasm/src/parser/mod.rs +++ b/packages/mrml-wasm/src/parser/mod.rs @@ -225,6 +225,7 @@ impl From for Span { #[tsify(into_wasm_abi)] pub enum WarningKind { UnexpectedAttributes, + DuplicateAttribute, InlineStyleUnsupported, } @@ -232,6 +233,7 @@ impl From for WarningKind { fn from(value: mrml::prelude::parser::WarningKind) -> Self { match value { mrml::prelude::parser::WarningKind::UnexpectedAttribute => Self::UnexpectedAttributes, + mrml::prelude::parser::WarningKind::DuplicateAttribute => Self::DuplicateAttribute, mrml::prelude::parser::WarningKind::InlineStyleUnsupported => { Self::InlineStyleUnsupported }