From 910130eff38258fcf3b6a0c48296aebe6da27cfd Mon Sep 17 00:00:00 2001 From: John MacFarlane Date: Wed, 9 Sep 2026 02:59:30 +0000 Subject: [PATCH] HTML reader: let the first of duplicate attributes win. toStringAttr deduplicated attributes with a right fold, so the last occurrence of a duplicated attribute was kept. HTML specifies that the first occurrence wins (and that lang takes precedence over xml:lang). Deduplicate left to right with a seen-set instead. Co-Authored-By: Claude --- src/Text/Pandoc/Readers/HTML/Parsing.hs | 27 ++++++++++++++----------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/Text/Pandoc/Readers/HTML/Parsing.hs b/src/Text/Pandoc/Readers/HTML/Parsing.hs index faa277be3..a91ad4cfe 100644 --- a/src/Text/Pandoc/Readers/HTML/Parsing.hs +++ b/src/Text/Pandoc/Readers/HTML/Parsing.hs @@ -199,20 +199,23 @@ t1 `closes` t2 | _ `closes` _ = False toStringAttr :: [(Text, Text)] -> [(Text, Text)] -toStringAttr = foldr go [] +toStringAttr = go mempty where - go :: (Text, Text) -> [(Text, Text)] -> [(Text, Text)] + go :: Set.Set Text -> [(Text, Text)] -> [(Text, Text)] + go _ [] = [] + go seen ((x,y):rest) + -- prevent duplicate attributes; the first one wins: + | x' `Set.member` seen = go seen rest + | otherwise = (x', y) : go (Set.insert x' seen) rest + where x' = normalizeName x -- treat xml:lang as lang - go ("xml:lang",y) ats = go ("lang",y) ats - -- prevent duplicate attributes - go (x,y) ats - | any (\(x',_) -> x == x') ats = ats - | otherwise = - case T.stripPrefix "data-" x of - Just x' | x' `Set.notMember` (html5Attributes <> - html4Attributes <> rdfaAttributes) - -> go (x',y) ats - _ -> (x,y):ats + normalizeName "xml:lang" = "lang" + -- strip data- prefix unless the bare name is a standard attribute + normalizeName x = + case T.stripPrefix "data-" x of + Just x' | x' `Set.notMember` standardAttrs -> x' + _ -> x + standardAttrs = html5Attributes <> html4Attributes <> rdfaAttributes -- Unlike fromAttrib from tagsoup, this distinguishes -- between a missing attribute and an attribute with empty content.