diff --git a/Example/Example.hs b/Example/Example.hs
deleted file mode 100644
--- a/Example/Example.hs
+++ /dev/null
@@ -1,144 +0,0 @@
-
-module Example.Example where
-
-import Text.HTML.TagSoup
-import Text.HTML.Download
-
-import Control.Monad
-import Data.List
-import Data.Char
-
-
-grab :: String -> IO ()
-grab x = openItem x >>= putStr
-
-parse :: String -> IO ()
-parse x = openItem x >>= putStr . show2 . parseTags
-    where
-        show2 [] = "[]"
-        show2 xs = "[" ++ concat (intersperseNotBroken "\n," $ map show xs) ++ "\n]"
-
-
--- the standard intersperse has a strictness bug which sucks!
-intersperseNotBroken :: a -> [a] -> [a]
-intersperseNotBroken _ [] = []
-intersperseNotBroken sep (x:xs) = x : is xs
-    where
-        is [] = []
-        is (y:ys) = sep : y : is ys
-
-
-{-
-<div class="printfooter">
-<p>Retrieved from "<a href="http://haskell.org/haskellwiki/Haskell">http://haskell.org/haskellwiki/Haskell</a>"</p>
-
-<p>This page has been accessed 507,753 times. This page was last modified 08:05, 24 January 2007. Recent content is available under <a href="/haskellwiki/HaskellWiki:Copyrights" title="HaskellWiki:Copyrights">a simple permissive license</a>.</p>
-</div>
--}
-haskellHitCount :: IO ()
-haskellHitCount = do
-        tags <- liftM parseTags $ openURL "http://haskell.org/haskellwiki/Haskell"
-        let count = fromFooter $ head $ sections (~== "<div class=printfooter>") tags
-        putStrLn $ "haskell.org has been hit " ++ show count ++ " times"
-    where
-        fromFooter x = read (filter isDigit num) :: Int
-            where
-                num = ss !! (i - 1)
-                Just i = findIndex (== "times.") ss
-                ss = words s
-                TagText s = sections (~== "<p>") x !! 1 !! 1
-
-
-{-
-<a href="http://www.cbc.ca/technology/story/2007/04/10/tech-bloggers.html" id=r-5_1115205181>
-<b>Blogger code of conduct proposed</b>
--}
-googleTechNews :: IO ()
-googleTechNews = do
-        tags <- liftM parseTags $ openURL "http://news.google.com/?ned=us&topic=t"
-        let links = [ text
-                    | TagOpen "a" atts:TagOpen "b" []:TagText text:_ <- tails tags,
-                    ("id",'u':'-':_) <- atts]
-        putStr $ unlines links
-
-
-spjPapers :: IO ()
-spjPapers = do
-        tags <- liftM parseTags $ openURL "http://research.microsoft.com/~simonpj/"
-        let links = map f $ sections (~== "<a>") $
-                    takeWhile (~/= "<a name=haskell>") $
-                    drop 5 $ dropWhile (~/= "<a name=current>") tags
-        putStr $ unlines links
-    where
-        f :: [Tag] -> String
-        f = dequote . unwords . words . fromTagText . head . filter isTagText
-
-        dequote ('\"':xs) | last xs == '\"' = init xs
-        dequote x = x
-
-
-ndmPapers :: IO ()
-ndmPapers = do
-        tags <- liftM parseTags $ openURL "http://www-users.cs.york.ac.uk/~ndm/downloads/"
-        let papers = map f $ sections (~== "<li class=paper>") tags
-        putStr $ unlines papers
-    where
-        f :: [Tag] -> String
-        f xs = fromTagText (xs !! 2)
-
-
-currentTime :: IO ()
-currentTime = do
-        tags <- liftM parseTags $ openURL "http://www.timeanddate.com/worldclock/city.html?n=136"
-        let time = fromTagText (dropWhile (~/= "<strong id=ct>") tags !! 1)
-        putStrLn time
-
-
-
-type Section = String
-data Package = Package {name :: String, desc :: String, href :: String}
-               deriving Show
-
-hackage :: IO [(Section,[Package])]
-hackage = do
-    tags <- liftM parseTags $ openURL "http://hackage.haskell.org/packages/archive/pkg-list.html"
-    return $ map parseSect $ partitions (~== "<h3>") tags
-    where
-        parseSect xs = (nam, packs)
-            where
-                nam = fromTagText $ xs !! 2
-                packs = map parsePackage $ partitions (~== "<li>") xs
-
-        parsePackage xs =
-           Package
-              (fromTagText $ xs !! 2)
-              (drop 2 $ dropWhile (/= ':') $ fromTagText $ xs !! 4)
-              (fromAttrib "href" $ xs !! 1)
-
--- rssCreators Example: prints names of story contributors on
--- sequence.complete.org. This content is RSS (not HTML), and the selected
--- tag uses a different XML namespace "dc:creator".
-rssCreators :: IO ()
-rssCreators = do
-    tags <- liftM parseTags $ openURL "http://sequence.complete.org/node/feed"
-    putStrLn $ unlines $ map names $ partitions (~== "<dc:creator>") tags
-    where
-      names xs = fromTagText $ xs !! 1
-
-
-validate :: String -> IO ()
-validate x = putStr . unlines . g . f . parseTagsOptions opts =<< openItem x
-    where
-        opts = parseOptions{optTagPosition=True, optTagWarning=True}
-
-        f :: [Tag] -> [String]
-        f (TagPosition row col:TagWarning warn:rest) =
-            ("Warning (" ++ show row ++ "," ++ show col ++ "): " ++ warn) : f rest
-        f (TagWarning warn:rest) =
-            ("Warning (?,?): " ++ warn) : f rest
-        f (_:rest) = f rest
-        f [] = []
-
-        g xs = xs ++ [if n == 0 then "Success, no warnings"
-                      else "Failed, " ++ show n ++ " warning" ++ ['s'|n>1]]
-            where n = length xs
diff --git a/Example/Regress.hs b/Example/Regress.hs
deleted file mode 100644
--- a/Example/Regress.hs
+++ /dev/null
@@ -1,143 +0,0 @@
-
-module Example.Regress (regress) where
-
-import Text.HTML.TagSoup
-import Text.HTML.TagSoup.Entity
-import qualified Text.HTML.TagSoup.Match as Match
-import Control.Exception
-
--- * The Test Monad
-
-data Test a = Pass
-instance Monad Test where
-    a >> b = a `seq` b
-    return = error "No return for Monad Test"
-    (>>=) = error "No bind (>>=) for Monad Test"
-instance Show (Test a) where
-    show x = x `seq` "All tests passed"
-
-pass :: Test ()
-pass = Pass
-
-(===) :: (Show a, Eq a) => a -> a -> Test ()
-a === b = if a == b then pass else fail $ "Does not equal: " ++ show a ++ " =/= " ++ show b
-
--- * The Main section
-
-regress :: IO ()
-regress = print $ do
-    parseTests
-    combiTests
-    entityTests
-    lazyTags == lazyTags `seq` pass
-    matchCombinators
-
-
-{- |
-This routine tests the laziness of the TagSoup parser.
-For each critical part of the parser we provide a test input
-with a token of infinite size.
-Then the output must be infinite too.
-If the laziness is broken, then the output will stop early.
-We collect the thousandth character of the output of each test case.
-If computation of the list stops somewhere,
-you have found a laziness stopper.
--}
-
-
-lazyTags :: [Char]
-lazyTags =
-   map ((!!1000) . show . parseTags) $
-      (cycle "Rhabarber") :
-      (repeat '&') :
-      ("<"++cycle "html") :
-      ("<html "++cycle "na!me=value ") :
-      ("<html name="++cycle "value") :
-      ("<html name=\""++cycle "value") :
-      ("<html name="++cycle "val!ue") :
-      ("<html "++cycle "name") :
-      ("</"++cycle "html") :
-      ("<!-- "++cycle "comment") :
-      ("<!"++cycle "doctype") :
-      ("<!DOCTYPE"++cycle " description") :
-      (cycle "1<2 ") :
-      
-      -- need further analysis
-      ("<html name="++cycle "val&ue") :
-      ("<html name="++cycle "va&l!ue") :
-      ("&" ++ cycle "t") :
-      (cycle "&amp; test") :
-
-      -- i don't see how this can work unless the junk gets into the AST?
-      --("</html "++cycle "junk") :
-
-      []
-
-
-
-matchCombinators :: Test ()
-matchCombinators = assert (and tests) pass
-    where
-        tests =
-            Match.tagText (const True) (TagText "test") :
-            Match.tagText ("test"==) (TagText "test") :
-            Match.tagText ("soup"/=) (TagText "test") :
-            Match.tagOpenNameLit "table"
-               (TagOpen "table" [("id", "name")]) :
-            Match.tagOpenLit "table" (Match.anyAttrLit ("id", "name"))
-               (TagOpen "table" [("id", "name")]) :
-            Match.tagOpenLit "table" (Match.anyAttrNameLit "id")
-               (TagOpen "table" [("id", "name")]) :
-            not (Match.tagOpenLit "table" (Match.anyAttrLit ("id", "name"))
-                  (TagOpen "table" [("id", "other name")])) :
-            []
-
-
-parseTests :: Test ()
-parseTests = do
-    parseTags "<!DOCTYPE TEST>" === [TagOpen "!DOCTYPE" [("TEST","")]]
-    parseTags "<test \"foo bar\">" === [TagOpen "test" [("","foo bar")]]
-    parseTags "<test \'foo bar\'>" === [TagOpen "test" [("","foo bar")]]
-    parseTags "<:test \'foo bar\'>" === [TagOpen ":test" [("","foo bar")]]
-    parseTags "hello &amp; world" === [TagText "hello & world"]
-    parseTags "hello &#64; world" === [TagText "hello @ world"]
-    parseTags "hello &#x40; world" === [TagText "hello @ world"]
-    parseTags "hello &haskell; world" === [TagText "hello &haskell; world"]
-    parseTags "hello \n\t world" === [TagText "hello \n\t world"]
-
-    parseTags "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" ===
-        [TagOpen "!DOCTYPE" [("HTML",""),("PUBLIC",""),("","-//W3C//DTD HTML 4.01//EN"),("","http://www.w3.org/TR/html4/strict.dtd")]]
-    parseTags "<script src=\"http://edge.jobthread.com/feeds/jobroll/?s_user_id=100540&subtype=slashdot\">" ===
-        [TagOpen "script" [("src","http://edge.jobthread.com/feeds/jobroll/?s_user_id=100540&subtype=slashdot")]]
-
-    parseTags "<a title='foo'bar' href=correct>text" === [TagOpen "a" [("title", "foo"),
-                                                                       ("bar",   ""),
-                                                                       ("href", "correct")],
-                                                         TagText "text"]
-
-
-entityTests :: Test ()
-entityTests = do
-    lookupNumericEntity "65" === Just 'A'
-    lookupNumericEntity "x41" === Just 'A'
-    lookupNumericEntity "x4E" === Just 'N'
-    lookupNumericEntity "x4e" === Just 'N'
-    lookupNumericEntity "Haskell" === Nothing
-    lookupNumericEntity "" === Nothing
-    lookupNumericEntity "89439085908539082" === Nothing
-    lookupNamedEntity "amp" === Just '&'
-    lookupNamedEntity "haskell" === Nothing
-    escapeXMLChar 'a' === Nothing
-    escapeXMLChar '&' === Just "amp"
-
-
-combiTests :: Test ()
-combiTests = do
-    (TagText "test" ~== TagText ""    ) === True
-    (TagText "test" ~== TagText "test") === True
-    (TagText "test" ~== TagText "soup") === False
-    (TagText "test" ~== "test") === True
-    (TagOpen "test" [] ~== "<test>") === True
-    (TagOpen "test" [] ~== "<soup>") === False
-    (TagOpen "test" [] ~/= "<soup>") === True
-
diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright Neil Mitchell 2006-2007.
+Copyright Neil Mitchell 2006-2009.
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
diff --git a/Main.hs b/Main.hs
--- a/Main.hs
+++ b/Main.hs
@@ -2,14 +2,16 @@
 module Main(main) where
 
 import System.Environment
-import Example.Example
-import Example.Regress
+import TagSoup.Sample
+import TagSoup.Test
+import TagSoup.Benchmark
 import Data.Char(toLower)
+import Text.HTML.TagSoup -- just to make it easier to use in GHCi
 
 
 helpMsg :: IO ()
 helpMsg = putStr $ unlines $
