diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,24 @@
 # Revision history for asciidoc-hs
 
+## 0.1.0.1 -- 2026-02-01
+
+  * Fix character escaping issue (#3). Unconstrained forms of
+    delimited constructions weren't being allowed after `++`.
+
+  * Fix some footnote parsing issues (#2).
+
+  * Fix parsing of document attributes in the body of the document (#1).
+    Previously only those in the header were handled.
+
+  * Change handling of doc attributes. Collect them in state so that
+    we can handle attributes defined in the body of the document.
+
+  * Friendlier error message than "endOfInput" on unexpected content
+    at the end.
+
+  * Move regression tests to test/regression.
+
 ## 0.1 -- 2025-11-30
 
-* Initial release.
+  * Initial release.
 
diff --git a/asciidoc.cabal b/asciidoc.cabal
--- a/asciidoc.cabal
+++ b/asciidoc.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.4
 name:               asciidoc
-version:            0.1
+version:            0.1.0.1
 synopsis:           AsciiDoc parser.
 description:        A parser for AsciiDoc syntax.
 license:            BSD-3-Clause
@@ -12,6 +12,7 @@
 build-type:         Simple
 extra-doc-files:    CHANGELOG.md
 extra-source-files: test/asciidoctor/**/*.test
+                    test/regression/*.test
                     test/feature/**/*.test
                     test/feature/include/**/*.adoc
 
diff --git a/src/AsciiDoc/Parse.hs b/src/AsciiDoc/Parse.hs
--- a/src/AsciiDoc/Parse.hs
+++ b/src/AsciiDoc/Parse.hs
@@ -19,7 +19,7 @@
 import qualified Data.Text as T
 import qualified Data.Text.Read as TR
 import Data.Text (Text)
-import Data.List (foldl', intersperse)
+import Data.List (foldl', intersperse, isPrefixOf)
 import qualified Data.Attoparsec.Text as A
 import System.FilePath
 import Control.Applicative
@@ -109,11 +109,15 @@
   deriving (Functor, Applicative, Alternative, Monad, MonadPlus,
             MonadFail, MonadReader ParserConfig, MonadState ParserState)
 
-newtype ParserState = ParserState
+data ParserState = ParserState
                      { counterMap :: M.Map Text (CounterType, Int)
+                     , docAttrs :: M.Map Text Text
                      }
         deriving (Show)
 
+defaultDocAttrs :: M.Map Text Text
+defaultDocAttrs = M.insert "sectids" "" mempty
+
 data ParserConfig = ParserConfig
                     { filePath :: FilePath
                     , blockContexts :: [BlockContext]
@@ -129,7 +133,9 @@
                                  , blockContexts = []
                                  , hardBreaks = False
                                  })
-                    (ParserState { counterMap = mempty })
+                    (ParserState { counterMap = mempty
+                                 , docAttrs = defaultDocAttrs
+                                 })
                     p
 
 parse' :: ParserConfig -> ParserState
@@ -137,7 +143,10 @@
 parse' cfg st p t =
   go $ A.parse (evalStateT ( runReaderT (unP p) cfg ) st) t
  where
-  go (A.Fail i _ msg) = Left $ ParseError (T.length t - T.length i) msg
+  go (A.Fail i _ msg) = Left $ ParseError (T.length t - T.length i)
+                             $ if "endOfInput" `isPrefixOf` msg
+                                  then "Unexpected " <> show (T.take 20 i)
+                                  else msg
   go (A.Partial continue) = go (continue "")
   go (A.Done _i r) = Right r
 
@@ -238,24 +247,23 @@
 pDocument :: P Document
 pDocument = do
   meta <- pDocumentHeader
-  let minSectionLevel = case M.lookup "doctype" (docAttributes meta) of
+  attr' <- gets docAttrs
+  let minSectionLevel = case M.lookup "doctype" attr' of
                           Just "book" -> 0
                           _ -> 1
-  bs <- (case M.lookup "hardbreaks-option" (docAttributes meta) of
+  bs <- (case M.lookup "hardbreaks-option" attr' of
             Just "" -> withHardBreaks
             _ -> id) $
         withBlockContext (SectionContext (minSectionLevel - 1)) (many pBlock)
   skipWhile isSpace
   endOfInput
-  pure $ Document { docMeta = meta , docBlocks = bs }
+  attr <- gets docAttrs
+  pure $ Document { docMeta = meta{ docAttributes = attr } , docBlocks = bs }
 
 pDocumentHeader :: P Meta
 pDocumentHeader = do
-  let handleAttr m (Left k) = M.delete k m
-      handleAttr m (Right (k,v)) = M.insert k v m
-  let defaultDocAttrs = M.insert "sectids" "" mempty
   skipBlankLines
-  topattrs <- foldl' handleAttr defaultDocAttrs <$> many pDocAttribute
+  skipMany pDocAttribute
   skipBlankLines
   (title, titleAttr) <- option ([], Nothing) $ do
     (_,titleAttr) <- pTitlesAndAttributes
@@ -269,12 +277,13 @@
   revision <- if null title
                  then pure Nothing
                  else optional pDocumentRevision
-  attrs <- foldl' handleAttr topattrs <$> many pDocAttribute
+  skipMany pDocAttribute
   pure $ Meta{ docTitle = title
              , docTitleAttributes = titleAttr
              , docAuthors = authors
              , docRevision = revision
-             , docAttributes = attrs }
+             , docAttributes = mempty }
+              -- docAttributes this gets added at the end from docAttrs in state
 
 pDocumentTitle :: P [Inline]
 pDocumentTitle = do
@@ -321,18 +330,18 @@
 pLine = takeWhile (not . isEndOfLine) <* (endOfLine <|> endOfInput)
 
 
--- Left key unsets key
--- Right (key, val) sets key
-pDocAttribute :: P (Either Text (Text, Text))
+pDocAttribute :: P ()
 pDocAttribute = do
   vchar ':'
   unset <- option False $ True <$ vchar '!'
   k <- pDocAttributeName
   vchar ':'
   v <- pLineWithEscapes
-  pure $ if unset
-            then Left k
-            else Right (k,v)
+  modify $ \s ->
+    s{ docAttrs =
+         if unset
+            then M.delete k (docAttrs s)
+            else M.insert k v (docAttrs s) }
 
 pDocAttributeName :: P Text
 pDocAttributeName = do
@@ -380,8 +389,16 @@
       pure x
 
 parseBlocks :: Text -> P [Block]
-parseBlocks = parseWith (many pBlock) . (<> "\n") . T.strip
+parseBlocks = parseWith pBlocks . (<> "\n") . T.strip
 
+pBlocks :: P [Block]
+pBlocks = do
+  bs <- many pBlock
+  skipBlankLines
+  skipMany pDocAttribute
+  skipBlankLines
+  pure bs
+
 parseAsciidoc :: Text -> P Document
 parseAsciidoc = parseWith pDocument . (<> "\n") . T.strip
 
@@ -402,11 +419,13 @@
 pBlock = do
   contexts <- asks blockContexts
   skipBlankLines
+  skipMany pDocAttribute
+  skipBlankLines
   (mbtitle, attr) <- pTitlesAndAttributes
+  skipMany (pCommentBlock attr)
   case contexts of
     ListContext{} : _ -> skipWhile (== ' ')
     _ -> pure ()
-  skipMany (pCommentBlock attr)
   let hardbreaks =
        case attr of
           Attr _ kvs
@@ -1326,7 +1345,7 @@
 pInline :: [Char] -> P Inline
 pInline prevChars = do
   let maybeUnconstrained = case prevChars of
-                              (d:_) -> isSpace d || isPunctuation d
+                              (d:_) -> isSpace d || isPunctuation d || d == '+'
                               [] -> True
   let inMatched = pInMatched maybeUnconstrained
   skipMany pLineComment
@@ -1411,7 +1430,7 @@
     mbc <- peekChar
     case mbc of
       Nothing -> pure ()
-      Just c -> guard $ isSpace c || isPunctuation c
+      Just c -> guard $ isSpace c || isPunctuation c || c == '+'
   Inline attr <$> toInlineType (T.pack cs)
 
 pInlineAnchor :: P Inline
@@ -1532,12 +1551,11 @@
                     then pure [Inline mempty (Str target)]
                     else parseInlines description)
   , ("footnote", \target -> do
-      attr <- pAttributes
-      let (contents, attr') = extractDescription attr
-          fnid = if target == mempty
+      ils <- pBracketedText >>= parseInlines
+      let fnid = if target == mempty
                     then Nothing
                     else Just (FootnoteId target)
-      Inline attr' . Footnote fnid <$> parseInlines contents)
+      pure $ Inline mempty (Footnote fnid ils))
   , ("footnoteref", \_ -> do
       (Attr ps kvs) <- pAttributes
       (target, contents) <- case ps of
@@ -1580,6 +1598,8 @@
   vchar '[' *>
     (mconcat <$> many
          (T.pack <$> some ((vchar '\\' *> char ']') <|>
+                           (vchar '+' *> vchar '+' *> char ']'
+                             <* vchar '+' <* vchar '+') <|>
                  satisfy (\c -> c /= ']' && not (isEndOfLine c)) <|>
                  (' ' <$ (vchar '\\' <* endOfLine)))
           <|> ((\x -> "[" <> x <> "]") <$> pBracketedText)))
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -20,9 +20,11 @@
 main = do
   asciidoctorTests <- goldenTests "asciidoctor"
   featureTests <- goldenTests "feature"
+  regressionTests <- goldenTests "regression"
   defaultMain $ testGroup "Tests"
     [ testGroup "Asciidoctor" asciidoctorTests
     , testGroup "Feature" featureTests
+    , testGroup "Regression" regressionTests
     , testGroup "Generic"
        [ foldInlineTest
        , foldBlockTest
@@ -33,18 +35,21 @@
 
 goldenTests :: FilePath -> IO [TestTree]
 goldenTests fp = do
-  files <- findTestFiles ("test" </> fp)
-  pure $ map (\(category, fs) ->
-          testGroup category (map toGoldenTest fs)) files
+  (toplevel, groups) <- findTestFiles ("test" </> fp)
+  pure $ map toGoldenTest toplevel ++
+         map (\(category, fs) ->
+          testGroup category (map toGoldenTest fs)) groups
 
 -- Find all .test files in a directory
-findTestFiles :: FilePath -> IO [(FilePath, [FilePath])]
+findTestFiles :: FilePath -> IO ([FilePath], [(FilePath, [FilePath])])
 findTestFiles baseDir = do
-  categories <- listDirectory baseDir
+  fs <- listDirectory baseDir
   let go f = do
         xs <- map ((baseDir </> f) </>) <$> listDirectory (baseDir </> f)
         return (f, sort $ filter ((== ".test") . takeExtension) xs)
-  mapM go categories
+  categories <- mapM go (filter (null . takeExtension) fs)
+  let toplevel = map (baseDir </>) $ filter ((== ".test") . takeExtension) fs
+  return (toplevel, categories)
 
 toGoldenTest :: FilePath -> TestTree
 toGoldenTest fp =
diff --git a/test/asciidoctor/outline/numbered.test b/test/asciidoctor/outline/numbered.test
--- a/test/asciidoctor/outline/numbered.test
+++ b/test/asciidoctor/outline/numbered.test
@@ -42,12 +42,7 @@
           Attr
           ( [] , fromList [ ( "id" , "_unnumbered_section" ) ] )
           Nothing
-          (Section
-             (Level 1)
-             [ Inline mempty (Str "Unnumbered Section") ]
-             [ Block
-                 mempty Nothing (Paragraph [ Inline mempty (Str ":numbered:") ])
-             ])
+          (Section (Level 1) [ Inline mempty (Str "Unnumbered Section") ] [])
       , Block
           Attr
           ( [] , fromList [ ( "id" , "_section_2" ) ] )
diff --git a/test/regression/issue_1.test b/test/regression/issue_1.test
new file mode 100644
--- /dev/null
+++ b/test/regression/issue_1.test
@@ -0,0 +1,23 @@
+= Test
+
+:doc_attr: bar
+doc_attr = {doc_attr}
+>>>
+Document
+  { docMeta =
+      Meta
+        { docTitle = [ Inline mempty (Str "Test") ]
+        , docTitleAttributes = Nothing
+        , docAuthors = []
+        , docRevision = Nothing
+        , docAttributes =
+            fromList [ ( "doc_attr" , "bar" ) , ( "sectids" , "" ) ]
+        }
+  , docBlocks =
+      [ Block
+          mempty
+          Nothing
+          (Paragraph
+             [ Inline mempty (Str "doc_attr = ") , Inline mempty (Str "bar") ])
+      ]
+  }
diff --git a/test/regression/issue_2.test b/test/regression/issue_2.test
new file mode 100644
--- /dev/null
+++ b/test/regression/issue_2.test
@@ -0,0 +1,60 @@
+Textfootnote:[Commas break foonotes, do you see?].
+
+Other textfootnote:[This is the same for closing ++[++brackets++]++,
+isn’t it?].
+
+And anotherfootnote:[Some dots do as well. But not all of them.], and a
+final onefootnote:[S. Jackson.].
+
+>>>
+Document
+  { docMeta =
+      Meta
+        { docTitle = []
+        , docTitleAttributes = Nothing
+        , docAuthors = []
+        , docRevision = Nothing
+        , docAttributes = fromList [ ( "sectids" , "" ) ]
+        }
+  , docBlocks =
+      [ Block
+          mempty
+          Nothing
+          (Paragraph
+             [ Inline mempty (Str "Text")
+             , Inline
+                 mempty
+                 (Footnote
+                    Nothing
+                    [ Inline mempty (Str "Commas break foonotes, do you see?") ])
+             , Inline mempty (Str ".")
+             ])
+      , Block
+          mempty
+          Nothing
+          (Paragraph
+             [ Inline
+                 mempty (Str "Other textfootnote:[This is the same for closing ")
+             , Inline mempty (Str "[")
+             , Inline mempty (Str "brackets")
+             , Inline mempty (Str "]")
+             , Inline mempty (Str ",\nisn\8217t it?].")
+             ])
+      , Block
+          mempty
+          Nothing
+          (Paragraph
+             [ Inline mempty (Str "And another")
+             , Inline
+                 mempty
+                 (Footnote
+                    Nothing
+                    [ Inline mempty (Str "Some dots do as well. But not all of them.")
+                    ])
+             , Inline mempty (Str ", and a\nfinal one")
+             , Inline
+                 mempty (Footnote Nothing [ Inline mempty (Str "S. Jackson.") ])
+             , Inline mempty (Str ".")
+             ])
+      ]
+  }
diff --git a/test/regression/issue_3.test b/test/regression/issue_3.test
new file mode 100644
--- /dev/null
+++ b/test/regression/issue_3.test
@@ -0,0 +1,79 @@
+Words _italicised_ in brackets are ++[++_broken_++]++.
+
+But this [_works_], ++[++__this__++]++ too, and ++[++this _also_ works.++]++
+
+Words in *bold* in brackets are ++{++*broken*++}++.
+
+But this {*works*}, ++{++**this**++}++ too, and ++{++this *also* works.++}++
+
+>>>
+Document
+  { docMeta =
+      Meta
+        { docTitle = []
+        , docTitleAttributes = Nothing
+        , docAuthors = []
+        , docRevision = Nothing
+        , docAttributes = fromList [ ( "sectids" , "" ) ]
+        }
+  , docBlocks =
+      [ Block
+          mempty
+          Nothing
+          (Paragraph
+             [ Inline mempty (Str "Words ")
+             , Inline mempty (Italic [ Inline mempty (Str "italicised") ])
+             , Inline mempty (Str " in brackets are ")
+             , Inline mempty (Str "[")
+             , Inline mempty (Italic [ Inline mempty (Str "broken") ])
+             , Inline mempty (Str "]")
+             , Inline mempty (Str ".")
+             ])
+      , Block
+          mempty
+          Nothing
+          (Paragraph
+             [ Inline mempty (Str "But this [")
+             , Inline mempty (Italic [ Inline mempty (Str "works") ])
+             , Inline mempty (Str "], ")
+             , Inline mempty (Str "[")
+             , Inline mempty (Italic [ Inline mempty (Str "this") ])
+             , Inline mempty (Str "]")
+             , Inline mempty (Str " too, and ")
+             , Inline mempty (Str "[")
+             , Inline mempty (Str "this ")
+             , Inline mempty (Italic [ Inline mempty (Str "also") ])
+             , Inline mempty (Str " works.")
+             , Inline mempty (Str "]")
+             ])
+      , Block
+          mempty
+          Nothing
+          (Paragraph
+             [ Inline mempty (Str "Words in ")
+             , Inline mempty (Bold [ Inline mempty (Str "bold") ])
+             , Inline mempty (Str " in brackets are ")
+             , Inline mempty (Str "{")
+             , Inline mempty (Bold [ Inline mempty (Str "broken") ])
+             , Inline mempty (Str "}")
+             , Inline mempty (Str ".")
+             ])
+      , Block
+          mempty
+          Nothing
+          (Paragraph
+             [ Inline mempty (Str "But this {")
+             , Inline mempty (Bold [ Inline mempty (Str "works") ])
+             , Inline mempty (Str "}, ")
+             , Inline mempty (Str "{")
+             , Inline mempty (Bold [ Inline mempty (Str "this") ])
+             , Inline mempty (Str "}")
+             , Inline mempty (Str " too, and ")
+             , Inline mempty (Str "{")
+             , Inline mempty (Str "this ")
+             , Inline mempty (Bold [ Inline mempty (Str "also") ])
+             , Inline mempty (Str " works.")
+             , Inline mempty (Str "}")
+             ])
+      ]
+  }
