diff --git a/CHANGES.txt b/CHANGES.txt
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,7 @@
 Changelog for Hexml
 
+0.3.5, released 2024-08-25
+    #1, add support for <![CDATA[ ... ]]>
 0.3.4, released 2018-09-10
     #15, licensed under BSD-3-Clause OR Apache-2.0
 0.3.3, released 2017-10-27
diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright Neil Mitchell 2016-2018.
+Copyright Neil Mitchell 2016-2024.
 
 Licensed under either of:
 
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Hexml [![Hackage version](https://img.shields.io/hackage/v/hexml.svg?label=Hackage)](https://hackage.haskell.org/package/hexml) [![Stackage version](https://www.stackage.org/package/hexml/badge/nightly?label=Stackage)](https://www.stackage.org/package/hexml) [![Linux Build Status](https://img.shields.io/travis/ndmitchell/hexml/master.svg?label=Linux%20build)](https://travis-ci.org/ndmitchell/hexml) [![Windows Build Status](https://img.shields.io/appveyor/ci/ndmitchell/hexml/master.svg?label=Windows%20build)](https://ci.appveyor.com/project/ndmitchell/hexml)
+# Hexml [![Hackage version](https://img.shields.io/hackage/v/hexml.svg?label=Hackage)](https://hackage.haskell.org/package/hexml) [![Stackage version](https://www.stackage.org/package/hexml/badge/nightly?label=Stackage)](https://www.stackage.org/package/hexml) [![Build status](https://img.shields.io/github/actions/workflow/status/ndmitchell/hexml/ci.yml?branch=master)](https://github.com/ndmitchell/hexml/actions)
 
 An XML DOM-style parser, that only parses a subset of XML, but is designed to be fast. In particular:
 
@@ -8,8 +8,10 @@
 
 The name "hexml" is a combination of "Hex" (a curse) and "XML". The "X" should not be capitalised because the parser is more curse and less XML.
 
-Hexml may be suitable if you want to quickly parse XML, from known sources, and a full XML parser has been shown to be a bottleneck. As an alternative to hexml, which supports things like entities but is still pretty fast, see [Pugixml](http://pugixml.org/) (with a [Haskell binding](https://hackage.haskell.org/package/pugixml)).
+Hexml may be suitable if you want to quickly parse XML, from known sources, and a full XML parser has been shown to be a bottleneck. As an alternative to hexml, which supports things like entities but is still pretty fast, see [Pugixml](http://pugixml.org/) (with a [Haskell binding](https://hackage.haskell.org/package/pugixml) - but be aware the Haskell binding [can segfault](https://github.com/philopon/pugixml-hs/issues/5)).
 
 Hexml is tested with [AFL](http://lcamtuf.coredump.cx/afl/).
 
 If you want lenses for Hexml, see [hexml-lens](http://hackage.haskell.org/package/hexml-lens).
+
+The optimisation work around Hexml spawned [Xeno](http://hackage.haskell.org/package/xeno), a Haskell-only alternative to Hexml. There is a [talk](https://ndmitchell.com/#hexml_12_oct_2017) covering the performance tricks of Hexml and Xeno.
diff --git a/cbits/hexml.c b/cbits/hexml.c
--- a/cbits/hexml.c
+++ b/cbits/hexml.c
@@ -40,7 +40,7 @@
     // have a cursor at the end, which is stack scoped, so children write out, then I do
     // when you commit, you copy over from end to front
 
-    // nodes 
+    // nodes
     int size;
     int used_front; // front entries, stored for good
     int used_back; // back entries, stack based, copied into front
@@ -466,26 +466,44 @@
             // have found a </
             break;
         }
-        else if (peek_at(d, 1) == '!' && peek_at(d, 2) == '-' && peek_at(d, 3) == '-')
+        else if (peek_at(d, 1) == '!')
         {
-            skip(d, 3);
-            // you can't reuse the two '-' characters for the closing as well
-            if (peek_at(d, 0) == '\0' || peek_at(d, 1) == '\0')
-            {
-                set_error(d, "Didn't get a closing comment");
-                return start_end(0, 0);
-            }
-            skip(d, 2);
-            for (;;)
+            if (peek_at(d, 2) == '-' && peek_at(d, 3) == '-')
             {
-                if (!find(d, '>'))
+                skip(d, 3);
+                // you can't reuse the two '-' characters for the closing as well
+                if (peek_at(d, 0) == '\0' || peek_at(d, 1) == '\0')
                 {
                     set_error(d, "Didn't get a closing comment");
                     return start_end(0, 0);
                 }
-                skip(d, 1);
-                if (peek_at(d, -3) == '-' && peek_at(d, -2) == '-')
-                    break;
+                skip(d, 2);
+                for (;;)
+                {
+                    if (!find(d, '>'))
+                    {
+                        set_error(d, "Didn't get a closing comment");
+                        return start_end(0, 0);
+                    }
+                    skip(d, 1);
+                    if (peek_at(d, -3) == '-' && peek_at(d, -2) == '-')
+                        break;
+                }
+            } else if (d->end - d->cursor >= 9 && memcmp(d->cursor + 2, "[CDATA[", 7) == 0) {
+                skip(d, 9);
+                for (;;)
+                {
+                    if (!find(d, '>'))
+                    {
+                        set_error(d, "Didn't close CDATA");
+                        return start_end(0, 0);
+                    }
+                    skip(d, 1);
+                    if (peek_at(d, -3) == ']' && peek_at(d, -2) == ']')
+                        break;
+                }
+            } else {
+                parse_tag(d);
             }
         }
         else
@@ -539,7 +557,7 @@
     d->nodes.nodes[0].outer = start_length(0, slen);
     d->nodes.nodes[0].inner = start_length(0, slen);
     d->nodes.nodes[0].attrs = start_length(0, 0);
-    
+
     // Introduce an intermediate result, otherwise behaviour is undefined
     // because there is no guaranteed ordering between LHS and RHS evaluation
     str content = parse_content(d);
diff --git a/hexml.cabal b/hexml.cabal
--- a/hexml.cabal
+++ b/hexml.cabal
@@ -1,20 +1,20 @@
-cabal-version:      >= 1.18
+cabal-version:      1.18
 build-type:         Simple
 name:               hexml
-version:            0.3.4
+version:            0.3.5
 license:            BSD3
 x-license:          BSD-3-Clause OR Apache-2.0
 license-file:       LICENSE
 category:           XML
 author:             Neil Mitchell <ndmitchell@gmail.com>
 maintainer:         Neil Mitchell <ndmitchell@gmail.com>
-copyright:          Neil Mitchell 2016-2018
+copyright:          Neil Mitchell 2016-2024
 synopsis:           XML subset DOM parser
 description:
     An XML DOM-style parser, that only parses a subset of XML, but is designed to be fast.
 homepage:           https://github.com/ndmitchell/hexml#readme
 bug-reports:        https://github.com/ndmitchell/hexml/issues
-tested-with:        GHC==8.4.3, GHC==8.2.2, GHC==8.0.2, GHC==7.10.3, GHC==7.8.4, GHC==7.6.3
+tested-with:        GHC==9.6, GHC==9.4, GHC==9.2, GHC==9.0, GHC==8.10, GHC==8.8
 extra-doc-files:
     README.md
     CHANGES.txt
diff --git a/src/Main.hs b/src/Main.hs
--- a/src/Main.hs
+++ b/src/Main.hs
@@ -20,6 +20,7 @@
     ,(False, "<test")
     ,(True, "<?xml version=\"1.1\"?>\n<greeting>Hello, world!</greeting>")
     ,(True, "<foo bar.baz=\"qux\"></foo>")
+    ,(True, "<test><![CDATA[foo<x>]]</y>]><z>]baz]]></test>")
     ]
 
 main :: IO ()
@@ -44,12 +45,12 @@
     attributes (head $ children doc) === [Attribute "id" "1", Attribute "extra" "2"]
     map (`attributeBy` "id") (childrenBy doc "test") === map (fmap (Attribute "id")) [Just "1", Just "2", Just "4", Nothing]
 
-    Right _ <- return $ parse $ "<test " <> BS.unwords [BS.pack $ "x" ++ show i ++ "='value'" | i <- [1..10000]] <> " />"
-    Right _ <- return $ parse $ BS.unlines $ replicate 10000 "<test x='value' />"
+    Right _ <- pure $ parse $ "<test " <> BS.unwords [BS.pack $ "x" ++ show i ++ "='value'" | i <- [1..10000]] <> " />"
+    Right _ <- pure $ parse $ BS.unlines $ replicate 10000 "<test x='value' />"
 
     let attrs = ["usd:jpy","test","extra","more","stuff","jpy:usd","xxx","xxxx"]
-    Right doc <- return $ parse $ "<test " <> BS.unwords [x <> "='" <> x <> "'" | x <- attrs] <> ">middle</test>"
-    [c] <- return $ childrenBy doc "test"
+    Right doc <- pure $ parse $ "<test " <> BS.unwords [x <> "='" <> x <> "'" | x <- attrs] <> ">middle</test>"
+    [c] <- pure $ childrenBy doc "test"
     forM_ attrs $ \a -> attributeBy c a === Just (Attribute a a)
     forM_ ["missing","gone","nothing"] $ \a -> attributeBy c a === Nothing
     putStrLn "\nSuccess"
@@ -80,5 +81,5 @@
                     | otherwise = error "Invalid name"
         validAttr x | BS.notElem '\"' x = x
                     | otherwise = error "Invalid attribute"
-        validStr x | BS.notElem '<' x || BS.isInfixOf "<!--" x = x
+        validStr x | BS.notElem '<' x || BS.isInfixOf "<!--" x || BS.isInfixOf "<![CDATA[" x = x
                    | otherwise = error $ show ("Invalid string", x)
diff --git a/src/Text/XML/Hexml.hs b/src/Text/XML/Hexml.hs
--- a/src/Text/XML/Hexml.hs
+++ b/src/Text/XML/Hexml.hs
@@ -90,11 +90,11 @@
         if err /= nullPtr then do
             bs <- BS.packCString =<< hexml_document_error doc
             hexml_document_free doc
-            return $ Left bs
+            pure $ Left bs
          else do
             node <- hexml_document_node doc
             doc <- newForeignPtr hexml_document_free_funptr doc
-            return $ Right $ Node src0 doc node
+            pure $ Right $ Node src0 doc node
 
 -- | Given a node, rerender it to something with an equivalent parse tree.
 --   Mostly useful for debugging - if you want the real source document use 'outer' instead.
@@ -103,7 +103,7 @@
     i <- hexml_node_render d n nullPtr 0
     res <- BS.create (fromIntegral i) $ \ptr -> void $ hexml_node_render d n (castPtr ptr) i
     touchBS src
-    return res
+    pure res
 
 applyStr :: BS.ByteString -> Str -> BS.ByteString
 applyStr bs Str{..} = BS.take (fromIntegral strLength) $ BS.drop (fromIntegral strStart) bs
@@ -118,7 +118,7 @@
 attrPeek src doc a = unsafePerformIO $ withForeignPtr doc $ \_ -> do
     name <- applyStr src <$> peekElemOff (castPtr a) 0
     val  <- applyStr src <$> peekElemOff (castPtr a) 1
-    return $ Attribute name val
+    pure $ Attribute name val
 
 -- | Get the name of a node, e.g. @\<test /\>@ produces @\"test\"@.
 name :: Node -> BS.ByteString
@@ -142,7 +142,7 @@
 contents :: Node -> [Either BS.ByteString Node]
 contents n@(Node src _ _) = f (strStart inner) outers
     where
-        f i [] = string i (strEnd inner) ++ []
+        f i [] = string i (strEnd inner)
         f i ((x, n):xs) = string i (strStart x) ++ Right n : f (strEnd x) xs
 
         string start end | start == end = []
@@ -156,7 +156,7 @@
     alloca $ \count -> do
         res <- hexml_node_children d n count
         count <- fromIntegral <$> peek count
-        return [Node src doc $ plusPtr res $ i*szNode | i <- [0..count-1]]
+        pure [Node src doc $ plusPtr res $ i*szNode | i <- [0..count-1]]
 
 -- | Get the attributes of this node.
 attributes :: Node -> [Attribute]
@@ -164,7 +164,7 @@
     alloca $ \count -> do
         res <- hexml_node_attributes d n count
         count <- fromIntegral <$> peek count
-        return [attrPeek src doc $ plusPtr res $ i*szAttr | i <- [0..count-1]]
+        pure [attrPeek src doc $ plusPtr res $ i*szAttr | i <- [0..count-1]]
 
 -- | Get the direct children of this node which have a specific name.
 --   A more efficient version of:
@@ -177,7 +177,7 @@
             BS.unsafeUseAsCStringLen str $ \(bs, len) -> do
                 r <- hexml_node_child d n old bs $ fromIntegral len
                 touchBS src
-                return $ if r == nullPtr then [] else Node src doc r : go r
+                pure $ if r == nullPtr then [] else Node src doc r : go r
 
 -- | Get the first attribute of this node which has a specific name, if there is one.
 --   A more efficient version of:
@@ -188,7 +188,7 @@
     BS.unsafeUseAsCStringLen str $ \(bs, len) -> do
         r <- hexml_node_attribute d n bs $ fromIntegral len
         touchBS src
-        return $ if r == nullPtr then Nothing else Just $ attrPeek src doc r
+        pure $ if r == nullPtr then Nothing else Just $ attrPeek src doc r
 
 -- | Find the starting location of a node, the @<@ character.
 --   The first character will be reported as @(line 1,column 1)@, because thats