-    ["TagSoup, (C) Neil Mitchell 2006-2008"
+    ["TagSoup, (C) Neil Mitchell 2006-2009"
     ,""
     ,"  tagsoup arguments"
     ,""
@@ -27,9 +29,11 @@
             
 
 actions :: [(String, String, Either (IO ()) (String -> IO ()))]
-actions = [("regress","Run the regression tests",Left regress)
+actions = [("test","Run the test suite",Left test)
           ,("grab","Grab a web page",Right grab)
           ,("parse","Parse a web page",Right parse)
+          ,("bench","Benchmark the parsing",Left time)
+          ,("benchfile","Benchmark the parsing of a file",Right timefile)
           ,("validate","Validate a page",Right validate)
           ,("hitcount","Get the Haskell.org hit count",Left haskellHitCount)
           ,("spj","Simon Peyton Jones' papers",Left spjPapers)
diff --git a/Setup.hs b/Setup.hs
--- a/Setup.hs
+++ b/Setup.hs
@@ -1,3 +1,3 @@
-#!/usr/bin/runhaskell
+#! /usr/bin/env runhaskell
 import Distribution.Simple
 main = defaultMain
diff --git a/TagSoup/Benchmark.hs b/TagSoup/Benchmark.hs
new file mode 100644
--- /dev/null
+++ b/TagSoup/Benchmark.hs
@@ -0,0 +1,192 @@
+
+module TagSoup.Benchmark where
+
+import Text.HTML.TagSoup
+
+import Control.DeepSeq
+import Control.Monad
+import Data.List
+import Data.Maybe
+import Data.Char
+import System.CPUTime
+import System.IO
+import System.IO.Unsafe(unsafeInterleaveIO)
+import qualified Data.ByteString.Char8 as BS
+import qualified Data.ByteString.Lazy.Char8 as LBS
+import Data.Time.Clock.POSIX(getPOSIXTime)
+
+conf = 0.95
+
+
+timefile :: FilePath -> IO ()
+timefile file = do
+    -- use LBS to be most representative of real life
+    lbs <- LBS.readFile file
+    let str = LBS.unpack lbs
+        bs = BS.concat $ LBS.toChunks lbs
+    () <- LBS.length lbs `seq` length str `seq` BS.length bs `seq` return ()
+    benchWith (const str, const bs, const lbs) $ benchStatic (toInteger $ LBS.length lbs)
+
+
+sample :: String
+sample = "<this is a test with='attributes' and other=\"things&quot;tested\" /><neil> is </here>" ++
+         "<!-- comment --> and some just random &amp; test &gt;&lt;<foo></bar><bar><bob href=no>"
+
+nsample = genericLength sample :: Integer
+
+time :: IO ()
+time = benchWith (str,bs,lbs) benchVariable
+    where
+        str = \i -> concat $ genericReplicate i sample
+        bs  = let s = BS.pack sample in \i -> BS.concat (genericReplicate i s)
+        lbs = let s = LBS.pack sample in \i -> LBS.concat (genericReplicate i s)
+
+
+
+benchWith :: (Integer -> String, Integer -> BS.ByteString, Integer -> LBS.ByteString)
+          -> ((Integer -> ()) -> IO [String]) -> IO ()
+benchWith (str,bs,lbs) bench = do
+        putStrLn "Timing parseTags in characters/second"
+        let header = map (:[]) ["(" ++ show (round $ conf * 100) ++ "% confidence)","String","BS","LBS"]
+        rows <- mapM row $ replicateM 3 [False,True]
+        mapM_ (putStrLn . strict . grid) $ delay2 $ header : rows
+    where
+        row [a,b,c] = do
+            let header = intercalate "," [g a "pos", g b "warn", g c "merge"]
+                g b x = (if b then ' ' else '!') : x
+                f x = bench $ \i -> rnf $ parseTagsOptions parseOptions{optTagPosition=a,optTagWarning=b,optTagTextMerge=c} $ x i
+            c1 <- f str
+            c2 <- f bs
+            c3 <- f lbs
+            return [[header],c1,c2,c3]
+
+        strict = reverse . reverse
+
+
+---------------------------------------------------------------------
+-- BENCHMARK ON THE SAMPLE INPUT
+
+disp xs = showUnit (floor xbar) ++ " (~" ++ rng ++ "%)"
+    where xbar = mean xs
+          rng = if length xs <= 1 then "?" else show (ceiling $ (range conf xs) * 100 / xbar) 
+
+cons x = fmap (x:)
+
+
+aimTime = 0.3 :: Double -- seconds to aim for
+minTime = 0.2 :: Double -- below this a test is considered invalid
+
+
+-- given a number of times to repeat sample, return a list of what
+-- to display
+benchVariable :: (Integer -> ()) -> IO [String]
+benchVariable op = cons "?" $ f 10 []
+    where
+        f i seen | length seen > 9 = cons ("  " ++ disp seen) $ return []
+                 | otherwise = unsafeInterleaveIO $ do
+            now <- timer $ op i
+            let cps = if now == 0 then 0 else fromInteger (i * nsample) / now
+            if now < minTime || (null seen && now < aimTime) then do
+                let factor = min 7 $ max 2 $ floor $ aimTime / now
+                cons ("? " ++ disp [cps]) $ f (i * factor) []
+             else
+                cons (show (9 - length seen) ++ " " ++ disp (cps:seen)) $ f i (cps:seen)
+
+
+
+benchStatic :: Integer -> (Integer -> ()) -> IO [String]
+benchStatic nsample op = cons "?" $ f []
+    where
+        f seen | length seen > 9 = cons ("  " ++ disp seen) $ return []
+               | otherwise = unsafeInterleaveIO $ do
+            now <- timer $ op $ genericLength seen
+            let cps = if now == 0 then 0 else fromInteger nsample / now
+            cons (show (9 - length seen) ++ " " ++ disp (cps:seen)) $ f (cps:seen)
+
+
+---------------------------------------------------------------------
+-- UTILITY FUNCTIONS
+
+-- | Given a number, show it using a unit and decimal place
+showUnit :: Integer -> String
+showUnit x = num ++ unit
+    where
+        units = " KMGTPEZY"
+        (use,skip) = splitAt 3 $ show x
+
+        unit = [units !! ((length skip + 2) `div` 3)]
+
+        dot = ((length skip - 1) `mod` 3) + 1
+        num = a ++ ['.' | b /= ""] ++ b
+            where (a,b) = splitAt dot use
+
+
+-- copied from the criterion package
+getTime :: IO Double
+getTime = (fromRational . toRational) `fmap` getPOSIXTime
+
+timer :: () -> IO Double
+timer x = do
+    start <- getTime
+    () <- return x
+    end <- getTime
+    return $ end - start
+
+
+-- display a grid
+grid :: [[String]] -> String
+grid xs = unlines $ map (concat . zipWith f cols) xs
+    where cols = map (maximum . map length) $ transpose xs
+          f n x = x ++ replicate (n+1 - length x) ' '
+
+
+-- display a series of grids over time
+-- when a grid gets to [] keep its value at that
+-- when all grids get to [] return []
+delay2 :: [[[String]]] -> [[[String]]]
+delay2 xs = map (map head) xs : (if all (null . tail) (concat xs) then [] else delay2 $ map (map tl) xs)
+    where tl (x:xs) = if null xs then x:xs else xs
+
+
+---------------------------------------------------------------------
+-- INSTANCES
+
+instance NFData a => NFData (Tag a) where
+    rnf (TagOpen x y) = rnf x `seq` rnf y
+    rnf (TagClose x) = rnf x
+    rnf (TagText x) = rnf x
+    rnf (TagComment x) = rnf x
+    rnf (TagWarning x) = rnf x
+    rnf (TagPosition x y) = () -- both are already ! bound
+
+instance NFData LBS.ByteString where
+    rnf x = LBS.length x `seq` ()
+
+instance NFData BS.ByteString where
+    rnf x = BS.length x `seq` ()
+
+
+---------------------------------------------------------------------
+-- STATISTICS
+-- Provided by Emily Mitchell
+
+confNs = let (*) = (,) in
+    [0.95 * 1.96
+    ,0.90 * 1.644]
+
+size :: [Double] -> Double
+size = genericLength
+
+mean :: [Double] -> Double
+mean xs = sum xs / size xs
+
+stddev :: [Double] -> Double
+stddev xs = sqrt $ sum [sqr (x - xbar) | x <- xs] / size xs
+    where xbar = mean xs
+          sqr x = x * x
+
+-- given a sample, and a required confidence
+-- of the mean (i.e. 2.5% = 0.025)
+range ::Double -> [Double] -> Double
+range conf xs = conf2 * stddev xs / sqrt (size xs)
+    where conf2 = fromMaybe (error $ "Unknown confidence interval: " ++ show conf) $ lookup conf confNs
diff --git a/TagSoup/Sample.hs b/TagSoup/Sample.hs
new file mode 100644
--- /dev/null
+++ b/TagSoup/Sample.hs
@@ -0,0 +1,153 @@
+
+module TagSoup.Sample where
+
+import Text.HTML.TagSoup
+import Network.HTTP
+
+import Data.Char
+import Data.List
+import Data.Maybe
+import System.IO
+
+
+openItem :: String -> IO String
+openItem x | "http://" `isPrefixOf` x = getResponseBody =<< simpleHTTP (getRequest x)
+           | otherwise = readFile x
+
+
+grab :: String -> IO ()
+grab x = openItem x >>= putStr
+
+parse :: String -> IO ()
+parse x = openItem x >>= putStr . show2 . parseTags
+    where
+        show2 [] = "[]"
+        show2 xs = "[" ++ concat (intersperseNotBroken "\n," $ map show xs) ++ "\n]\n"
+
+
+-- the standard intersperse has a strictness bug which sucks!
+intersperseNotBroken :: a -> [a] -> [a]
+intersperseNotBroken _ [] = []
+intersperseNotBroken sep (x:xs) = x : is xs
+    where
+        is [] = []
+        is (y:ys) = sep : y : is ys
+
+
+{-
+<div class="printfooter">
+<p>Retrieved from "<a href="http://haskell.org/haskellwiki/Haskell">http://haskell.org/haskellwiki/Haskell</a>"</p>
+
+<p>This page has been accessed 507,753 times. This page was last modified 08:05, 24 January 2007. Recent content is available under <a href="/haskellwiki/HaskellWiki:Copyrights" title="HaskellWiki:Copyrights">a simple permissive license</a>.</p>
+</div>
+-}
+haskellHitCount :: IO ()
+haskellHitCount = do
+        tags <- fmap parseTags $ openItem "http://haskell.org/haskellwiki/Haskell"
+        let count = fromFooter $ head $ sections (~== "<div class=printfooter>") tags
+        putStrLn $ "haskell.org has been hit " ++ show count ++ " times"
+    where
+        fromFooter x = read (filter isDigit num) :: Int
+            where
+                num = ss !! (i - 1)
+                Just i = findIndex (== "times.") ss
+                ss = words s
+                TagText s = sections (~== "<p>") x !! 1 !! 1
+
+
+googleTechNews :: IO ()
+googleTechNews = do
+        tags <- fmap parseTags $ openItem "http://news.google.com/?ned=us&topic=t"
+        let links = [ ascii name ++ " <" ++ maybe "unknown" shortUrl (lookup "href" atts) ++ ">"
+                    | TagOpen "h2" [("class","title")]:TagText spaces:TagOpen "a" atts:TagText name:_ <- tails tags]
+        putStr $ unlines links
+    where
+        shortUrl x | "http://" `isPrefixOf` x = shortUrl $ drop 7 x
+                   | "www." `isPrefixOf` x = shortUrl $ drop 4 x
+                   | otherwise = takeWhile (/= '/') x
+
+        ascii ('\226':'\128':'\147':xs) = '-' : ascii xs
+        ascii ('\194':'\163':xs) = "#GBP " ++ ascii xs
+        ascii (x:xs) = x : ascii xs
+        ascii [] = []
+
+
+spjPapers :: IO ()
+spjPapers = do
+        tags <- fmap parseTags $ openItem "http://research.microsoft.com/en-us/people/simonpj/"
+        let links = map f $ sections (~== "<A>") $
+                    takeWhile (~/= "<A name=haskell>") $
+                    drop 5 $ dropWhile (~/= "<A name=current>") tags
+        putStr $ unlines links
+    where
+        f :: [Tag String] -> String
+        f = dequote . unwords . words . fromTagText . head . filter isTagText
+
+        dequote ('\"':xs) | last xs == '\"' = init xs
+        dequote x = x
+
+
+ndmPapers :: IO ()
+ndmPapers = do
+        tags <- fmap parseTags $ openItem "http://community.haskell.org/~ndm/downloads/"
+        let papers = map f $ sections (~== "<li class=paper>") tags
+        putStr $ unlines papers
+    where
+        f :: [Tag String] -> String
+        f xs = fromTagText (xs !! 2)
+
+
+currentTime :: IO ()
+currentTime = do
+        tags <- fmap parseTags $ openItem "http://www.timeanddate.com/worldclock/city.html?n=136"
+        let res = fromTagText (dropWhile (~/= "<strong id=ct>") tags !! 1)
+        putStrLn res
+
+
+
+type Section = String
+data Package = Package {name :: String, desc :: String, href :: String}
+               deriving Show
+
+hackage :: IO [(Section,[Package])]
+hackage = do
+    tags <- fmap parseTags $ openItem "http://hackage.haskell.org/packages/archive/pkg-list.html"
+    return $ map parseSect $ partitions (~== "<h3>") tags
+    where
+        parseSect xs = (nam, packs)
+            where
+                nam = fromTagText $ xs !! 2
+                packs = map parsePackage $ partitions (~== "<li>") xs
+
+        parsePackage xs =
+           Package
+              (fromTagText $ xs !! 2)
+              (drop 2 $ dropWhile (/= ':') $ fromTagText $ xs !! 4)
+              (fromAttrib "href" $ xs !! 1)
+
+-- rssCreators Example: prints names of story contributors on
+-- sequence.complete.org. This content is RSS (not HTML), and the selected
+-- tag uses a different XML namespace "dc:creator".
+rssCreators :: IO ()
+rssCreators = do
+    tags <- fmap parseTags $ openItem "http://sequence.complete.org/node/feed"
+    putStrLn $ unlines $ map names $ partitions (~== "<dc:creator>") tags
+    where names xs = fromTagText $ xs !! 1
+
+
+validate :: String -> IO ()
+validate x = putStr . unlines . g . f . parseTagsOptions opts =<< openItem x
+    where
+        opts = parseOptions{optTagPosition=True, optTagWarning=True}
+
+        f :: [Tag String] -> [String]
+        f (TagPosition row col:TagWarning warn:rest) =
+            ("Warning (" ++ show row ++ "," ++ show col ++ "): " ++ warn) : f rest
+        f (TagWarning warn:rest) =
+            ("Warning (?,?): " ++ warn) : f rest
+        f (_:rest) = f rest
+        f [] = []
+
+        g xs = xs ++ [if n == 0 then "Success, no warnings"
+                      else "Failed, " ++ show n ++ " warning" ++ ['s'|n>1]]
+            where n = length xs
diff --git a/TagSoup/Test.hs b/TagSoup/Test.hs
new file mode 100644
--- /dev/null
+++ b/TagSoup/Test.hs
@@ -0,0 +1,219 @@
+{-# OPTIONS_GHC -fno-warn-deprecations #-}
+
+module TagSoup.Test(test) where
+
+import Text.HTML.TagSoup
+import Text.HTML.TagSoup.Entity
+import Text.HTML.TagSoup.Match
+
+import Control.Exception
+import Control.Monad
+import Data.List
+import Test.QuickCheck
+
+-- * The Test Monad
+
+type Test a = IO a
+
+pass :: Test ()
+pass = return ()
+
+runTest :: Test () -> IO ()
+runTest x = x >> putStrLn "All tests passed"
+
+(===) :: (Show a, Eq a) => a -> a -> IO ()
+a === b = if a == b then pass else fail $ "Does not equal: " ++ show a ++ " =/= " ++ show b
+
+check :: Testable prop => prop -> IO ()
+check prop = do
+    res <- quickCheckWithResult stdArgs{maxSuccess=1000} prop
+    case res of
+        Success{} -> pass
+        _ -> fail "Property failed"
+
+newtype HTML = HTML String deriving Show
+instance Arbitrary HTML where
+    arbitrary = fmap (HTML . concat) $ listOf $ elements frags
+        where frags = map (:[]) " \n!-</>#&;xy01[]?'\"" ++ ["CDATA","amp","gt","lt"]
+    shrink (HTML x) = map HTML $ zipWith (++) (inits x) (tail $ tails x)
+
+
+-- * The Main section
+
+test :: IO ()
+test = runTest $ do
+    parseTests
+    optionsTests
+    renderTests
+    combiTests
+    entityTests
+    lazyTags == lazyTags `seq` pass
+    matchCombinators
+
+
+{- |
+This routine tests the laziness of the TagSoup parser.
+For each critical part of the parser we provide a test input
+with a token of infinite size.
+Then the output must be infinite too.
+If the laziness is broken, then the output will stop early.
+We collect the thousandth character of the output of each test case.
+If computation of the list stops somewhere,
+you have found a laziness stopper.
+-}
+
+
+lazyTags :: [Char]
+lazyTags = map ((!!1000) . show . parseTags)
+    [cycle "Rhabarber"
+    ,repeat '&'
+    ,"<"++cycle "html"
+    ,"<html "++cycle "na!me=value "
+    ,"<html name="++cycle "value"
+    ,"<html name=\""++cycle "value"
+    ,"<html name="++cycle "val!ue"
+    ,"<html "++cycle "name"
+    ,"</"++cycle "html"
+    ,"<!-- "++cycle "comment"
+    ,"<!"++cycle "doctype"
+    ,"<!DOCTYPE"++cycle " description"
+    ,cycle "1<2 "
+    ,"&" ++ cycle "t"
+    ,"<html name="++cycle "val&ue"
+    ,"<html name="++cycle "va&l!ue"
+    ,cycle "&amp; test"
+
+    -- i don't see how this can work unless the junk gets into the AST?
+    -- ,("</html "++cycle "junk") :
+    ]
+
+
+
+matchCombinators :: Test ()
+matchCombinators = do
+    tagText (const True) (TagText "test") === True
+    tagText ("test"==) (TagText "test") === True
+    tagText ("soup"/=) (TagText "test") === True
+    tagOpenNameLit "table" (TagOpen "table" [("id", "name")]) === True
+    tagOpenLit "table" (anyAttrLit ("id", "name")) (TagOpen "table" [("id", "name")]) === True
+    tagOpenLit "table" (anyAttrNameLit "id") (TagOpen "table" [("id", "name")]) === True
+    tagOpenLit "table" (anyAttrLit ("id", "name")) (TagOpen "table" [("id", "other name")]) === False
+
+
+parseTests :: Test ()
+parseTests = do
+    parseTags "<!DOCTYPE TEST>" === [TagOpen "!DOCTYPE" [("TEST","")]]
+    parseTags "<test \"foo bar\">" === [TagOpen "test" [("","foo bar")]]
+    parseTags "<test baz \"foo\">" === [TagOpen "test" [("baz",""),("","foo")]]
+    parseTags "<test \'foo bar\'>" === [TagOpen "test" [("","foo bar")]]
+    parseTags "<test2 a b>" === [TagOpen "test2" [("a",""),("b","")]]
+    parseTags "<test2 ''>" === [TagOpen "test2" [("","")]]
+    parseTags "</test foo>" === [TagClose "test"]
+    parseTags "<test/>" === [TagOpen "test" [], TagClose "test"]
+    parseTags "<test1 a = b>" === [TagOpen "test1" [("a","b")]]
+    parseTags "hello &amp; world" === [TagText "hello & world"]
+    parseTags "hello &#64; world" === [TagText "hello @ world"]
+    parseTags "hello &#x40; world" === [TagText "hello @ world"]
+    parseTags "hello &haskell; world" === [TagText "hello &haskell; world"]
+    parseTags "hello \n\t world" === [TagText "hello \n\t world"]
+    parseTags "<a href=http://www.google.com>" === [TagOpen "a" [("href","http://www.google.com")]]
+
+    -- real cases reported by users
+    parseTags "<a href=\"series.php?view=single&ID=72710\">" === [TagOpen "a" [("href","series.php?view=single&ID=72710")]]
+
+    parseTags "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" ===
+        [TagOpen "!DOCTYPE" [("HTML",""),("PUBLIC",""),("","-//W3C//DTD HTML 4.01//EN"),("","http://www.w3.org/TR/html4/strict.dtd")]]
+
+    parseTags "<script src=\"http://edge.jobthread.com/feeds/jobroll/?s_user_id=100540&subtype=slashdot\">" ===
+        [TagOpen "script" [("src","http://edge.jobthread.com/feeds/jobroll/?s_user_id=100540&subtype=slashdot")]]
+
+    parseTags "<a title='foo'bar' href=correct>text" === [TagOpen "a" [("title","foo"),("bar'",""),("href", "correct")],TagText "text"]
+
+    parseTags "<test><![CDATA[Anything goes, <em>even hidden markup</em> &amp; entities]]> but this is outside</test>" ===
+        [TagOpen "test" [],TagText "Anything goes, <em>even hidden markup</em> &amp; entities but this is outside",TagClose "test"]
+
+
+optionsTests :: Test ()
+optionsTests = check $ \(HTML x) -> all (f x) $ replicateM 3 [False,True]
+    where
+        f str [pos,warn,merge] =
+                bool "merge" (not merge || adjacentTagText tags) &&
+                bool "warn" (warn || all (not . isTagWarning) tags) &&
+                bool "pos" (if pos then alternatePos tags else all (not . isTagPosition) tags)
+            where tags = parseTagsOptions parseOptions{optTagPosition=pos,optTagWarning=warn,optTagTextMerge=merge} str
+                  bool x b = b || error ("optionsTests failed with " ++ x ++ " on " ++ show (pos,warn,merge,str,tags))
+
+        -- optTagTextMerge implies no adjacent TagText cells
+        -- and none separated by only warnings or positions
+        adjacentTagText = g True -- can the next be a tag text
+            where g i (x:xs) | isTagText x = i && g False xs
+                             | isTagPosition x || isTagWarning x = g i xs
+                             | otherwise = g True xs
+                  g i [] = True
+
+        -- optTagPosition implies every element must be followed
+        -- by a position node, no two position nodes must be adjacent
+        -- and all positions must be increasing
+        alternatePos (TagPosition l1 c1 : x : TagPosition l2 c2 : xs)
+            | (l1,c1) <= (l2,c2) && not (isTagPosition x) = alternatePos $ TagPosition l2 c2 : xs
+        alternatePos [TagPosition l1 c1, x] | not $ isTagPosition x = True
+        alternatePos [] = True
+        alternatePos _ = False
+
+
+renderTests :: Test ()
+renderTests = do
+    let rp = renderTags . parseTags
+    rp "<test>" === "<test>"
+    rp "<br></br>" === "<br />"
+    rp "<script></script>" === "<script></script>"
+    rp "hello & world" === "hello &amp; world"
+    rp "<a href=test>" === "<a href=\"test\">"
+    rp "<a href>" === "<a href>"
+    rp "<a href?>" === "<a href?>"
+    rp "<?xml foo?>" === "<?xml foo ?>"
+    rp "<?xml foo?>" === "<?xml foo ?>"
+    rp "<!-- neil -->" === "<!-- neil -->"
+    check $ \(HTML x) -> let y = rp x in rp y == (y :: String)
+
+    
+entityTests :: Test ()
+entityTests = do
+    lookupNumericEntity "65" === Just 'A'
+    lookupNumericEntity "x41" === Just 'A'
+    lookupNumericEntity "x4E" === Just 'N'
+    lookupNumericEntity "x4e" === Just 'N'
+    lookupNumericEntity "Haskell" === Nothing
+    lookupNumericEntity "" === Nothing
+    lookupNumericEntity "89439085908539082" === Nothing
+    lookupNamedEntity "amp" === Just '&'
+    lookupNamedEntity "haskell" === Nothing
+    escapeXMLChar 'a' === Nothing
+    escapeXMLChar '&' === Just "amp"
+
+
+combiTests :: Test ()
+combiTests = do
+    (TagText "test" ~== TagText ""    ) === True
+    (TagText "test" ~== TagText "test") === True
+    (TagText "test" ~== TagText "soup") === False
+    (TagText "test" ~== "test") === True
+    (TagOpen "test" [] ~== "<test>") === True
+    (TagOpen "test" [] ~== "<soup>") === False
+    (TagOpen "test" [] ~/= "<soup>") === True
+
+
+warnTests :: Test ()
+warnTests = do
+    return ()
+{-
+    -- TODO: Check that when there is a warning, it shows up at the right place
+    warns "neil &foo bar" = missing ;
+    let parse = parseTagsOptions parseOptions{
+    let noWarn x = 
+    
+    
+    
+        {optTagPosition :: Bool -- ^ Should 'TagPosition' values be given before some items (default=False,fast=False)
+        ,optTagWarning :: Bool  -- ^ Should 'TagWarning' values be given (default=False,fast=False)
+-}
diff --git a/Text/HTML/Download.hs b/Text/HTML/Download.hs
--- a/Text/HTML/Download.hs
+++ b/Text/HTML/Download.hs
@@ -1,15 +1,11 @@
 {-|
-    Module      :  Text.HTML.Download
-    Copyright   :  (c) Neil Mitchell 2006-2007
-    License     :  BSD-style
+    /DEPRECATED/: Use the HTTP package instead:
 
-    Maintainer  :  http://www.cs.york.ac.uk/~ndm/
-    Stability   :  unstable
-    Portability :  portable
+    > import Network.HTTP
+    > openURL x = getResponseBody =<< simpleHTTP (getRequest x)
 
     This module simply downloads a page off the internet. It is very restricted,
-    and it not intended for proper use. The primary purpose is to allow more
-    interesting examples for the "Data.Html.TagSoup" module.
+    and it not intended for proper use.
     
     The original version was by Alistair Bayley, with additional help from
     Daniel McAllansmith. It is taken from the Haskell-Cafe mailing list
@@ -23,6 +19,9 @@
 import System.IO.Unsafe
 import Network
 import Data.List
+
+{-# DEPRECATED openItem, openURL "Use package HTTP, module Network.HTTP, getResponseBody =<< simpleHTTP (getRequest url)" #-}
+
 
 -- | This function opens a URL on the internet.
 --   Any @http:\/\/@ prefix is ignored.
diff --git a/Text/HTML/TagSoup.hs b/Text/HTML/TagSoup.hs
--- a/Text/HTML/TagSoup.hs
+++ b/Text/HTML/TagSoup.hs
@@ -1,29 +1,28 @@
+{-# LANGUAGE TypeSynonymInstances, PatternGuards #-}
+
 {-|
-    Module      :  Text.HTML.TagSoup
-    Copyright   :  (c) Neil Mitchell 2006-2007
-    License     :  BSD-style
+    This module is for working with HTML/XML. It deals with both well-formed XML and
+    malformed HTML from the web. It features:
 
-    Maintainer  :  http://www.cs.york.ac.uk/~ndm/
-    Stability   :  unstable
-    Portability :  portable
+    * A lazy parser, based on the HTML 5 specification - see 'parseTags'.
 
-    This module is for extracting information out of unstructured HTML code,
-    sometimes known as tag-soup. This is for situations where the author of
-    the HTML is not cooperating with the person trying to extract the information,
-    but is also not trying to hide the information.
+    * A renderer that can write out HTML/XML - see 'renderTags'.
 
-    The standard practice is to parse a String to 'Tag's using 'parseTags',
+    * Utilities for extracting information from a document - see '~==', 'sections' and 'partitions'.
+
+    The standard practice is to parse a 'String' to @[@'Tag' 'String'@]@ using 'parseTags',
     then operate upon it to extract the necessary information.
 -}
 
 module Text.HTML.TagSoup(
     -- * Data structures and parsing
-    Tag(..), Attribute,
+    Tag(..), Row, Column, Attribute,
     module Text.HTML.TagSoup.Parser,
+    module Text.HTML.TagSoup.Render,
     canonicalizeTags,
 
     -- * Tag identification
-    isTagOpen, isTagClose, isTagText, isTagWarning,
+    isTagOpen, isTagClose, isTagText, isTagWarning, isTagPosition,
     isTagOpenName, isTagCloseName,
 
     -- * Extraction
@@ -35,42 +34,41 @@
     sections, partitions,
     
     -- * Combinators
-    TagRep, IsChar, (~==),(~/=)
+    TagRep, (~==),(~/=)
     ) where
 
-import Text.HTML.TagSoup.Parser
 import Text.HTML.TagSoup.Type
+import Text.HTML.TagSoup.Parser
+import Text.HTML.TagSoup.Render
 import Data.Char
 import Data.List
+import Text.StringLike
 
 
-{- |
-Turns all tag names to lower case and
-converts DOCTYPE to upper case.
--}
-canonicalizeTags :: [Tag] -> [Tag]
+-- | Turns all tag names and attributes to lower case and
+--   converts DOCTYPE to upper case.
+canonicalizeTags :: StringLike str => [Tag str] -> [Tag str]
 canonicalizeTags = map f
     where
-        f (TagOpen name attrs) | "!" `isPrefixOf` name = TagOpen (map toUpper name) attrs
-        f (TagOpen name attrs) | otherwise             = TagOpen (map toLower name) attrs
-        f (TagClose name) = TagClose (map toLower name)
+        f (TagOpen tag attrs) | Just ('!',name) <- uncons tag = TagOpen ('!' `cons` ucase name) attrs
+        f (TagOpen name attrs) = TagOpen (lcase name) [(lcase k, v) | (k,v) <- attrs]
+        f (TagClose name) = TagClose (lcase name)
         f a = a
 
+        ucase = fromString . map toUpper . toString
+        lcase = fromString . map toLower . toString
 
--- | Define a class to allow String's or Tag's to be used as matches
-class TagRep a where
-    toTagRep :: a -> Tag
 
-instance TagRep Tag where toTagRep = id
+-- | Define a class to allow String's or Tag str's to be used as matches
+class TagRep a where
+    toTagRep :: StringLike str => a -> Tag str
 
-class    IsChar a    where toChar :: a -> Char
-instance IsChar Char where toChar =  id
+instance StringLike str => TagRep (Tag str) where toTagRep = fmap castString
 
-instance IsChar c => TagRep [c] where
-    toTagRep x = case parseTags s of
-                     [a] -> a
-                     _ -> error $ "When using a TagRep it must be exactly one tag, you gave: " ++ s
-        where s = map toChar x
+instance TagRep String where
+    toTagRep x = case parseTags x of
+                     [a] -> toTagRep a
+                     _ -> error $ "When using a TagRep it must be exactly one tag, you gave: " ++ x
 
 
 
@@ -83,20 +81,20 @@
 -- > (TagText "test" ~== TagText "soup") == False
 --
 -- For 'TagOpen' missing attributes on the right are allowed.
-(~==) :: TagRep t => Tag -> t -> Bool
+(~==) :: (StringLike str, TagRep t) => Tag str -> t -> Bool
 (~==) a b = f a (toTagRep b)
     where
-        f (TagText y) (TagText x) = null x || x == y
-        f (TagClose y) (TagClose x) = null x || x == y
-        f (TagOpen y ys) (TagOpen x xs) = (null x || x == y) && all g xs
+        f (TagText y) (TagText x) = strNull x || x == y
+        f (TagClose y) (TagClose x) = strNull x || x == y
+        f (TagOpen y ys) (TagOpen x xs) = (strNull x || x == y) && all g xs
             where
-                g (name,val) | null name = val  `elem` map snd ys
-                             | null val  = name `elem` map fst ys
+                g (name,val) | strNull name = val  `elem` map snd ys
+                             | strNull val  = name `elem` map fst ys
                 g nameval = nameval `elem` ys
         f _ _ = False
 
 -- | Negation of '~=='
-(~/=) :: TagRep t => Tag -> t -> Bool
+(~/=) :: (StringLike str, TagRep t) => Tag str -> t -> Bool
 (~/=) a b = not (a ~== b)
 
 
diff --git a/Text/HTML/TagSoup/Entity.hs b/Text/HTML/TagSoup/Entity.hs
--- a/Text/HTML/TagSoup/Entity.hs
+++ b/Text/HTML/TagSoup/Entity.hs
@@ -1,4 +1,5 @@
--- this should be in Text.HTML but then we provoke name clashes
+-- | This module converts between HTML/XML entities (i.e. @&amp;@) and
+--   the characters they represent.
 module Text.HTML.TagSoup.Entity(
     lookupEntity, lookupNamedEntity, lookupNumericEntity,
     escapeXMLChar,
@@ -7,7 +8,6 @@
 
 import Data.Char
 import Data.Ix
-import Control.Monad
 import Numeric
 
 
@@ -53,7 +53,7 @@
 -- > lookupNamedEntity "amp" == Just '&'
 -- > lookupNamedEntity "haskell" == Nothing
 lookupNamedEntity :: String -> Maybe Char
-lookupNamedEntity x = liftM chr $ lookup x htmlEntities
+lookupNamedEntity x = fmap chr $ lookup x htmlEntities
 
 
 -- | Escape a character before writing it out to XML.
@@ -69,269 +69,269 @@
 -- | A table mapping XML entity names to code points.
 --   Does /not/ include @apos@ as Internet Explorer does not know about it.
 xmlEntities :: [(String, Int)]
-xmlEntities =
-   ("quot",     ord '"') :
-   ("amp",      ord '&') :
---   ("apos",   ord '\'') :   Internet Explorer does not know that
-   ("lt",       ord '<') :
-   ("gt",       ord '>') :
-   []
+xmlEntities = let a*b = (a,ord b) in
+    ["quot" * '"'
+    ,"amp"  * '&'
+    -- ,"apos" * '\''    -- Internet Explorer does not know that
+    ,"lt"   * '<'
+    ,"gt"   * '>'
+    ]
 
 -- | A table mapping HTML entity names to code points
 htmlEntities :: [(String, Int)]
-htmlEntities =
-   xmlEntities ++
-   ("apos",   ord '\'') : -- quirky IE!!!
+htmlEntities = let (*) = (,) in
+    xmlEntities ++
+    ["apos"    * ord '\''  -- quirky IE!!!
 
-   ("nbsp",     160) :
-   ("iexcl",    161) :
-   ("cent",     162) :
-   ("pound",    163) :
-   ("curren",   164) :
-   ("yen",      165) :
-   ("brvbar",   166) :
-   ("sect",     167) :
-   ("uml",      168) :
-   ("copy",     169) :
-   ("ordf",     170) :
-   ("laquo",    171) :
-   ("not",      172) :
-   ("shy",      173) :
-   ("reg",      174) :
-   ("macr",     175) :
-   ("deg",      176) :
-   ("plusmn",   177) :
-   ("sup2",     178) :
-   ("sup3",     179) :
-   ("acute",    180) :
-   ("micro",    181) :
-   ("para",     182) :
-   ("middot",   183) :
-   ("cedil",    184) :
-   ("sup1",     185) :
-   ("ordm",     186) :
-   ("raquo",    187) :
-   ("frac14",   188) :
-   ("frac12",   189) :
-   ("frac34",   190) :
-   ("iquest",   191) :
-   ("Agrave",   192) :
-   ("Aacute",   193) :
-   ("Acirc",    194) :
-   ("Atilde",   195) :
-   ("Auml",     196) :
-   ("Aring",    197) :
-   ("AElig",    198) :
-   ("Ccedil",   199) :
-   ("Egrave",   200) :
-   ("Eacute",   201) :
-   ("Ecirc",    202) :
-   ("Euml",     203) :
-   ("Igrave",   204) :
-   ("Iacute",   205) :
-   ("Icirc",    206) :
-   ("Iuml",     207) :
-   ("ETH",      208) :
-   ("Ntilde",   209) :
-   ("Ograve",   210) :
-   ("Oacute",   211) :
-   ("Ocirc",    212) :
-   ("Otilde",   213) :
-   ("Ouml",     214) :
-   ("times",    215) :
-   ("Oslash",   216) :
-   ("Ugrave",   217) :
-   ("Uacute",   218) :
-   ("Ucirc",    219) :
-   ("Uuml",     220) :
-   ("Yacute",   221) :
-   ("THORN",    222) :
-   ("szlig",    223) :
-   ("agrave",   224) :
-   ("aacute",   225) :
-   ("acirc",    226) :
-   ("atilde",   227) :
-   ("auml",     228) :
-   ("aring",    229) :
-   ("aelig",    230) :
-   ("ccedil",   231) :
-   ("egrave",   232) :
-   ("eacute",   233) :
-   ("ecirc",    234) :
-   ("euml",     235) :
-   ("igrave",   236) :
-   ("iacute",   237) :
-   ("icirc",    238) :
-   ("iuml",     239) :
-   ("eth",      240) :
-   ("ntilde",   241) :
-   ("ograve",   242) :
-   ("oacute",   243) :
-   ("ocirc",    244) :
-   ("otilde",   245) :
-   ("ouml",     246) :
-   ("divide",   247) :
-   ("oslash",   248) :
-   ("ugrave",   249) :
-   ("uacute",   250) :
-   ("ucirc",    251) :
-   ("uuml",     252) :
-   ("yacute",   253) :
-   ("thorn",    254) :
-   ("yuml",     255) :
+    ,"nbsp"    * 160
+    ,"iexcl"   * 161
+    ,"cent"    * 162
+    ,"pound"   * 163
+    ,"curren"  * 164
+    ,"yen"     * 165
+    ,"brvbar"  * 166
+    ,"sect"    * 167
+    ,"uml"     * 168
+    ,"copy"    * 169
+    ,"ordf"    * 170
+    ,"laquo"   * 171
+    ,"not"     * 172
+    ,"shy"     * 173
+    ,"reg"     * 174
+    ,"macr"    * 175
+    ,"deg"     * 176
+    ,"plusmn"  * 177
+    ,"sup2"    * 178
+    ,"sup3"    * 179
+    ,"acute"   * 180
+    ,"micro"   * 181
+    ,"para"    * 182
+    ,"middot"  * 183
+    ,"cedil"   * 184
+    ,"sup1"    * 185
+    ,"ordm"    * 186
+    ,"raquo"   * 187
+    ,"frac14"  * 188
+    ,"frac12"  * 189
+    ,"frac34"  * 190
+    ,"iquest"  * 191
+    ,"Agrave"  * 192
+    ,"Aacute"  * 193
+    ,"Acirc"   * 194
+    ,"Atilde"  * 195
+    ,"Auml"    * 196
+    ,"Aring"   * 197
+    ,"AElig"   * 198
+    ,"Ccedil"  * 199
+    ,"Egrave"  * 200
+    ,"Eacute"  * 201
+    ,"Ecirc"   * 202
+    ,"Euml"    * 203
+    ,"Igrave"  * 204
+    ,"Iacute"  * 205
+    ,"Icirc"   * 206
+    ,"Iuml"    * 207
+    ,"ETH"     * 208
+    ,"Ntilde"  * 209
+    ,"Ograve"  * 210
+    ,"Oacute"  * 211
+    ,"Ocirc"   * 212
+    ,"Otilde"  * 213
+    ,"Ouml"    * 214
+    ,"times"   * 215
+    ,"Oslash"  * 216
+    ,"Ugrave"  * 217
+    ,"Uacute"  * 218
+    ,"Ucirc"   * 219
+    ,"Uuml"    * 220
+    ,"Yacute"  * 221
+    ,"THORN"   * 222
+    ,"szlig"   * 223
+    ,"agrave"  * 224
+    ,"aacute"  * 225
+    ,"acirc"   * 226
+    ,"atilde"  * 227
+    ,"auml"    * 228
+    ,"aring"   * 229
+    ,"aelig"   * 230
+    ,"ccedil"  * 231
+    ,"egrave"  * 232
+    ,"eacute"  * 233
+    ,"ecirc"   * 234
+    ,"euml"    * 235
+    ,"igrave"  * 236
+    ,"iacute"  * 237
+    ,"icirc"   * 238
+    ,"iuml"    * 239
+    ,"eth"     * 240
+    ,"ntilde"  * 241
+    ,"ograve"  * 242
+    ,"oacute"  * 243
+    ,"ocirc"   * 244
+    ,"otilde"  * 245
+    ,"ouml"    * 246
+    ,"divide"  * 247
+    ,"oslash"  * 248
+    ,"ugrave"  * 249
+    ,"uacute"  * 250
+    ,"ucirc"   * 251
+    ,"uuml"    * 252
+    ,"yacute"  * 253
+    ,"thorn"   * 254
+    ,"yuml"    * 255
 
-   ("OElig",    338) :
-   ("oelig",    339) :
-   ("Scaron",   352) :
-   ("scaron",   353) :
-   ("Yuml",     376) :
-   ("circ",     710) :
-   ("tilde",    732) :
+    ,"OElig"   * 338
+    ,"oelig"   * 339
+    ,"Scaron"  * 352
+    ,"scaron"  * 353
+    ,"Yuml"    * 376
+    ,"circ"    * 710
+    ,"tilde"   * 732
 
-   ("ensp",     8194) :
-   ("emsp",     8195) :
-   ("thinsp",   8201) :
-   ("zwnj",     8204) :
-   ("zwj",      8205) :
-   ("lrm",      8206) :
-   ("rlm",      8207) :
-   ("ndash",    8211) :
-   ("mdash",    8212) :
-   ("lsquo",    8216) :
-   ("rsquo",    8217) :
-   ("sbquo",    8218) :
-   ("ldquo",    8220) :
-   ("rdquo",    8221) :
-   ("bdquo",    8222) :
-   ("dagger",   8224) :
-   ("Dagger",   8225) :
-   ("permil",   8240) :
-   ("lsaquo",   8249) :
-   ("rsaquo",   8250) :
-   ("euro",     8364) :
+    ,"ensp"    * 8194
+    ,"emsp"    * 8195
+    ,"thinsp"  * 8201
+    ,"zwnj"    * 8204
+    ,"zwj"     * 8205
+    ,"lrm"     * 8206
+    ,"rlm"     * 8207
+    ,"ndash"   * 8211
+    ,"mdash"   * 8212
+    ,"lsquo"   * 8216
+    ,"rsquo"   * 8217
+    ,"sbquo"   * 8218
+    ,"ldquo"   * 8220
+    ,"rdquo"   * 8221
+    ,"bdquo"   * 8222
+    ,"dagger"  * 8224
+    ,"Dagger"  * 8225
+    ,"permil"  * 8240
+    ,"lsaquo"  * 8249
+    ,"rsaquo"  * 8250
+    ,"euro"    * 8364
 
-   ("fnof",     402) :
-   ("Alpha",    913) :
-   ("Beta",     914) :
-   ("Gamma",    915) :
-   ("Delta",    916) :
-   ("Epsilon",  917) :
-   ("Zeta",     918) :
-   ("Eta",      919) :
-   ("Theta",    920) :
-   ("Iota",     921) :
-   ("Kappa",    922) :
-   ("Lambda",   923) :
-   ("Mu",       924) :
-   ("Nu",       925) :
-   ("Xi",       926) :
-   ("Omicron",  927) :
-   ("Pi",       928) :
-   ("Rho",      929) :
-   ("Sigma",    931) :
-   ("Tau",      932) :
-   ("Upsilon",  933) :
-   ("Phi",      934) :
-   ("Chi",      935) :
-   ("Psi",      936) :
-   ("Omega",    937) :
-   ("alpha",    945) :
-   ("beta",     946) :
-   ("gamma",    947) :
-   ("delta",    948) :
-   ("epsilon",  949) :
-   ("zeta",     950) :
-   ("eta",      951) :
-   ("theta",    952) :
-   ("iota",     953) :
-   ("kappa",    954) :
-   ("lambda",   955) :
-   ("mu",       956) :
-   ("nu",       957) :
-   ("xi",       958) :
-   ("omicron",  959) :
-   ("pi",       960) :
-   ("rho",      961) :
-   ("sigmaf",   962) :
-   ("sigma",    963) :
-   ("tau",      964) :
-   ("upsilon",  965) :
-   ("phi",      966) :
-   ("chi",      967) :
-   ("psi",      968) :
-   ("omega",    969) :
-   ("thetasym", 977) :
-   ("upsih",    978) :
-   ("piv",      982) :
-   ("bull",     8226) :
-   ("hellip",   8230) :
-   ("prime",    8242) :
-   ("Prime",    8243) :
-   ("oline",    8254) :
-   ("frasl",    8260) :
-   ("weierp",   8472) :
-   ("image",    8465) :
-   ("real",     8476) :
-   ("trade",    8482) :
-   ("alefsym",  8501) :
-   ("larr",     8592) :
-   ("uarr",     8593) :
-   ("rarr",     8594) :
-   ("darr",     8595) :
-   ("harr",     8596) :
-   ("crarr",    8629) :
-   ("lArr",     8656) :
-   ("uArr",     8657) :
-   ("rArr",     8658) :
-   ("dArr",     8659) :
-   ("hArr",     8660) :
-   ("forall",   8704) :
-   ("part",     8706) :
-   ("exist",    8707) :
-   ("empty",    8709) :
-   ("nabla",    8711) :
-   ("isin",     8712) :
-   ("notin",    8713) :
-   ("ni",       8715) :
-   ("prod",     8719) :
-   ("sum",      8721) :
-   ("minus",    8722) :
-   ("lowast",   8727) :
-   ("radic",    8730) :
-   ("prop",     8733) :
-   ("infin",    8734) :
-   ("ang",      8736) :
-   ("and",      8743) :
-   ("or",       8744) :
-   ("cap",      8745) :
-   ("cup",      8746) :
-   ("int",      8747) :
-   ("there4",   8756) :
-   ("sim",      8764) :
-   ("cong",     8773) :
-   ("asymp",    8776) :
-   ("ne",       8800) :
-   ("equiv",    8801) :
-   ("le",       8804) :
-   ("ge",       8805) :
-   ("sub",      8834) :
-   ("sup",      8835) :
-   ("nsub",     8836) :
-   ("sube",     8838) :
-   ("supe",     8839) :
-   ("oplus",    8853) :
-   ("otimes",   8855) :
-   ("perp",     8869) :
-   ("sdot",     8901) :
-   ("lceil",    8968) :
-   ("rceil",    8969) :
-   ("lfloor",   8970) :
-   ("rfloor",   8971) :
-   ("lang",     9001) :
-   ("rang",     9002) :
-   ("loz",      9674) :
-   ("spades",   9824) :
-   ("clubs",    9827) :
-   ("hearts",   9829) :
-   ("diams",    9830) :
-   []
+    ,"fnof"    * 402
+    ,"Alpha"   * 913
+    ,"Beta"    * 914
+    ,"Gamma"   * 915
+    ,"Delta"   * 916
+    ,"Epsilon" * 917
+    ,"Zeta"    * 918
+    ,"Eta"     * 919
+    ,"Theta"   * 920
+    ,"Iota"    * 921
+    ,"Kappa"   * 922
+    ,"Lambda"  * 923
+    ,"Mu"      * 924
+    ,"Nu"      * 925
+    ,"Xi"      * 926
+    ,"Omicron" * 927
+    ,"Pi"      * 928
+    ,"Rho"     * 929
+    ,"Sigma"   * 931
+    ,"Tau"     * 932
+    ,"Upsilon" * 933
+    ,"Phi"     * 934
+    ,"Chi"     * 935
+    ,"Psi"     * 936
+    ,"Omega"   * 937
+    ,"alpha"   * 945
+    ,"beta"    * 946
+    ,"gamma"   * 947
+    ,"delta"   * 948
+    ,"epsilon" * 949
+    ,"zeta"    * 950
+    ,"eta"     * 951
+    ,"theta"   * 952
+    ,"iota"    * 953
+    ,"kappa"   * 954
+    ,"lambda"  * 955
+    ,"mu"      * 956
+    ,"nu"      * 957
+    ,"xi"      * 958
+    ,"omicron" * 959
+    ,"pi"      * 960
+    ,"rho"     * 961
+    ,"sigmaf"  * 962
+    ,"sigma"   * 963
+    ,"tau"     * 964
+    ,"upsilon" * 965
+    ,"phi"     * 966
+    ,"chi"     * 967
+    ,"psi"     * 968
+    ,"omega"   * 969
+    ,"thetasym"* 977
+    ,"upsih"   * 978
+    ,"piv"     * 982
+    ,"bull"    * 8226
+    ,"hellip"  * 8230
+    ,"prime"   * 8242
+    ,"Prime"   * 8243
+    ,"oline"   * 8254
+    ,"frasl"   * 8260
+    ,"weierp"  * 8472
+    ,"image"   * 8465
+    ,"real"    * 8476
+    ,"trade"   * 8482
+    ,"alefsym" * 8501
+    ,"larr"    * 8592
+    ,"uarr"    * 8593
+    ,"rarr"    * 8594
+    ,"darr"    * 8595
+    ,"harr"    * 8596
+    ,"crarr"   * 8629
+    ,"lArr"    * 8656
+    ,"uArr"    * 8657
+    ,"rArr"    * 8658
+    ,"dArr"    * 8659
+    ,"hArr"    * 8660
+    ,"forall"  * 8704
+    ,"part"    * 8706
+    ,"exist"   * 8707
+    ,"empty"   * 8709
+    ,"nabla"   * 8711
+    ,"isin"    * 8712
+    ,"notin"   * 8713
+    ,"ni"      * 8715
+    ,"prod"    * 8719
+    ,"sum"     * 8721
+    ,"minus"   * 8722
+    ,"lowast"  * 8727
+    ,"radic"   * 8730
+    ,"prop"    * 8733
+    ,"infin"   * 8734
+    ,"ang"     * 8736
+    ,"and"     * 8743
+    ,"or"      * 8744
+    ,"cap"     * 8745
+    ,"cup"     * 8746
+    ,"int"     * 8747
+    ,"there4"  * 8756
+    ,"sim"     * 8764
+    ,"cong"    * 8773
+    ,"asymp"   * 8776
+    ,"ne"      * 8800
+    ,"equiv"   * 8801
+    ,"le"      * 8804
+    ,"ge"      * 8805
+    ,"sub"     * 8834
+    ,"sup"     * 8835
+    ,"nsub"    * 8836
+    ,"sube"    * 8838
+    ,"supe"    * 8839
+    ,"oplus"   * 8853
+    ,"otimes"  * 8855
+    ,"perp"    * 8869
+    ,"sdot"    * 8901
+    ,"lceil"   * 8968
+    ,"rceil"   * 8969
+    ,"lfloor"  * 8970
+    ,"rfloor"  * 8971
+    ,"lang"    * 9001
+    ,"rang"    * 9002
+    ,"loz"     * 9674
+    ,"spades"  * 9824
+    ,"clubs"   * 9827
+    ,"hearts"  * 9829
+    ,"diams"   * 9830
+    ]
diff --git a/Text/HTML/TagSoup/Generated.hs b/Text/HTML/TagSoup/Generated.hs
new file mode 100644
--- /dev/null
+++ b/Text/HTML/TagSoup/Generated.hs
@@ -0,0 +1,2 @@
+module Text.HTML.TagSoup.Generated(parseTagsOptions) where
+import Text.HTML.TagSoup.Manual
diff --git a/Text/HTML/TagSoup/Implementation.hs b/Text/HTML/TagSoup/Implementation.hs
new file mode 100644
--- /dev/null
+++ b/Text/HTML/TagSoup/Implementation.hs
@@ -0,0 +1,191 @@
+{-# LANGUAGE RecordWildCards, PatternGuards, ScopedTypeVariables #-}
+
+module Text.HTML.TagSoup.Implementation where
+
+import Data.List
+import Text.HTML.TagSoup.Type
+import Text.HTML.TagSoup.Options
+import Text.StringLike as Str
+import Numeric
+import Data.Char
+import Control.Exception(assert)
+import Control.Arrow
+
+---------------------------------------------------------------------
+-- BOTTOM LAYER
+
+data Out
+    = Char Char
+    | Tag          -- <
+    | TagShut      -- </
+    | AttName
+    | AttVal
+    | TagEnd       -- >
+    | TagEndClose  -- />
+    | Comment      -- <!--
+    | CommentEnd   -- -->
+    | Entity       -- &
+    | EntityNum    -- &#
+    | EntityHex    -- &#x
+    | EntityEnd    -- ;
+    | EntityEndAtt -- missing the ; and in an attribute
+    | Warn String
+    | Pos Position
+      deriving (Show,Eq)
+
+errSeen x = Warn $ "Unexpected " ++ show x
+errWant x = Warn $ "Expected " ++ show x
+
+data S = S
+    {s :: S
+    ,tl :: S
+    ,hd :: Char
+    ,eof :: Bool
+    ,next :: String -> Maybe S
+    ,pos :: [Out] -> [Out]
+    }
+
+
+expand :: Position -> String -> S
+expand p text = res
+    where res = S{s = res
+                 ,tl = expand (positionChar p (head text)) (tail text)
+                 ,hd = if null text then '\0' else head text
+                 ,eof = null text
+                 ,next = next p text
+                 ,pos = (Pos p:)
+                 }
+
+          next p (t:ext) (s:tr) | t == s = next (positionChar p t) ext tr
+          next p text [] = Just $ expand p text
+          next _ _ _ = Nothing
+
+
+infixr &
+
+class Outable a where (&) :: a -> [Out] -> [Out]
+instance Outable Char where (&) = ampChar
+instance Outable Out where (&) = ampOut
+ampChar x y = Char x : y
+ampOut x y = x : y
+
+
+state :: String -> S
+state s = expand nullPosition s
+
+---------------------------------------------------------------------
+-- TOP LAYER
+
+
+output :: forall str . StringLike str => ParseOptions str -> [Out] -> [Tag str]
+output ParseOptions{..} x = (if optTagTextMerge then tagTextMerge else id) $ go ((nullPosition,[]),x)
+    where
+        -- main choice loop
+        go :: ((Position,[Tag str]),[Out]) -> [Tag str]
+        go ((p,ws),xs) | p `seq` False = [] -- otherwise p is a space leak when optTagPosition == False
+        go ((p,ws),xs) | not $ null ws = (if optTagWarning then (reverse ws++) else id) $ go ((p,[]),xs)
+        go ((p,ws),Pos p2:xs) = go ((p2,ws),xs)
+
+        go x | isChar x = pos x $ TagText a : go y
+            where (y,a) = charsStr x
+        go x | isTag x = pos x $ TagOpen a b : (if isTagEndClose z then pos x $ TagClose a : go (next z) else go (skip isTagEnd z))
+            where (y,a) = charsStr $ next x
+                  (z,b) = atts y
+        go x | isTagShut x = pos x $ (TagClose a:) $
+                (if not (null b) then warn x "Unexpected attributes in close tag" else id) $
+                if isTagEndClose z then warn x "Unexpected self-closing in close tag" $ go (next z) else go (skip isTagEnd z)
+            where (y,a) = charsStr $ next x
+                  (z,b) = atts y
+        go x | isComment x = pos x $ TagComment a : go (skip isCommentEnd y)
+            where (y,a) = charsStr $ next x
+        go x | isEntity x = poss x ((if optTagWarning then id else filter (not . isTagWarning)) $ optEntityData a) ++ go (skip isEntityEnd y) 
+            where (y,a) = charsStr $ next x
+        go x | isEntityChr x = pos x $ TagText (fromChar $ entityChr x a) : go (skip isEntityEnd y)
+            where (y,a) = chars $ next x
+        go x | Just a <- fromWarn x = if optTagWarning then pos x $ TagWarning (fromString a) : go (next x) else go (next x)
+        go x | isEof x = []
+
+        atts :: ((Position,[Tag str]),[Out]) -> ( ((Position,[Tag str]),[Out]) , [(str,str)] )
+        atts x | isAttName x = second ((a,b):) $ atts z
+            where (y,a) = charsStr (next x)
+                  (z,b) = if isAttVal y then charsEntsStr (next y) else (y, empty)
+        atts x | isAttVal x = second ((empty,a):) $ atts y
+            where (y,a) = charsEntsStr (next x)
+        atts x = (x, [])
+
+        -- chars
+        chars x = charss False x
+        charsStr x = (id *** fromString) $ chars x
+        charsEntsStr x = (id *** fromString) $ charss True x
+
+        -- loop round collecting characters, if the b is set including entity
+        charss :: Bool -> ((Position,[Tag str]),[Out]) -> ( ((Position,[Tag str]),[Out]) , String)
+        charss t x | Just a <- fromChr x = (y, a:b)
+            where (y,b) = charss t (next x)
+        charss t x | t, isEntity x = second (toString n ++) $ charss t $ addWarns m z
+            where (y,a) = charsStr $ next x
+                  b = not $ isEntityEndAtt y
+                  z = if b then skip isEntityEnd y else next y
+                  (n,m) = optEntityAttrib (a,b)
+        charss t x | t, isEntityChr x = second (entityChr x a:) $ charss t z
+            where (y,a) = chars $ next x
+                  (z,b) = charss t $ if isEntityEnd y then next y else skip isEntityEndAtt y
+        charss t ((_,w),Pos p:xs) = charss t ((p,w),xs)
+        charss t x | Just a <- fromWarn x = charss t $ (if optTagWarning then addWarns [TagWarning $ fromString a] else id) $ next x
+        charss t x = (x, [])
+
+        -- utility functions
+        next x = second (drop 1) x
+        skip f x = assert (isEof x || f x) (next x)
+        addWarns ws x@((p,w),y) = ((p, reverse (poss x ws) ++ w), y)
+        pos ((p,_),_) rest = if optTagPosition then tagPosition p : rest else rest
+        warn x s rest = if optTagWarning then pos x $ TagWarning (fromString s) : rest else rest
+        poss x = concatMap (\w -> pos x [w]) 
+
+
+entityChr x s | isEntityNum x = chr $ read s
+              | isEntityHex x = chr $ fst $ head $ readHex s
+
+
+isEof (_,[]) = True; isEof _ = False
+isChar (_,Char{}:_) = True; isChar _ = False
+isTag (_,Tag{}:_) = True; isTag _ = False
+isTagShut (_,TagShut{}:_) = True; isTagShut _ = False
+isAttName (_,AttName{}:_) = True; isAttName _ = False
+isAttVal (_,AttVal{}:_) = True; isAttVal _ = False
+isTagEnd (_,TagEnd{}:_) = True; isTagEnd _ = False
+isTagEndClose (_,TagEndClose{}:_) = True; isTagEndClose _ = False
+isComment (_,Comment{}:_) = True; isComment _ = False
+isCommentEnd (_,CommentEnd{}:_) = True; isCommentEnd _ = False
+isEntity (_,Entity{}:_) = True; isEntity _ = False
+isEntityChr (_,EntityNum{}:_) = True; isEntityChr (_,EntityHex{}:_) = True; isEntityChr _ = False
+isEntityNum (_,EntityNum{}:_) = True; isEntityNum _ = False
+isEntityHex (_,EntityHex{}:_) = True; isEntityHex _ = False
+isEntityEnd (_,EntityEnd{}:_) = True; isEntityEnd _ = False
+isEntityEndAtt (_,EntityEndAtt{}:_) = True; isEntityEndAtt _ = False
+isWarn (_,Warn{}:_) = True; isWarn _ = False
+
+fromChr (_,Char x:_) = Just x ; fromChr _ = Nothing
+fromWarn (_,Warn x:_) = Just x ; fromWarn _ = Nothing
+
+
+-- Merge all adjacent TagText bits
+tagTextMerge :: StringLike str => [Tag str] -> [Tag str]
+tagTextMerge (TagText x:xs) = TagText (strConcat (x:a)) : tagTextMerge b
+    where
+        (a,b) = f xs
+
+        -- additional brackets on 3 lines to work around HSE 1.3.2 bugs with pattern fixities
+        f (TagText x:xs) = (x:a,b)
+            where (a,b) = f xs
+        f (TagPosition{}:(x@TagText{}:xs)) = f $ x : xs
+        f x = g x id x
+
+        g o op (p@TagPosition{}:(w@TagWarning{}:xs)) = g o (op . (p:) . (w:)) xs
+        g o op (w@TagWarning{}:xs) = g o (op . (w:)) xs
+        g o op (p@TagPosition{}:(x@TagText{}:xs)) = f $ p : x : op xs
+        g o op (x@TagText{}:xs) = f $ x : op xs
+        g o op _ = ([], o)
+
+tagTextMerge (x:xs) = x : tagTextMerge xs
+tagTextMerge [] = []
diff --git a/Text/HTML/TagSoup/Manual.hs b/Text/HTML/TagSoup/Manual.hs
new file mode 100644
--- /dev/null
+++ b/Text/HTML/TagSoup/Manual.hs
@@ -0,0 +1,13 @@
+
+module Text.HTML.TagSoup.Manual(parseTagsOptions) where
+
+import Text.HTML.TagSoup.Specification
+import Text.HTML.TagSoup.Implementation
+import Text.HTML.TagSoup.Type
+import Text.HTML.TagSoup.Options
+import Text.StringLike
+
+
+parseTagsOptions :: StringLike str => ParseOptions str -> str -> [Tag str]
+parseTagsOptions opts = output opts . parse . toString
+
diff --git a/Text/HTML/TagSoup/Match.hs b/Text/HTML/TagSoup/Match.hs
--- a/Text/HTML/TagSoup/Match.hs
+++ b/Text/HTML/TagSoup/Match.hs
@@ -5,35 +5,35 @@
 
 
 -- | match an opening tag
-tagOpen :: (String -> Bool) -> ([Attribute] -> Bool) -> Tag -> Bool
+tagOpen :: (str -> Bool) -> ([Attribute str] -> Bool) -> Tag str -> Bool
 tagOpen pName pAttrs (TagOpen name attrs) =
    pName name && pAttrs attrs
 tagOpen _ _ _ = False
 
 -- | match an closing tag
-tagClose :: (String -> Bool) -> Tag -> Bool
+tagClose :: (str -> Bool) -> Tag str -> Bool
 tagClose pName (TagClose name) = pName name
 tagClose _ _ = False
 
 -- | match a text
-tagText :: (String -> Bool) -> Tag -> Bool
+tagText :: (str -> Bool) -> Tag str -> Bool
 tagText p (TagText text) = p text
 tagText _ _ = False
 
-tagComment :: (String -> Bool) -> Tag -> Bool
+tagComment :: (str -> Bool) -> Tag str -> Bool
 tagComment p (TagComment text) = p text
 tagComment _ _ = False
 
 
 -- | match a opening tag's name literally
-tagOpenLit :: String -> ([Attribute] -> Bool) -> Tag -> Bool
+tagOpenLit :: Eq str => str -> ([Attribute str] -> Bool) -> Tag str -> Bool
 tagOpenLit name = tagOpen (name==)
 
 -- | match a closing tag's name literally
-tagCloseLit :: String -> Tag -> Bool
+tagCloseLit :: Eq str => str -> Tag str -> Bool
 tagCloseLit name = tagClose (name==)
 
-tagOpenAttrLit :: String -> Attribute -> Tag -> Bool
+tagOpenAttrLit :: Eq str => str -> Attribute str -> Tag str -> Bool
 tagOpenAttrLit name attr =
    tagOpenLit name (anyAttrLit attr)
 
@@ -43,45 +43,45 @@
 If an attribute occurs multiple times,
 all occurrences are checked.
 -}
-tagOpenAttrNameLit :: String -> String -> (String -> Bool) -> Tag -> Bool
+tagOpenAttrNameLit :: Eq str => str -> str -> (str -> Bool) -> Tag str -> Bool
 tagOpenAttrNameLit tagName attrName pAttrValue =
    tagOpenLit tagName
       (anyAttr (\(name,value) -> name==attrName && pAttrValue value))
 
 
--- | Check if the 'Tag' is 'TagOpen' and matches the given name
-tagOpenNameLit :: String -> Tag -> Bool
+-- | Check if the 'Tag str' is 'TagOpen' and matches the given name
+tagOpenNameLit :: Eq str => str -> Tag str -> Bool
 tagOpenNameLit name = tagOpenLit name (const True)
 
--- | Check if the 'Tag' is 'TagClose' and matches the given name
-tagCloseNameLit :: String -> Tag -> Bool
+-- | Check if the 'Tag str' is 'TagClose' and matches the given name
+tagCloseNameLit :: Eq str => str -> Tag str -> Bool
 tagCloseNameLit name = tagCloseLit name
 
 
 
 
-anyAttr :: ((String,String) -> Bool) -> [Attribute] -> Bool
+anyAttr :: ((str,str) -> Bool) -> [Attribute str] -> Bool
 anyAttr = any
 
-anyAttrName :: (String -> Bool) -> [Attribute] -> Bool
+anyAttrName :: (str -> Bool) -> [Attribute str] -> Bool
 anyAttrName p = any (p . fst)
 
-anyAttrValue :: (String -> Bool) -> [Attribute] -> Bool
+anyAttrValue :: (str -> Bool) -> [Attribute str] -> Bool
 anyAttrValue p = any (p . snd)
 
 
-anyAttrLit :: (String,String) -> [Attribute] -> Bool
+anyAttrLit :: Eq str => (str,str) -> [Attribute str] -> Bool
 anyAttrLit attr = anyAttr (attr==)
 
-anyAttrNameLit :: String -> [Attribute] -> Bool
+anyAttrNameLit :: Eq str => str -> [Attribute str] -> Bool
 anyAttrNameLit name = anyAttrName (name==)
 
-anyAttrValueLit :: String -> [Attribute] -> Bool
+anyAttrValueLit :: Eq str => str -> [Attribute str] -> Bool
 anyAttrValueLit value = anyAttrValue (value==)
 
 
 
-getTagContent :: String -> ([Attribute] -> Bool) -> [Tag] -> [Tag]
+getTagContent :: Eq str => str -> ([Attribute str] -> Bool) -> [Tag str] -> [Tag str]
 getTagContent name pAttrs =
    takeWhile (not . tagCloseLit name) . drop 1 .
    head . sections (tagOpenLit name pAttrs)
diff --git a/Text/HTML/TagSoup/Options.hs b/Text/HTML/TagSoup/Options.hs
new file mode 100644
--- /dev/null
+++ b/Text/HTML/TagSoup/Options.hs
@@ -0,0 +1,50 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+
+module Text.HTML.TagSoup.Options where
+
+import Data.Typeable
+import Text.HTML.TagSoup.Type
+import Text.HTML.TagSoup.Entity
+import Text.StringLike
+
+
+-- | These options control how 'parseTags' works.
+data ParseOptions str = ParseOptions
+    {optTagPosition :: Bool -- ^ Should 'TagPosition' values be given before some items (default=False,fast=False)
+    ,optTagWarning :: Bool  -- ^ Should 'TagWarning' values be given (default=False,fast=False)
+    ,optEntityData :: str -> [Tag str] -- ^ How to lookup an entity
+    ,optEntityAttrib :: (str,Bool) -> (str,[Tag str]) -- ^ How to lookup an entity in an attribute (Bool = has ending @';'@?)
+    ,optTagTextMerge :: Bool -- ^ Require no adjacent 'TagText' values (default=True,fast=False)
+    }
+    deriving Typeable
+
+
+-- | The default parse options value, described in 'ParseOptions'.
+parseOptions :: StringLike str => ParseOptions str
+parseOptions = ParseOptions False False entityData entityAttrib True
+    where
+        entityData x = case lookupEntity y of
+            Just y -> [TagText $ fromChar y]
+            Nothing -> [TagText $ fromString $ "&" ++ y ++ ";"
+                       ,TagWarning $ fromString $ "Unknown entity: " ++ y]
+            where y = toString x
+
+        entityAttrib (x,b) = case lookupEntity y of
+            Just y -> (fromChar y, [])
+            Nothing -> (fromString $ "&" ++ y ++ [';'|b], [TagWarning $ fromString $ "Unknown entity: " ++ y])
+            where y = toString x
+
+
+-- | A 'ParseOptions' structure optimised for speed, following the fast options.
+parseOptionsFast :: StringLike str => ParseOptions str
+parseOptionsFast = parseOptions{optTagTextMerge=False}
+
+
+-- | Change the underlying string type of a 'ParseOptions' value.
+fmapParseOptions :: (StringLike from, StringLike to) => ParseOptions from -> ParseOptions to
+fmapParseOptions (ParseOptions a b c d e) = ParseOptions a b c2 d2 e
+    where
+        c2 x = map (fmap castString) $ c $ castString x
+        d2 (x,y) = (castString r, map (fmap castString) s)
+            where (r,s) = d (castString x, y)
+
diff --git a/Text/HTML/TagSoup/Parser.hs b/Text/HTML/TagSoup/Parser.hs
--- a/Text/HTML/TagSoup/Parser.hs
+++ b/Text/HTML/TagSoup/Parser.hs
@@ -1,391 +1,26 @@
-{-# OPTIONS_GHC -w #-}
 
-{-
-The XML spec is done mainly from memory. The only special bits are:
-
-Attributes
-----------
-
-"foo" as an attribute comes out as an attribute with a value and no text
-to allow DOCTYPE tags to get parsed as normal.
-
-References (aka Character and Entity References)
-----------
-
-A character reference is one of:
-
-entity = '&#' [0-9]+ ';' 
-       | '&#x' [0-9a-fA-F]+ ';'
-       | '&' name ';'
-
-The maximum character reference is 0x10FFFF
-
-name = (letter | '_' | ':') (namechar)*
-namechar = letter | digit | '.' | '-' | '_' | ':' | combiningchar | extender
-combiningchar and extender are assumed to be empty
-letter = isAlpha
-digit = isDigit
--}
-
-
 module Text.HTML.TagSoup.Parser(
     parseTags, parseTagsOptions,
-    ParseOptions(..), parseOptions
+    ParseOptions(..), parseOptions, parseOptionsFast
     ) where
 
 import Text.HTML.TagSoup.Type
-import Text.HTML.TagSoup.Entity
-import Control.Monad.State
-import Data.Char
-import Data.List
-import Data.Maybe
-
-
-infix 9 ?->
-
-(?->) :: Bool -> [x] -> [x]
-(?->) b true = if b then true else []
-
----------------------------------------------------------------------
--- * ParseOptions
-
-data ParseOptions = ParseOptions
-    {optTagPosition :: Bool -- ^ Should 'TagPosition' values be given before every item
-    ,optTagWarning :: Bool -- ^ Should 'TagWarning' values be given
-    ,optLookupEntity :: String -> [Tag] -- ^ How to lookup an entity
-    ,optMaxEntityLength :: Maybe Int -- ^ The maximum length of an entities content
-                                     --   (Nothing for no maximum, default to 10)
-    }
-
-
--- Default 'ParseOptions' structure
-parseOptions :: ParseOptions
-parseOptions = ParseOptions False False f (Just 10)
-    where
-        f x = case lookupEntity x of
-                  Nothing -> [TagText $ "&" ++ x ++ ";", TagWarning $ "Unknown entity: &" ++ x ++ ";"]
-                  Just x -> [TagText [x]]
+import Text.HTML.TagSoup.Options
+import Text.StringLike
+import qualified Text.HTML.TagSoup.Generated as Gen
 
 
-parseTags :: String -> [Tag]
+-- | Parse a string to a list of tags, using an HTML 5 compliant parser.
+--
+-- > parseTags "<hello>my&amp;</world>" == [TagOpen "hello" [],TagText "my&",TagClose "world"]
+parseTags :: StringLike str => str -> [Tag str]
 parseTags = parseTagsOptions parseOptions
 
-tagWarn :: ParseOptions -> String -> [Tag]
-tagWarn opts x = [TagWarning x | optTagWarning opts]
 
----------------------------------------------------------------------
--- * Positions
-
--- All positions are stored as a row and a column, with (1,1) being the
--- top-left position
-
-data Position = Position !Row !Column
-
-
-updateOnString :: Position -> String -> Position
-updateOnString = foldl' updateOnChar
-
-updateOnChar   :: Position -> Char -> Position
-updateOnChar (Position r c) x = case x of
-    '\n' -> Position (r+1) c
-    '\t' -> Position r (c + 8 - mod (c-1) 8)
-    _    -> Position r (c+1)
-
-
-tagPos :: ParseOptions -> Position -> [Tag]
-tagPos opts (Position r c) = [TagPosition r c | optTagPosition opts]
-
-tagPosWarn :: ParseOptions -> Position -> String -> [Tag]
-tagPosWarn opts p x = optTagWarning opts ?-> (tagPos opts p ++ [TagWarning x])
-
-tagPosWarnFix :: ParseOptions -> Position -> [Tag] -> [Tag]
-tagPosWarnFix opts p = addPositions . remWarnings
-    where
-        remWarnings = if optTagWarning opts then id else filter (not . isTagWarning)
-        addPositions = concatMap (\x -> tagPos opts p ++ [x])
-    
-
----------------------------------------------------------------------
--- * Driver
-
-parseTagsOptions :: ParseOptions -> String -> [Tag]
-parseTagsOptions opts x = mergeTexts $ evalState (parse opts) $ Value x (Position 0 0)
-
-
--- | Combine adjacent text nodes.
---
---   If two text nodes are separated only a position node, delete the position.
---   If two text nodes are separated only by a warning, move the warning afterwards.
---   If a position immediately proceeds a warning, count that into the warning.
+-- | Parse a string to a list of tags, using settings supplied by the 'ParseOptions' parameter,
+--   eg. to output position information:
 --
---   Note: this function leaks stack on Hugs.
-mergeTexts :: [Tag] -> [Tag]
-mergeTexts (TagText x:xs) = (TagText $ concat $ x:texts) : warns ++ mergeTexts rest
-    where
-        (texts,warns,rest) = f xs
-
-        f (TagText x:xs) = (x:a,b,c)
-            where (a,b,c) = f xs
-        f (TagPosition _ _:TagText x:xs) = (x:a,b,c)
-            where (a,b,c) = f xs
-
-        f (p@TagPosition{}:TagWarning y:xs) = (a,p:TagWarning y:b,c)
-            where (a,b,c) = f xs
-        f (TagWarning x:xs) = (a,TagWarning x:b,c)
-            where (a,b,c) = f xs
-
-        f xs = ([],[],xs)
-
-mergeTexts (x:xs) = x : mergeTexts xs
-mergeTexts [] = []
-
-
----------------------------------------------------------------------
--- * Combinators
-
-data Value = Value String !Position
-
-type Parser a = State Value a
-
-
-isNameCharFirst x = isAlphaNum x || x `elem` "_:"
-isNameChar x = isAlphaNum x || x `elem` "-_:."
-
--- Read and consume n characters from the stream, updating the position.
--- Highly likely to be a potential for space leaks from this, unless @n@ is bounded.
-consume :: Int -> Parser ()
-consume n = do
-    Value s p <- get
-    let (a,b) = splitAt n s
-    put $ Value b (updateOnString p a)
-
-
--- Break once an end string is encountered.
--- Return the string before, and a boolean indicating if the end was matched.
-breakOn :: String -> Parser (String,Bool)
-breakOn end = do
-    Value s p <- get
-    if null s then
-        return ("",True)
-     else if end `isPrefixOf` s then
-        consume (length end) >> return ("",False)
-     else do
-        consume 1
-        ~(a,b) <- breakOn end
-        return (head s:a,b)
-
--- Break after an HTML name (entity, attribute or tag name)
--- Note: potential space leak with consume at the end
-breakName :: Parser String
-breakName = do
-    Value s p <- get
-    if not (null s) && isNameCharFirst (head s) then do
-        let (a,b) = span isNameChar s
-        consume (length a)
-        return a
-     else
-        return ""
-
--- Break after a number has been read
--- Note: potential space leak with consume at the end
-breakNumber :: Parser (Maybe Int)
-breakNumber = do
-    Value s p <- get
-    if not (null s) && isDigit (head s) then do
-        let (a,b) = span isDigit s
-        consume (length a)
-        return $ Just $ read a
-     else
-        return Nothing
-
-
--- Drop a number of spaces
--- Note: potential space leak with consume at the end
-dropSpaces :: Parser ()
-dropSpaces = do
-    Value s p <- get
-    let n = length $ takeWhile isSpace s
-    consume n
-
-
----------------------------------------------------------------------
--- * Parser
-
-parse :: ParseOptions -> Parser [Tag]
-parse opts = do
-    Value s p <- get
-    case s of
-        '<':'!':'-':'-':_ -> consume 4 >> comment opts p
-        '<':'/':_         -> consume 2 >> close opts p
-        '<':_             -> consume 1 >> open opts p
-        []                -> return []
-        '&':_             -> do
-            consume 1
-            s <- entity opts p
-            rest <- parse opts
-            return $ s ++ rest
-        s:ss              -> do
-            consume 1
-            rest <- parse opts
-            return $ tagPos opts p ++ [TagText [s]] ++ rest
-
--- have read "<!--"
-comment opts p1 = do
-    ~(inner,bad) <- breakOn "-->"
-    rest <- parse opts
-    return $ tagPos opts p1 ++ [TagComment inner] ++
-             (bad ?-> tagPosWarn opts p1 "Unexpected end when looking for \"-->\"") ++
-             rest
-
-
-close opts p1 = do
-        name <- breakName
-        dropSpaces
-        ~(Value s p) <- get
-        rest <- f s
-        return $ tagPos opts p1 ++ [TagClose name] ++
-                 (null name ?-> tagPosWarn opts p1 "Empty name in close tag") ++
-                 rest
-    where
-        f ('>':s) = do
-            consume 1
-            rest <- parse opts
-            return rest
-
-        f _ = do
-            ~(_,bad) <- breakOn ">"
-            rest <- parse opts
-            return $ tagPosWarn opts p1 "Junk in closing tag" ++
-                     bad ?-> tagPosWarn opts p1 "Unexpected end when looking for \">\"" ++
-                     rest
-
-
--- an open tag, perhaps <? or <!
-open opts p1 = do
-    Value s p <- get
-    prefix <- if take 1 s `elem` ["!","?"] then consume 1 >> return [head s] else return ""
-    name <- liftM (prefix++) breakName
-    if null name then do
-        rest <- parse opts
-        return $ tagPos opts p1 ++ [TagText ('<':prefix)] ++ tagPosWarn opts p1 "Expected name of tag" ++ rest
-     else do
-        ~(atts,shut,warns) <- attribs opts p1
-        rest <- parse opts
-        return $ tagPos opts p1 ++ [TagOpen name atts] ++
-                 shut ?-> (tagPos opts p1 ++ [TagClose name]) ++
-                 warns ++ rest
-
-
--- read a list of attributes
--- return (the attributes read, if the tag is self-shutting, any warnings) 
-attribs :: ParseOptions -> Position -> Parser ([Attribute],Bool,[Tag])
-attribs opts p1 = do
-    dropSpaces
-    Value s p <- get
-    case s of
-        '/':'>':_ -> consume 2 >> return ([],True ,[])
-        '>':_     -> consume 1 >> return ([],False,[])
-        x:xs | x `elem` "'\"" -> do
-            ~(val,warns1) <- value opts
-            ~(atts,shut,warns2) <- attribs opts p1
-            return (("",val):atts,shut,warns1++warns2)
-        []        -> return ([],False,tagPosWarn opts p1 "Unexpected end when looking for \">\"")
-        _ -> attrib opts p1
-
-
--- read a single attribute
--- return (the attributes read, if the tag is self-shutting, any warnings) 
-attrib :: ParseOptions -> Position -> Parser ([Attribute],Bool,[Tag])
-attrib opts p1 = do
-    name <- breakName
-    if null name then do
-        consume 1
-        ~(atts,shut,warns) <- attribs opts p1
-        return (atts,shut,tagPosWarn opts p1 "Junk character in tag" ++ warns)
-     else do
-        ~(Value s p) <- get
-        ~(val,warns1) <- f s
-        ~(atts,shut,warns2) <- attribs opts p1
-        return ((name,val):atts,shut,warns1++warns2)
-    where
-        f ('=':s) = consume 1 >> value opts
-        f xs | not $ junk xs = return ([], [])
-             | otherwise = do
-                ~(Value s p) <- get
-                dropJunk
-                return ([], tagPosWarn opts p "Junk character in tag")
-
-        junk ('/':'>':_) = False
-        junk ('>':_) = False
-        junk (c:cs) | not $ isSpace c = True
-        junk _ = False
-        
-        dropJunk = do
-            ~(Value s p) <- get
-            when (junk s) $ consume 1 >> dropJunk
-
-
--- read a single value
--- return (value,warnings)
-value :: ParseOptions -> Parser (String,[Tag])
-value opts = do
-    Value s p <- get
-    case s of
-        '\"':_ -> consume 1 >> f p True "\""
-        '\'':_ -> consume 1 >> f p True "\'"
-        _ -> f p False " />"
-    where
-        f p1 quote end = do
-            Value s p <- get
-            case s of
-                '&':_ -> do
-                    consume 1
-                    ~(cs1,warns1) <- entityString opts p
-                    ~(cs2,warns2) <- f p1 quote end
-                    return (cs1++cs2,warns1++warns2)
-                c:_ | c `elem` end -> do
-                    if quote then consume 1 else return ()
-                    return ([],[])
-                c:_ -> do
-                    consume 1
-                    ~(cs,warns) <- f p1 quote end
-                    return (c:cs,warns)
-                [] -> return ([],tagPosWarn opts p1 "Unexpected end in attibute value")
-
-
-
--- have seen an &, and have consumed it
--- return a [Tag] to go in a tag stream
-entity :: ParseOptions -> Position -> Parser [Tag]
-entity opts p1 = do
-    Value s p <- get
-    case s of
-        '#':'x':_ -> f "#x" isHexDigit 
-        '#':_     -> f "#"  isDigit
-        _         -> f ""   isNameChar
-    where
-        f prefix match = do
-            consume (length prefix)
-            g match (reverse prefix) (fromMaybe maxBound (optMaxEntityLength opts))
-        
-        g match buf bound | bound < 0 = return $
-            tagPos opts p1 ++ [TagText ('&':reverse buf)] ++
-            tagPosWarn opts p1 "Unexpected '&' not in an entity"
-
-        g match buf bound = do
-            Value s p <- get
-            case s of
-                ';':_ -> do
-                    consume 1
-                    return $ tagPosWarnFix opts p1 $ optLookupEntity opts (reverse buf)
-                x:xs | match x -> consume 1 >> g match (x:buf) (bound-1) 
-                _ -> g match buf (-1)
-            
-
-
--- return the tag and some position information
-entityString :: ParseOptions -> Position -> Parser (String,[Tag])
-entityString opts p = do
-    tags <- entity opts p
-    let warnings = tagPosWarnFix opts p $ filter isTagWarning tags
-    return (innerText tags, warnings)
+-- > parseTagsOptions parseOptions{optTagPosition = True} "<hello>my&amp;</world>" ==
+-- >    [TagPosition 1 1,TagOpen "hello" [],TagPosition 1 8,TagText "my&",TagPosition 1 15,TagClose "world"]
+parseTagsOptions :: StringLike str => ParseOptions str -> str -> [Tag str]
+parseTagsOptions = Gen.parseTagsOptions
diff --git a/Text/HTML/TagSoup/Render.hs b/Text/HTML/TagSoup/Render.hs
--- a/Text/HTML/TagSoup/Render.hs
+++ b/Text/HTML/TagSoup/Render.hs
@@ -1,11 +1,9 @@
+{-# LANGUAGE PatternGuards #-}
 {-|
-    This module is preliminary and may change at a future date.
-    If you wish to use its features, please email me and I will
-    help evolve an API that suits you.
+    This module converts a list of 'Tag' back into a string.
 -}
 
 module Text.HTML.TagSoup.Render
-    {-# DEPRECATED "Not quite ready for use yet, email me if it looks useful to you" #-}
     (
     renderTags, renderTagsOptions,
     RenderOptions(..), renderOptions
@@ -15,42 +13,71 @@
 import qualified Data.IntMap as IntMap
 import Text.HTML.TagSoup.Entity
 import Text.HTML.TagSoup.Type
+import Text.StringLike
 
 
-data RenderOptions = RenderOptions
-    {optEscape :: Char -> String
+-- | These options control how 'renderTags' works.
+--
+--   The strange quirk of only minimizing @\<br\>@ tags is due to Internet Explorer treating
+--   @\<br\>\<\/br\>@ as @\<br\>\<br\>@.
+data RenderOptions str = RenderOptions
+    {optEscape :: str -> str        -- ^ Escape a piece of text (default = escape the four characters @&\"\<\>@)
+    ,optMinimize :: str -> Bool     -- ^ Minimise \<b\>\<\/b\> -> \<b/\> (default = minimise only @\<br\>@ tags)
     }
 
-renderOptions :: RenderOptions
-renderOptions = RenderOptions (\x -> IntMap.findWithDefault [x] (ord x) esc)
+
+-- | The default render options value, described in 'RenderOptions'.
+renderOptions :: StringLike str => RenderOptions str
+renderOptions = RenderOptions
+        (\x -> fromString $ concatMap esc1 $ toString x)
+        (\x -> toString x == "br")
     where esc = IntMap.fromList [(b, "&"++a++";") | (a,b) <- htmlEntities]
+          esc1 x = IntMap.findWithDefault [x] (ord x) esc
 
 
--- | Show a list of tags, as they might have been parsed
-renderTags :: [Tag] -> String
+fmapRenderOptions :: (StringLike a, StringLike b) => RenderOptions a -> RenderOptions b
+fmapRenderOptions (RenderOptions x y) = RenderOptions (castString . x . castString) (y . castString)
+
+
+-- | Show a list of tags, as they might have been parsed, using the default settings given in
+--   'RenderOptions'.
+--
+-- > renderTags [TagOpen "hello" [],TagText "my&",TagClose "world"] == "<hello>my&amp;</world>"
+renderTags :: StringLike str => [Tag str] -> str
 renderTags = renderTagsOptions renderOptions
 
 
-renderTagsOptions :: RenderOptions -> [Tag] -> String
-renderTagsOptions opts = tags
+-- | Show a list of tags using settings supplied by the 'RenderOptions' parameter,
+--   eg. to avoid escaping any characters one could do:
+--
+-- > renderTagsOptions renderOptions{optEscape = id} [TagText "my&"] == "my&"
+renderTagsOptions :: StringLike str => RenderOptions str -> [Tag str] -> str
+renderTagsOptions opts = strConcat . tags
     where
+        s = fromString
+        ss x = [s x]
+    
         tags (TagOpen name atts:TagClose name2:xs)
-            | name == name2 = open name atts " /" ++ tags xs
+            | name == name2 && optMinimize opts name = open name atts (s " /") ++ tags xs
+        tags (TagOpen name atts:xs) | Just ('?',_) <- uncons name = open name atts (s " ?") ++ tags xs
         tags (x:xs) = tag x ++ tags xs
         tags [] = []
 
-        tag (TagOpen name atts) = open name atts ""
-        tag (TagClose name) = "</" ++ name ++ ">"
-        tag (TagText text) = txt text
-        tag (TagComment text) = "<!--" ++ com text ++ "-->"
-        tag _ = ""
+        tag (TagOpen name atts) = open name atts (s "")
+        tag (TagClose name) = [s "</", name, s ">"]
+        tag (TagText text) = [txt text]
+        tag (TagComment text) = ss "<!--" ++ com text ++ ss "-->"
+        tag _ = ss ""
 
-        txt = concatMap (optEscape opts)
-        open name atts shut = "<" ++ name ++ concatMap att atts ++ shut ++ ">"
-        att (x,"") = " " ++ x
-        att ("",y) = " " ++ "\"" ++ txt y ++ "\""
-        att (x,y) = " " ++ x ++ "=\"" ++ txt y ++ "\""
+        txt = optEscape opts
+        open name atts shut = [s "<",name] ++ concatMap att atts ++ [shut,s ">"]
+        att (x,y) | xnull && ynull = [s " \"\""]
+                  | ynull = [s " ", x]
+                  | xnull = [s " \"",txt y,s "\""]
+                  | otherwise = [s " ",x,s "=\"",txt y,s "\""]
+            where (xnull, ynull) = (strNull x, strNull y)
 
-        com ('-':'-':'>':xs) = "-- >" ++ com xs
-        com (x:xs) = x : com xs
-        com [] = []
+        com xs | Just ('-',xs) <- uncons xs, Just ('-',xs) <- uncons xs, Just ('>',xs) <- uncons xs = s "-- >" : com xs
+        com xs = case uncons xs of
+            Nothing -> []
+            Just (x,xs) -> fromChar x : com xs
diff --git a/Text/HTML/TagSoup/Specification.hs b/Text/HTML/TagSoup/Specification.hs
new file mode 100644
--- /dev/null
+++ b/Text/HTML/TagSoup/Specification.hs
@@ -0,0 +1,308 @@
+{-# LANGUAGE RecordWildCards, PatternGuards #-}
+
+module Text.HTML.TagSoup.Specification(parse) where
+
+import Text.HTML.TagSoup.Implementation
+import Data.Char
+
+
+white x = x `elem` "\t\n\f "
+
+-- We make some generalisations:
+-- <!name is a valid tag start closed by >
+-- <?name is a valid tag start closed by ?>
+-- </!name> is a valid closing tag
+-- </?name> is a valid closing tag
+-- <a "foo"> is a valid tag attibute, i.e missing an attribute name
+-- We also don't do lowercase conversion
+-- Entities are handled without a list of known entity names
+-- We don't have RCData, CData or Escape modes (only effects dat and tagOpen)
+
+-- 9.2.4 Tokenization
+
+type Parser = S -> [Out]
+
+parse :: String -> [Out]
+parse = dat . state 
+
+-- 9.2.4.1 Data state
+dat :: Parser
+dat S{..} = pos $ case hd of
+    '&' -> charReference tl
+    '<' -> tagOpen tl
+    _ | eof -> []
+    _ -> hd & dat tl
+
+
+-- 9.2.4.2 Character reference data state
+charReference s = charRef dat False Nothing s
+
+
+-- 9.2.4.3 Tag open state
+tagOpen S{..} = case hd of
+    '!' -> markupDeclOpen tl
+    '/' -> closeTagOpen tl
+    _ | isAlpha hd -> Tag & hd & tagName False tl
+    '>' -> errSeen "<>" & '<' & '>' & dat tl
+    '?' -> neilXmlTagOpen tl -- NEIL
+    _ -> errSeen  "<" & '<' & dat s
+
+
+-- seen "<?", emitted []
+neilXmlTagOpen S{..} = pos $ case hd of
+    _ | isAlpha hd -> Tag & '?' & hd & tagName True tl
+    _ -> errSeen "<?" & '<' & '?' & dat s
+
+-- seen "?", expecting ">"
+neilXmlTagClose S{..} = pos $ case hd of
+    '>' -> TagEnd & dat tl
+    _ -> errSeen "?" & beforeAttName True s
+
+
+-- just seen ">" at the end, am given tl
+neilTagEnd xml S{..}
+    | xml = pos $ errWant "?>" & TagEnd & dat s
+    | otherwise = pos $ TagEnd & dat s
+
+
+-- 9.2.4.4 Close tag open state
+-- Deviation: We ignore the if CDATA/RCDATA bits and tag matching
+-- Deviation: On </> we output </> to the text
+-- Deviation: </!name> is a closing tag, not a bogus comment
+closeTagOpen S{..} = case hd of
+    _ | isAlpha hd || hd `elem` "?!" -> TagShut & hd & tagName False tl
+    '>' -> errSeen "</>" & '<' & '/' & '>' & dat tl
+    _ | eof -> '<' & '/' & dat s
+    _ -> errWant "tag name" & bogusComment s
+
+
+-- 9.2.4.5 Tag name state
+tagName xml S{..} = pos $ case hd of
+    _ | white hd -> beforeAttName xml tl
+    '/' -> selfClosingStartTag xml tl
+    '>' -> neilTagEnd xml tl
+    '?' | xml -> neilXmlTagClose tl
+    _ | isAlpha hd -> hd & tagName xml tl
+    _ | eof -> errWant (if xml then "?>" else ">") & dat s
+    _ -> hd & tagName xml tl
+
+
+-- 9.2.4.6 Before attribute name state
+beforeAttName xml S{..} = pos $ case hd of
+    _ | white hd -> beforeAttName xml tl
+    '/' -> selfClosingStartTag xml tl
+    '>' -> neilTagEnd xml tl
+    '?' | xml -> neilXmlTagClose tl
+    _ | hd `elem` "\'\"" -> beforeAttValue xml s -- NEIL
+    _ | hd `elem` "\"'<=" -> errSeen [hd] & AttName & hd & attName xml tl
+    _ | eof -> errWant (if xml then "?>" else ">") & dat s
+    _ -> AttName & hd & attName xml tl
+
+
+-- 9.2.4.7 Attribute name state
+attName xml S{..} = pos $ case hd of
+    _ | white hd -> afterAttName xml tl
+    '/' -> selfClosingStartTag xml tl
+    '=' -> beforeAttValue xml tl
+    '>' -> neilTagEnd xml tl
+    '?' | xml -> neilXmlTagClose tl
+    _ | hd `elem` "\"'<" -> errSeen [hd] & def
+    _ | eof -> errWant (if xml then "?>" else ">") & dat s
+    _ -> def
+    where def = hd & attName xml tl
+
+
+-- 9.2.4.8 After attribute name state
+afterAttName xml S{..} = pos $ case hd of
+    _ | white hd -> afterAttName xml tl
+    '/' -> selfClosingStartTag xml tl
+    '=' -> beforeAttValue xml tl
+    '>' -> neilTagEnd xml tl
+    '?' | xml -> neilXmlTagClose tl
+    _ | hd `elem` "\"'" -> AttVal & beforeAttValue xml s -- NEIL
+    _ | hd `elem` "\"'<" -> errSeen [hd] & def
+    _ | eof -> errWant (if xml then "?>" else ">") & dat s
+    _ -> def
+    where def = AttName & hd & attName xml tl
+
+-- 9.2.4.9 Before attribute value state
+beforeAttValue xml S{..} = pos $ case hd of
+    _ | white hd -> beforeAttValue xml tl
+    '\"' -> AttVal & attValueDQuoted xml tl
+    '&' -> AttVal & attValueUnquoted xml s
+    '\'' -> AttVal & attValueSQuoted xml tl
+    '>' -> errSeen "=" & neilTagEnd xml tl
+    '?' | xml -> neilXmlTagClose tl
+    _ | hd `elem` "<=" -> errSeen [hd] & def
+    _ | eof -> errWant (if xml then "?>" else ">") & dat s
+    _ -> def
+    where def = AttVal & hd & attValueUnquoted xml tl
+
+
+-- 9.2.4.10 Attribute value (double-quoted) state
+attValueDQuoted xml S{..} = pos $ case hd of
+    '\"' -> afterAttValueQuoted xml tl
+    '&' -> charRefAttValue (attValueDQuoted xml) (Just '\"') tl
+    _ | eof -> errWant "\"" & dat s
+    _ -> hd & attValueDQuoted xml tl
+
+
+-- 9.2.4.11 Attribute value (single-quoted) state
+attValueSQuoted xml S{..} = pos $ case hd of
+    '\'' -> afterAttValueQuoted xml tl
+    '&' -> charRefAttValue (attValueSQuoted xml) (Just '\'') tl
+    _ | eof -> errWant "\'" & dat s
+    _ -> hd & attValueSQuoted xml tl
+
+
+-- 9.2.4.12 Attribute value (unquoted) state
+attValueUnquoted xml S{..} = pos $ case hd of
+    _ | white hd -> beforeAttName xml tl
+    '&' -> charRefAttValue (attValueUnquoted xml) Nothing tl
+    '>' -> neilTagEnd xml tl
+    '?' | xml -> neilXmlTagClose tl
+    _ | hd `elem` "\"'<=" -> errSeen [hd] & def
+    _ | eof -> errWant (if xml then "?>" else ">") & dat s
+    _ -> def
+    where def = hd & attValueUnquoted xml tl
+
+
+-- 9.2.4.13 Character reference in attribute value state
+charRefAttValue :: Parser -> Maybe Char -> Parser
+charRefAttValue resume c s = charRef resume True c s
+
+
+-- 9.2.4.14 After attribute value (quoted) state
+afterAttValueQuoted xml S{..} = pos $ case hd of
+    _ | white hd -> beforeAttName xml tl
+    '/' -> selfClosingStartTag xml tl
+    '>' -> neilTagEnd xml tl
+    '?' | xml -> neilXmlTagClose tl
+    _ | eof -> dat s
+    _ -> errSeen [hd] & beforeAttName xml s
+
+
+-- 9.2.4.15 Self-closing start tag state
+selfClosingStartTag xml S{..} = pos $ case hd of
+    _ | xml -> errSeen "/" & beforeAttName xml s
+    '>' -> TagEndClose & dat tl
+    _ | eof -> errWant ">" & dat s
+    _ -> errSeen "/" & beforeAttName xml s
+
+
+-- 9.2.4.16 Bogus comment state
+bogusComment S{..} = Comment & bogusComment1 s
+bogusComment1 S{..} = pos $ case hd of
+    '>' -> CommentEnd & dat tl
+    _ | eof -> CommentEnd & dat s
+    _ -> hd & bogusComment1 tl
+
+
+-- 9.2.4.17 Markup declaration open state
+markupDeclOpen S{..} = pos $ case hd of
+    _ | Just s <- next "--" -> Comment & commentStart s
+    _ | isAlpha hd -> Tag & '!' & hd & tagName False tl -- NEIL
+    _ | Just s <- next "[CDATA[" -> cdataSection s
+    _ -> errWant "tag name" & bogusComment s
+
+
+-- 9.2.4.18 Comment start state
+commentStart S{..} = pos $ case hd of
+    '-' -> commentStartDash tl
+    '>' -> errSeen "<!-->" & CommentEnd & dat tl
+    _ | eof -> errWant "-->" & CommentEnd & dat s
+    _ -> hd & comment tl
+
+
+-- 9.2.4.19 Comment start dash state
+commentStartDash S{..} = pos $ case hd of
+    '-' -> commentEnd tl
+    '>' -> errSeen "<!--->" & CommentEnd & dat tl
+    _ | eof -> errWant "-->" & CommentEnd & dat s
+    _ -> '-' & hd & comment tl
+
+
+-- 9.2.4.20 Comment state
+comment S{..} = pos $ case hd of
+    '-' -> commentEndDash tl
+    _ | eof -> errWant "-->" & CommentEnd & dat s
+    _ -> hd & comment tl
+
+
+-- 9.2.4.21 Comment end dash state
+commentEndDash S{..} = pos $ case hd of
+    '-' -> commentEnd tl
+    _ | eof -> errWant "-->" & CommentEnd & dat s
+    _ -> '-' & hd & comment tl
+
+
+-- 9.2.4.22 Comment end state
+commentEnd S{..} = pos $ case hd of
+    '>' -> CommentEnd & dat tl
+    '-' -> errWant "-->" & '-' & commentEnd tl
+    _ | white hd -> errSeen "--" & '-' & '-' & hd & commentEndSpace tl
+    '!' -> errSeen "!" & commentEndBang tl
+    _ | eof -> errWant "-->" & CommentEnd & dat s
+    _ -> errSeen "--" & '-' & '-' & hd & comment tl
+
+
+-- 9.2.4.23 Comment end bang state
+commentEndBang S{..} = pos $ case hd of
+    '>' -> CommentEnd & dat tl
+    '-' -> '-' & '-' & '!' & commentEndDash tl
+    _ | eof -> errWant "-->" & CommentEnd & dat s
+    _ -> '-' & '-' & '!' & hd & comment tl
+
+
+-- 9.2.4.24 Comment end space state
+commentEndSpace S{..} = pos $ case hd of
+    '>' -> CommentEnd & dat tl
+    '-' -> commentEndDash tl
+    _ | white hd -> hd & commentEndSpace tl
+    _ | eof -> errWant "-->" & CommentEnd & dat s
+    _ -> hd & comment tl
+
+
+-- 9.2.4.38 CDATA section state
+cdataSection S{..} = pos $ case hd of
+    _ | Just s <- next "]]>" -> dat s
+    _ | eof -> dat s
+    _ | otherwise -> hd & cdataSection tl
+
+
+-- 9.2.4.39 Tokenizing character references
+-- Change from spec: this is reponsible for writing '&' if nothing is to be written
+charRef :: Parser -> Bool -> Maybe Char -> S -> [Out]
+charRef resume att end S{..} = pos $ case hd of
+    _ | eof || hd `elem` "\t\n\f <&" || maybe False (== hd) end -> '&' & resume s
+    '#' -> charRefNum resume s tl
+    _ -> charRefAlpha resume att s
+
+charRefNum resume o S{..} = pos $ case hd of
+    _ | hd `elem` "xX" -> charRefNum2 resume o True tl
+    _ -> charRefNum2 resume o False s
+
+charRefNum2 resume o hex S{..} = pos $ case hd of
+    _ | hexChar hex hd -> (if hex then EntityHex else EntityNum) & hd & charRefNum3 resume hex tl
+    _ -> errSeen "&" & '&' & resume o
+
+charRefNum3 resume hex S{..} = pos $ case hd of
+    _ | hexChar hex hd -> hd & charRefNum3 resume hex tl
+    ';' -> EntityEnd & resume tl
+    _ -> errWant ";" & EntityEnd & resume s
+
+charRefAlpha resume att S{..} = pos $ case hd of
+    _ | isAlpha hd -> Entity & hd & charRefAlpha2 resume att tl
+    _ -> errSeen "&" & '&' & resume s
+
+charRefAlpha2 resume att S{..} = pos $ case hd of
+    _ | alphaChar hd -> hd & charRefAlpha2 resume att tl
+    ';' -> EntityEnd & resume tl
+    _ | att -> EntityEndAtt & resume s
+    _ -> errWant ";" & EntityEnd & resume s
+
+
+alphaChar x = isAlphaNum x || x `elem` ":-_"
+
+hexChar False x = isDigit x
+hexChar True  x = isDigit x || (x >= 'a' && x <= 'f') || (x >= 'A' && x <= 'F')
diff --git a/Text/HTML/TagSoup/Tree.hs b/Text/HTML/TagSoup/Tree.hs
--- a/Text/HTML/TagSoup/Tree.hs
+++ b/Text/HTML/TagSoup/Tree.hs
@@ -1,7 +1,10 @@
 {-|
-    This module is preliminary and may change at a future date.
+    /NOTE/: This module is preliminary and may change at a future date.
     If you wish to use its features, please email me and I will
     help evolve an API that suits you.
+
+    This module is intended to help converting a list of tags into a
+    tree of tags.
 -}
 
 module Text.HTML.TagSoup.Tree
@@ -12,26 +15,30 @@
     ) where
 
 import Text.HTML.TagSoup.Type
+import Control.Arrow
 
 
-data TagTree = TagBranch String [Attribute] [TagTree]
-             | TagLeaf Tag
-             deriving Show
+data TagTree str = TagBranch str [Attribute str] [TagTree str]
+                 | TagLeaf (Tag str)
+                   deriving Show
 
+instance Functor TagTree where
+    fmap f (TagBranch x y z) = TagBranch (f x) (map (f***f) y) (map (fmap f) z)
+    fmap f (TagLeaf x) = TagLeaf (fmap f x)
 
 
 -- | Convert a list of tags into a tree. This version is not lazy at
 --   all, that is saved for version 2.
-tagTree :: [Tag] -> [TagTree]
+tagTree :: Eq str => [Tag str] -> [TagTree str]
 tagTree = g
     where
-        g :: [Tag] -> [TagTree]
+        g :: Eq str => [Tag str] -> [TagTree str]
         g [] = []
         g xs = a ++ map TagLeaf (take 1 b) ++ g (drop 1 b)
             where (a,b) = f xs
 
         -- the second tuple is either null or starts with a close
-        f :: [Tag] -> ([TagTree],[Tag])
+        f :: Eq str => [Tag str] -> ([TagTree str],[Tag str])
         f (TagOpen name atts:rest) =
             case f rest of
                 (inner,[]) -> (TagLeaf (TagOpen name atts):inner, [])
@@ -46,7 +53,7 @@
         f [] = ([], [])
 
 
-flattenTree :: [TagTree] -> [Tag]
+flattenTree :: [TagTree str] -> [Tag str]
 flattenTree xs = concatMap f xs
     where
         f (TagBranch name atts inner) =
@@ -54,14 +61,14 @@
         f (TagLeaf x) = [x]
 
 
-universeTree :: [TagTree] -> [TagTree]
+universeTree :: [TagTree str] -> [TagTree str]
 universeTree = concatMap f
     where
         f t@(TagBranch _ _ inner) = t : universeTree inner
         f x = [x]
 
 
-transformTree :: (TagTree -> [TagTree]) -> [TagTree] -> [TagTree]
+transformTree :: (TagTree str -> [TagTree str]) -> [TagTree str] -> [TagTree str]
 transformTree act = concatMap f
     where
         f (TagBranch a b inner) = act $ TagBranch a b (transformTree act inner)
diff --git a/Text/HTML/TagSoup/Type.hs b/Text/HTML/TagSoup/Type.hs
--- a/Text/HTML/TagSoup/Type.hs
+++ b/Text/HTML/TagSoup/Type.hs
@@ -1,11 +1,15 @@
+{-# LANGUAGE DeriveDataTypeable #-}
 -- | The central type in TagSoup
 
 module Text.HTML.TagSoup.Type(
     -- * Data structures and parsing
-    Tag(..), Attribute, Row, Column,
+    StringLike, Tag(..), Attribute, Row, Column,
+    
+    -- * Position manipulation
+    Position(..), tagPosition, nullPosition, positionChar, positionString,
 
     -- * Tag identification
-    isTagOpen, isTagClose, isTagText, isTagWarning,
+    isTagOpen, isTagClose, isTagText, isTagWarning, isTagPosition,
     isTagOpenName, isTagCloseName,
 
     -- * Extraction
@@ -16,75 +20,113 @@
 
 
 import Data.Char
+import Data.List
 import Data.Maybe
-
+import Text.StringLike
+import Data.Data(Data, Typeable)
 
 -- | An HTML attribute @id=\"name\"@ generates @(\"id\",\"name\")@
-type Attribute = (String,String)
+type Attribute str = (str,str)
 
+-- | The row/line of a position, starting at 1
 type Row = Int
+
+-- | The column of a position, starting at 1
 type Column = Int
 
--- | An HTML element, a document is @[Tag]@.
---   There is no requirement for 'TagOpen' and 'TagClose' to match
-data Tag =
-     TagOpen String [Attribute]  -- ^ An open tag with 'Attribute's in their original order.
-   | TagClose String             -- ^ A closing tag
-   | TagText String              -- ^ A text node, guaranteed not to be the empty string
-   | TagComment String           -- ^ A comment
-   | TagWarning String           -- ^ Meta: Mark a syntax error in the input file
-   | TagPosition !Row !Column    -- ^ Meta: The position of a parsed element
-     deriving (Show, Eq, Ord)
 
+--- All positions are stored as a row and a column, with (1,1) being the
+--- top-left position
+data Position = Position !Row !Column deriving (Show,Eq,Ord)
 
+nullPosition :: Position
+nullPosition = Position 1 1
+
+positionString :: Position -> String -> Position
+positionString = foldl' positionChar
+
+positionChar :: Position -> Char -> Position
+positionChar (Position r c) x = case x of
+    '\n' -> Position (r+1) 1
+    '\t' -> Position r (c + 8 - mod (c-1) 8)
+    _    -> Position r (c+1)
+
+tagPosition :: Position -> Tag str
+tagPosition (Position r c) = TagPosition r c
+
+
+-- | A single HTML element. A whole document is represented by a list of @Tag@.
+--   There is no requirement for 'TagOpen' and 'TagClose' to match.
+data Tag str =
+     TagOpen str [Attribute str]  -- ^ An open tag with 'Attribute's in their original order
+   | TagClose str                 -- ^ A closing tag
+   | TagText str                  -- ^ A text node, guaranteed not to be the empty string
+   | TagComment str               -- ^ A comment
+   | TagWarning str               -- ^ Meta: A syntax error in the input file
+   | TagPosition !Row !Column     -- ^ Meta: The position of a parsed element
+     deriving (Show, Eq, Ord, Data, Typeable)
+
+instance Functor Tag where
+    fmap f (TagOpen x y) = TagOpen (f x) [(f a, f b) | (a,b) <- y]
+    fmap f (TagClose x) = TagClose (f x)
+    fmap f (TagText x) = TagText (f x)
+    fmap f (TagComment x) = TagComment (f x)
+    fmap f (TagWarning x) = TagWarning (f x)
+    fmap f (TagPosition x y) = TagPosition x y
+
+
 -- | Test if a 'Tag' is a 'TagOpen'
-isTagOpen :: Tag -> Bool
+isTagOpen :: Tag str -> Bool
 isTagOpen (TagOpen {})  = True; isTagOpen  _ = False
 
 -- | Test if a 'Tag' is a 'TagClose'
-isTagClose :: Tag -> Bool
+isTagClose :: Tag str -> Bool
 isTagClose (TagClose {}) = True; isTagClose _ = False
 
 -- | Test if a 'Tag' is a 'TagText'
-isTagText :: Tag -> Bool
+isTagText :: Tag str -> Bool
 isTagText (TagText {})  = True; isTagText  _ = False
 
 -- | Extract the string from within 'TagText', otherwise 'Nothing'
-maybeTagText :: Tag -> Maybe String
+maybeTagText :: Tag str -> Maybe str
 maybeTagText (TagText x) = Just x
 maybeTagText _ = Nothing
 
 -- | Extract the string from within 'TagText', crashes if not a 'TagText'
-fromTagText :: Tag -> String
+fromTagText :: Show str => Tag str -> str
 fromTagText (TagText x) = x
-fromTagText x = error ("(" ++ show x ++ ") is not a TagText")
+fromTagText x = error $ "(" ++ show x ++ ") is not a TagText"
 
 -- | Extract all text content from tags (similar to Verbatim found in HaXml)
-innerText :: [Tag] -> String
-innerText = concat . mapMaybe maybeTagText
+innerText :: StringLike str => [Tag str] -> str
+innerText = strConcat . mapMaybe maybeTagText
 
 -- | Test if a 'Tag' is a 'TagWarning'
-isTagWarning :: Tag -> Bool
+isTagWarning :: Tag str -> Bool
 isTagWarning (TagWarning {})  = True; isTagWarning _ = False
 
 -- | Extract the string from within 'TagWarning', otherwise 'Nothing'
-maybeTagWarning :: Tag -> Maybe String
+maybeTagWarning :: Tag str -> Maybe str
 maybeTagWarning (TagWarning x) = Just x
 maybeTagWarning _ = Nothing
 
+-- | Test if a 'Tag' is a 'TagPosition'
+isTagPosition :: Tag str -> Bool
+isTagPosition TagPosition{} = True; isTagPosition _ = False
+
 -- | Extract an attribute, crashes if not a 'TagOpen'.
 --   Returns @\"\"@ if no attribute present.
-fromAttrib :: String -> Tag -> String
-fromAttrib att (TagOpen _ atts) = fromMaybe [] $ lookup att atts
+fromAttrib :: (Show str, Eq str, StringLike str) => str -> Tag str -> str
+fromAttrib att (TagOpen _ atts) = fromMaybe empty $ lookup att atts
 fromAttrib _ x = error ("(" ++ show x ++ ") is not a TagOpen")
 
 
 -- | Returns True if the 'Tag' is 'TagOpen' and matches the given name
-isTagOpenName :: String -> Tag -> Bool
+isTagOpenName :: Eq str => str -> Tag str -> Bool
 isTagOpenName name (TagOpen n _) = n == name
 isTagOpenName _ _ = False
 
 -- | Returns True if the 'Tag' is 'TagClose' and matches the given name
-isTagCloseName :: String -> Tag -> Bool
+isTagCloseName :: Eq str => str -> Tag str -> Bool
 isTagCloseName name (TagClose n) = n == name
 isTagCloseName _ _ = False
diff --git a/Text/StringLike.hs b/Text/StringLike.hs
new file mode 100644
--- /dev/null
+++ b/Text/StringLike.hs
@@ -0,0 +1,79 @@
+{-# LANGUAGE TypeSynonymInstances #-}
+
+-- | /WARNING/: This module is /not/ intended for use outside the TagSoup library.
+--
+--   This module provides an abstraction for String's as used inside TagSoup. It allows
+--   TagSoup to work with String (list of Char), ByteString.Char8 and ByteString.Lazy.Char8.
+module Text.StringLike where
+
+import Data.List
+import Data.Maybe
+import Data.Typeable
+
+import qualified Data.ByteString.Char8 as BS
+import qualified Data.ByteString.Lazy.Char8 as LBS
+
+
+-- | A class to generalise TagSoup parsing over many types of string-like types.
+--   Examples are given for the String type.
+class (Typeable a, Eq a) => StringLike a where
+    -- | > empty = ""
+    empty :: a
+    -- | > cons = (:)
+    cons :: Char -> a -> a
+    -- | > uncons []     = Nothing
+    --   > uncons (x:xs) = Just (x, xs)
+    uncons :: a -> Maybe (Char, a)
+
+    -- | > toString = id
+    toString :: a -> String
+    -- | > fromString = id
+    fromString :: String -> a
+    -- | > fromChar = return
+    fromChar :: Char -> a
+    -- | > strConcat = concat
+    strConcat :: [a] -> a
+    -- | > strNull = null
+    strNull :: a -> Bool
+    -- | > append = (++)
+    append :: a -> a -> a
+
+
+-- | Convert a String from one type to another.
+castString :: (StringLike a, StringLike b) => a -> b
+castString = fromString . toString
+
+
+instance StringLike String where
+    uncons [] = Nothing
+    uncons (x:xs) = Just (x, xs)
+    toString = id
+    fromString = id
+    fromChar = (:[])
+    strConcat = concat
+    empty = []
+    strNull = null
+    cons c = (c:)
+    append = (++)
+
+instance StringLike BS.ByteString where
+    uncons = BS.uncons
+    toString = BS.unpack
+    fromString = BS.pack
+    fromChar = BS.singleton
+    strConcat = BS.concat
+    empty = BS.empty
+    strNull = BS.null
+    cons = BS.cons
+    append = BS.append
+
+instance StringLike LBS.ByteString where
+    uncons = LBS.uncons
+    toString = LBS.unpack
+    fromString = LBS.pack
+    fromChar = LBS.singleton
+    strConcat = LBS.concat
+    empty = LBS.empty
+    strNull = LBS.null
+    cons = LBS.cons
+    append = LBS.append
diff --git a/tagsoup.cabal b/tagsoup.cabal
--- a/tagsoup.cabal
+++ b/tagsoup.cabal
@@ -1,47 +1,60 @@
-Cabal-Version:  >= 1.2
-Name:           tagsoup
-Version:        0.6
-Copyright:      2006-8, Neil Mitchell
-Maintainer:     ndmitchell@gmail.com
-Author:         Neil Mitchell
-Homepage:       http://www-users.cs.york.ac.uk/~ndm/tagsoup/
-License:        BSD3
-Category:       XML
-License-File:   LICENSE
-Build-type:     Simple
-Synopsis:       Parsing and extracting information from (possibly malformed) HTML documents
-Description:
-    TagSoup is a library for extracting information out of unstructured HTML code,
-    sometimes known as tag-soup. The HTML does not have to be well formed, or render
-    properly within any particular framework. This library is for situations where
-    the author of the HTML is not cooperating with the person trying to extract the
-    information, but is also not trying to hide the information.
-Extra-Source-Files:
+cabal-version:  >= 1.6
+name:           tagsoup
+version:        0.8
+copyright:      Neil Mitchell 2006-2010
+author:         Neil Mitchell <ndmitchell@gmail.com>
+maintainer:     Neil Mitchell <ndmitchell@gmail.com>
+homepage:       http://community.haskell.org/~ndm/tagsoup/
+license:        BSD3
+category:       XML
+license-file:   LICENSE
+build-type:     Simple
+synopsis:       Parsing and extracting information from (possibly malformed) HTML/XML documents
+description:
+    TagSoup is a library for parsing HTML/XML. It supports the HTML 5 specification,
+    and can be used to parse either well-formed XML, or unstructured and malformed HTML
+    from the web. The library also provides useful functions to extract information
+    from an HTML document, making it ideal for screen-scraping.
+    .
+    Users should start from the "Text.HTML.TagSoup" module.
+extra-source-files:
     tagsoup.htm
 
-Flag splitBase
-    Description: Choose the new smaller, split-up base package.
+flag testprog
+    default: True
+    description: Build the test program
 
-Library
-    if flag(splitBase)
-        build-depends: base >= 3, network, mtl, containers
-    else
-        build-depends: base <  3, network, mtl
+library
+    ghc-options: -O2
+    build-depends: base == 4.*, network, mtl, containers, bytestring
 
-    GHC-Options: -Wall
-    Exposed-modules:
+    exposed-modules:
+        Text.HTML.Download
         Text.HTML.TagSoup
         Text.HTML.TagSoup.Entity
+        Text.HTML.TagSoup.Tree
+        Text.StringLike
+    other-modules:
+        Text.HTML.TagSoup.Generated
+        Text.HTML.TagSoup.Implementation
+        Text.HTML.TagSoup.Manual
         Text.HTML.TagSoup.Match
+        Text.HTML.TagSoup.Options
         Text.HTML.TagSoup.Parser
         Text.HTML.TagSoup.Render
-        Text.HTML.TagSoup.Tree
+        Text.HTML.TagSoup.Specification
         Text.HTML.TagSoup.Type
-        Text.HTML.Download
 
-Executable tagsoup
-    Main-Is: Main.hs
-    GHC-Options: -Wall
-    Other-Modules:
-        Example.Example
-        Example.Regress
+executable tagsoup
+    ghc-options: -O2
+    if flag(testprog)
+        buildable: True
+    else
+        buildable: False
+    build-depends: QuickCheck == 2.1.*, time, deepseq == 1.1.0.0, HTTP
+
+    main-is: Main.hs
+    other-modules:
+        TagSoup.Benchmark
+        TagSoup.Sample
+        TagSoup.Test
diff --git a/tagsoup.htm b/tagsoup.htm
--- a/tagsoup.htm
+++ b/tagsoup.htm
@@ -48,9 +48,9 @@
 
 p.rule {
     background-color: #ffb;
-	padding: 3px;
-	margin-left: 50px;
-	margin-right: 50px;
+    padding: 3px;
+    margin-left: 50px;
+    margin-right: 50px;
 }
         </style>
     </head>
@@ -59,14 +59,14 @@
 <h1>Drinking TagSoup by Example</h1>
 
 <p style="text-align:right;margin-bottom:25px;">
-    by <a href="http://www.cs.york.ac.uk/~ndm/">Neil Mitchell</a>
+    by <a href="http://community.haskell.org/~ndm/">Neil Mitchell</a>
 </p>
 
 <p>
-	TagSoup is a library for extracting information out of unstructured HTML code, sometimes known as tag-soup. The HTML does not have to be well formed, or render properly within any particular framework. This library is for situations where the author of the HTML is not cooperating with the person trying to extract the information, but is also not trying to hide the information.
+	TagSoup is a library for parsing HTML/XML. It supports the HTML 5 specification, and can be used to parse either well-formed XML, or unstructured and malformed HTML from the web. The library also provides useful functions to extract information from an HTML document, making it ideal for screen-scraping.
 </p>
 <p>
-	This document gives two particular examples, and two more may be found in the <a href="http://www.cs.york.ac.uk/fp/darcs/tagsoup/Example/Example.hs">Example</a> file from the darcs repository. The examples we give are:
+    This document gives two particular examples of scraping information from the web, while a few more may be found in the <a href="http://community.haskell.org/~ndm/darcs/tagsoup/TagSoup/Sample.hs">Sample</a> file from the darcs repository. The examples we give are:
 </p>
 <ol>
     <li>Obtaining the Hit Count from Haskell.org</li>
@@ -74,27 +74,21 @@
     <li>A brief overview of some other examples</li>
 </ol>
 <p>
-	The intial version of this library was written in Javascript and has been used for various commercial projects involving screen scraping. In the examples general hints on screen scraping are included, learnt from bitter experience. It should be noted that if you depend on data which someone else may change at any given time, you may be in for a shock!
+    The intial version of this library was written in Javascript and has been used for various commercial projects involving screen scraping. In the examples general hints on screen scraping are included, learnt from bitter experience. It should be noted that if you depend on data which someone else may change at any given time, you may be in for a shock!
 </p>
 <p>
-	This library was written without knowledge of the Java version of <a href="http://home.ccil.org/~cowan/XML/tagsoup/">TagSoup</a>. They have made a very different design decision: to ensure default attributes are present and to properly nest parsed tags. We do not do this - tags are merely a list devoid of nesting information.
+    This library was written without knowledge of the Java version of <a href="http://home.ccil.org/~cowan/XML/tagsoup/">TagSoup</a>. They have made a very different design decision: to ensure default attributes are present and to properly nest parsed tags. We do not do this - tags are merely a list devoid of nesting information.
 </p>
 
 <h3>Acknowledgements</h3>
 
 <p>
-    Thanks to Mike Dodds for persuading me to write this up as a library. Thanks to many people for debugging and code contributions, including: Gleb Alexeev, Ketil Malde, Conrad Parker, Henning Thielemann.
+    Thanks to Mike Dodds for persuading me to write this up as a library. Thanks to many people for debugging and code contributions, including: Gleb Alexeev, Ketil Malde, Conrad Parker, Henning Thielemann, Dino Morelli, Emily Mitchell, Gwern Branwen.
 </p>
 
 <h3>Version History</h3>
 
-<h4>0.4-0.6</h4>
-
-<ul>
-    <li>Addition of <tt>Text.HTML.TagSoup.Tree</tt> and <tt>Text.HTML.TagSoup.Render</tt>. Both these modules need review, so if they are useful to you please do send suggestions and comments.</li>
-    <li>In <tt>Text.HTML.TagSoup.Parser</tt>, the <tt>Options</tt> type has been renamed to <tt>ParseOptions</tt>.</li>
-    <li>In <tt>Text.HTML.TagSoup.Parser</tt>, the <tt>options</tt> function has been renamed to <tt>parseOptions</tt>.</li>
-</ul>
+<p>A changelog is kept at <a href="http://community.haskell.org/~ndm/darcs/tagsoup/CHANGES.txt">http://community.haskell.org/~ndm/darcs/tagsoup/CHANGES.txt</a>.</p>
 
 <h2>Potential Bugs</h2>
 
@@ -102,11 +96,8 @@
     There are two things that may go wrong with these examples:
 </p>
 <ul>
-    <li>
-        <i>The Websites being scraped may change.</i> There is nothing I can do about this, but if you suspect this is the case let me know, and I'll update the examples and tutorials. I have already done so once or twice, its only a few minutes work.
-    </li>
-    <li>
-        <i>The <tt>openURL</tt> method may not work.</i> This happens quite regularly, and depending on your server, proxies and direction of the wind, they may not work. The solution is to use <tt>wget</tt> to download the page locally, then use <tt>readFile</tt> instead. Hopefully a decent Haskell HTTP library will emerge, and that can be used instead.
+<li><i>The Websites being scraped may change.</i> There is nothing I can do about this, but if you suspect this is the case let me know, and I'll update the examples and tutorials. I have already done so once or twice, its only a few minutes work.    </li>
+    <li><i>The <tt>openURL</tt> method may not work.</i> This happens quite regularly, and depending on your server, proxies and direction of the wind, they may not work. The solution is to use <tt>wget</tt> to download the page locally, then use <tt>readFile</tt> instead. Hopefully a decent Haskell HTTP library will emerge, and that can be used instead.
     </li>
 </ul>
 
@@ -120,7 +111,7 @@
 <h3>Finding the Page</h3>
 
 <p>
-	We first need to find where the information is displayed, and in what format. Taking a look at the <a href="http://www.haskell.org/haskellwiki/Haskell">front web page</a>, when not logged in, you may notice that there is no hit count. However, looking at the source shows us:
+    We first need to find where the information is displayed, and in what format. Taking a look at the <a href="http://www.haskell.org/haskellwiki/Haskell">front web page</a>, when not logged in, you may notice that there is no hit count. However, looking at the source shows us:
 </p>
 <pre>
 &lt;div class="printfooter"&gt;
@@ -133,23 +124,25 @@
 title="HaskellWiki:Copyrights"&gt;a simple permissive license&lt;/a&gt;.&lt;/p&gt;
 </pre>
 <p>
-	So we see that the hit count is available, but not shown. This leads us to rule 1:
+    So we see that the hit count is available, but not shown. This leads us to rule 1:
 </p>
 <p class="rule">
-	<b>Rule 1:</b><br/>
-	Scrape from what the page returns, not what a browser renders, or what view-source gives.
+    <b>Rule 1:</b><br/>
+    Scrape from what the page returns, not what a browser renders, or what view-source gives.
 </p>
 <p>
-	Some web servers will serve different content depending on the user agent, some browsers will have scripting modify their displayed HTML, some pages will display differently depending on your cookies. Before you can start to figure out how to start scraping, first decide what the input to your program will be. There are three ways to get the page as it will appear to your program.
+    Some web servers will serve different content depending on the user agent, some browsers will have scripting modify their displayed HTML, some pages will display differently depending on your cookies. Before you can start to figure out how to start scraping, first decide what the input to your program will be. There are two ways to get the page as it will appear to your program.
 </p>
 
-<h4>Using <tt>Text.Html.Download</tt></h4>
+<h4>Using the HTTP package</h4>
 <p>
-    Tagsoup provides a module <tt>Text.Html.Download</tt>, which contains <tt>openURL</tt>.
+	We can write a simple HTTP downloader with using the <a href="http://hackage.haskell.org/package/HTTP">HTTP package</a>:
 </p>
 <pre>
-import Text.HTML.Download
+import Network.HTTP
 
+openURL x = getResponseBody =<< simpleHTTP (getRequest x)
+
 main = do src <- openURL "http://haskell.org/haskellwiki/Haskell"
           writeFile "temp.htm" src
 </pre>
@@ -165,39 +158,25 @@
 $ tagsoup grab http://haskell.org/haskellwiki/Haskell > temp.htm
 </pre>
 
-<h4>Using <tt>Network.HTTP</tt></h4>
-<p>
-    An alternative fragment of text for reading in the original web page, using the network library, can be written as:
-</p>
-<pre>
-import qualified Data.ByteString.Lazy.Char8 as BS
-import Network.HTTP (rspBody)
-import Network.HTTP.UserAgent as UA
 
-main :: IO ()
-main = do rsp <- UA.get "http://haskell.org/haskellwiki/Haskell"
-          let src = BS.unpack $ rspBody rsp
-          writeFile "temp.htm" src
-</pre>
-
 <h3>Finding the Information</h3>
 
 <p>
-	Now we examine both the fragment that contains our snippet of information, and the wider page. What does the fragment has that nothing else has? What algorithm would we use to obtain that particular element? How can we still return the element as the content changes? What if the design changes? But wait, before going any further:
+    Now we examine both the fragment that contains our snippet of information, and the wider page. What does the fragment has that nothing else has? What algorithm would we use to obtain that particular element? How can we still return the element as the content changes? What if the design changes? But wait, before going any further:
 </p>
 <p class="rule">
-	<b>Rule 2:</b><br/>
-	Do not be robust to design changes, do not even consider the possibility when writing the code.
+    <b>Rule 2:</b><br/>
+    Do not be robust to design changes, do not even consider the possibility when writing the code.
 </p>
 <p>
-	If the user changes their website, they will do so in unpredictable ways. They may move the page, they may put the information somewhere else, they may remove the information entirely. If you want something robust talk to the site owner, or buy the data from someone. If you try and think about design changes, you will complicate your design, and it still won't work. It is better to write an extraction method quickly, and happily rewrite it when things change.
+    If the user changes their website, they will do so in unpredictable ways. They may move the page, they may put the information somewhere else, they may remove the information entirely. If you want something robust talk to the site owner, or buy the data from someone. If you try and think about design changes, you will complicate your design, and it still won't work. It is better to write an extraction method quickly, and happily rewrite it when things change.
 </p>
 <p>
-	So now, lets consider the fragment from above. It is useful to find a tag which is unique just above your snippet - something with a nice "id" property, or a "class" - something which is unlikely to occur multiple times. In the above example, "printfooter" as the class seems perfect. We decide that to find the snippet, we will start at a "div" tag, with a "class" attribute with the value "printfooter".
+    So now, lets consider the fragment from above. It is useful to find a tag which is unique just above your snippet - something with a nice "id" property, or a "class" - something which is unlikely to occur multiple times. In the above example, "printfooter" as the class seems perfect. We decide that to find the snippet, we will start at a "div" tag, with a "class" attribute with the value "printfooter".
 </p>
 <pre>
 haskellHitCount = do
-    tags <- liftM parseTags $ openURL "http://haskell.org/haskellwiki/Haskell"
+    tags <- fmap parseTags $ openURL "http://haskell.org/haskellwiki/Haskell"
     let count = fromFooter $ head $ sections (~== "&lt;div class=printfooter&gt;") tags
     putStrLn $ "haskell.org has been hit " ++ show count ++ " times"
 </pre>
@@ -205,71 +184,70 @@
     Now we start writing the code! The first thing to do is open the required URL, then we parse the code into a list of <tt>Tag</tt>s. We then apply the <tt>sections</tt> function, which returns all the lists whose first element matches the query. We use the <tt>(~==)</tt> operator to construct the query - in this case asking for the "div" we mentioned earlier. This <tt>(~==)</tt> operator is very different from standard equality, it allows additional attributes to be present but does not match them. We write <tt>"&lt;div class=printfooter&gt;"</tt> as syntactic sugar for <tt>TagOpen "div" [("class","printfooter")]</tt>. If we just wanted any open tag with the given class we could have written <tt>(~== TagOpen "" [("class","printfooter")])</tt> and this would have matched. Any empty strings in the second element of the match are considered as wildcards.
 </p>
 <p>
-	Once we have a list of all matching prefixes, we take the <tt>head</tt> - assuming that only one will match. Then we apply <tt>fromFooter</tt> which needs to perform the traversal from the "printfooter" attribute onwards to the actual hit count data.
+    Once we have a list of all matching prefixes, we take the <tt>head</tt> - assuming that only one will match. Then we apply <tt>fromFooter</tt> which needs to perform the traversal from the "printfooter" attribute onwards to the actual hit count data.
 </p>
 
 <h3>Extracting the Information</h3>
 
 <p>
-	Now we have a stream starting at the right place, we generally mangle the code using standard list operators:
+    Now we have a stream starting at the right place, we generally mangle the code using standard list operators:
 </p>
 <pre>
 fromFooter x = read (filter isDigit num) :: Int
-	where
-		num = ss !! (i - 1)
-		Just i = findIndex (== "times.") ss
-		ss = words s
-		TagText s = sections (~== "&lt;p&gt;") x !! 1 !! 1
+    where num = ss !! (i - 1)
+          Just i = findIndex (== "times.") ss
+          ss = words s
+          TagText s = sections (~== "&lt;p&gt;") x !! 1 !! 1
 </pre>
 <p>
-	This code finds <tt>s</tt>, the text inside the appropriate paragraph by knowing that its the second (<tt>!! 1</tt>) paragraph, and within that paragraph, its the second tag - the actual text. We then split up the text using <tt>words</tt>, find the message that comes after hit count, and read all the digits we can find - filtering out the comma. I'm pretty sure this could be done better using regular expressions, and I invite a reader to submit improved code. This code may seem slightly messy, and indeed it is - often that is the nature of extracting information from a tag soup.
+    This code finds <tt>s</tt>, the text inside the appropriate paragraph by knowing that its the second (<tt>!! 1</tt>) paragraph, and within that paragraph, its the second tag - the actual text. We then split up the text using <tt>words</tt>, find the message that comes after hit count, and read all the digits we can find - filtering out the comma. I'm pretty sure this could be done better using regular expressions, and I invite a reader to submit improved code. This code may seem slightly messy, and indeed it is - often that is the nature of extracting information from a tag soup.
 </p>
 <p class="rule">
-	<b>Rule 3:</b><br/>
-	TagSoup is for extracting information where structure has been lost, use more structured information if it is available.
+    <b>Rule 3:</b><br/>
+    TagSoup is for extracting information where structure has been lost, use more structured information if it is available.
 </p>
 
 
 <h2>Simon's Papers</h2>
 
 <p>
-	Our next very important task is to extract a list of all Simon Peyton Jones' recent research papers off his <a href="http://research.microsoft.com/~simonpj/">home page</a>. The largest change to the previous example is that now we desire a list of papers, rather than just a single result.
+    Our next very important task is to extract a list of all Simon Peyton Jones' recent research papers off his <a href="http://research.microsoft.com/en-us/people/simonpj/">home page</a>. The largest change to the previous example is that now we desire a list of papers, rather than just a single result.
 </p>
 <p>
-	As before we first start by writing a simple program that downloads the appropriate page, and look for common patterns. This time we want to look for all patterns which occur every time a paper is mentioned, but no where else. The other difference from last time is that previous we grabbed an automatically generated piece of information - this time the information is entered in a more freeform way by a human.
+    As before we first start by writing a simple program that downloads the appropriate page, and look for common patterns. This time we want to look for all patterns which occur every time a paper is mentioned, but no where else. The other difference from last time is that previous we grabbed an automatically generated piece of information - this time the information is entered in a more freeform way by a human.
 </p>
 <p>
-	First we spot that the page helpfully has named anchors, there is a current work anchor, and after that is one for Haskell. We can extract all the information between them with a simple <tt>take</tt>/<tt>drop</tt> pair:
+    First we spot that the page helpfully has named anchors, there is a current work anchor, and after that is one for Haskell. We can extract all the information between them with a simple <tt>take</tt>/<tt>drop</tt> pair:
 </p>
 <pre>
-takeWhile (~/= "&lt;a name=haskell&gt;") $
-drop 5 $ dropWhile (~/= "&lt;a name=current&gt;") tags
+takeWhile (~/= "&lt;A name=haskell&gt;") $
+drop 5 $ dropWhile (~/= "&lt;A name=current&gt;") tags
 </pre>
 <p>
-	This code drops until you get to the "current" section, then takes until you get to the "haskell" section, ensuring we only look at the important bit of the page. Next we want to find all hyperlinks within this section:
+    This code drops until you get to the "current" section, then takes until you get to the "haskell" section, ensuring we only look at the important bit of the page. Next we want to find all hyperlinks within this section:
 </p>
 <pre>
-map f $ sections (~== "&lt;a&gt;") $ ...
+map f $ sections (~== "&lt;A&gt;") $ ...
 </pre>
 <p>
-	Remember that the function to select all tags with name "a" could have been written as <tt>(~== TagOpen "a" [])</tt>, or alternatively <tt>isTagOpenName "a"</tt>. Afterwards we map each item with an <tt>f</tt> function. This function needs to take the tags starting just after the link, and find the text inside the link.
+    Remember that the function to select all tags with name "A" could have been written as <tt>(~== TagOpen "A" [])</tt>, or alternatively <tt>isTagOpenName "A"</tt>. Afterwards we map each item with an <tt>f</tt> function. This function needs to take the tags starting just after the link, and find the text inside the link.
 </p>
 <pre>
 f = dequote . unwords . words . fromTagText . head . filter isTagText
 </pre>
 <p>
-	Here the complexity of interfacing to human written markup comes through. Some of the links are in italic, some are not - the <tt>filter</tt> drops all those that are not, until we find a pure text node. The <tt>unwords . words</tt> deletes all multiple spaces, replaces tabs and newlines with spaces and trims the front and back - a neat trick when dealing with text which has spacing at the source code but not when displayed. The final thing to take account of is that some papers are given with quotes around the name, some are not - dequote will remove the quotes if they exist.
+    Here the complexity of interfacing to human written markup comes through. Some of the links are in italic, some are not - the <tt>filter</tt> drops all those that are not, until we find a pure text node. The <tt>unwords . words</tt> deletes all multiple spaces, replaces tabs and newlines with spaces and trims the front and back - a neat trick when dealing with text which has spacing at the source code but not when displayed. The final thing to take account of is that some papers are given with quotes around the name, some are not - dequote will remove the quotes if they exist.
 </p>
 <p>
-	For completeness, we now present the entire example:
+    For completeness, we now present the entire example:
 </p>
 <pre>
 spjPapers :: IO ()
 spjPapers = do
-        tags <- liftM parseTags $ openURL "http://research.microsoft.com/~simonpj/"
-        let links = map f $ sections (~== "&lt;a&gt;") $
-                    takeWhile (~/= "&lt;a name=haskell&gt;") $
-                    drop 5 $ dropWhile (~/= "&lt;a name=current&gt;") tags
+        tags <- fmap parseTags $ openURL "http://research.microsoft.com/en-us/people/simonpj/"
+        let links = map f $ sections (~== "&lt;A&gt;") $
+                    takeWhile (~/= "&lt;A name=haskell&gt;") $
+                    drop 5 $ dropWhile (~/= "&lt;A name=current&gt;") tags
         putStr $ unlines links
     where
         f :: [Tag] -> String
@@ -283,7 +261,7 @@
 <h2>Other Examples</h2>
 
 <p>
-	Several more examples are given in the Example file, including obtaining the (short) list of papers from my site, getting the current time and a basic XML validator. All can be invoked using the <tt>tagsoup</tt> executable program. All use very much the same style as presented here - writing screen scrapers follow a standard pattern. We present the code from two for enjoyment only.
+    Several more examples are given in the Example file, including obtaining the (short) list of papers from my site, getting the current time and a basic XML validator. All can be invoked using the <tt>tagsoup</tt> executable program. All use very much the same style as presented here - writing screen scrapers follow a standard pattern. We present the code from two for enjoyment only.
 </p>
 
 <h3>My Papers</h3>
@@ -291,7 +269,7 @@
 <pre>
 ndmPapers :: IO ()
 ndmPapers = do
-        tags <- liftM parseTags $ openURL "http://www-users.cs.york.ac.uk/~ndm/downloads/"
+        tags <- fmap parseTags $ openURL "http://community.haskell.org/~ndm/downloads/"
         let papers = map f $ sections (~== "&lt;li class=paper&gt;") tags
         putStr $ unlines papers
     where
@@ -304,7 +282,7 @@
 <pre>
 currentTime :: IO ()
 currentTime = do
-        tags <- liftM parseTags $ openURL "http://www.timeanddate.com/worldclock/city.html?n=136"
+        tags <- fmap parseTags $ openURL "http://www.timeanddate.com/worldclock/city.html?n=136"
         let time = fromTagText (dropWhile (~/= "&lt;strong id=ct&gt;") tags !! 1)
         putStrLn time
 </pre>
