@@ -1,5 +1,6 @@
11{-# LANGUAGE OverloadedStrings #-}
22{-# LANGUAGE DeriveGeneric #-}
3+{-# LANGUAGE LambdaCase #-}
34
45-- | This is a performance-oriented HTML tokenizer aim at web-crawling
56-- applications. It follows the HTML5 parsing specification quite closely,
@@ -14,6 +15,12 @@ module Text.HTML.Parser
1415 , Token(..)
1516 , TagName, AttrName, AttrValue
1617 , Attr(..)
18+ -- * Rendering, text canonicalization
19+ , renderTokens
20+ , renderToken
21+ , renderAttrs
22+ , renderAttr
23+ , canonicalizeTokens
1724 ) where
1825
1926import Data.Char hiding (isSpace)
@@ -277,3 +284,42 @@ parseTokensLazy = unfoldr f
277284 case AL.parse token t of
278285 AL.Done rest tok -> Just (tok, rest)
279286 _ -> Nothing
287+
288+-- | See 'renderToken'.
289+renderTokens :: [Token] -> TL.Text
290+renderTokens = mconcat . fmap renderToken
291+
292+-- | (Somewhat) canonical string representation of 'Token'.
293+renderToken :: Token -> TL.Text
294+renderToken = TL.fromStrict . mconcat . \case
295+ (TagOpen n []) -> ["<", n, ">"]
296+ (TagOpen n attrs) -> ["<", n, " ", renderAttrs attrs, ">"]
297+ (TagClose n) -> ["</", n, ">"]
298+ (ContentChar c) -> [T.singleton c]
299+ (ContentText t) -> [t]
300+ (Comment builder) -> ["<!--", TL.toStrict $ B.toLazyText builder, "-->"]
301+ (Doctype t) -> ["<!DOCTYPE", t, ">"]
302+
303+-- | See 'renderAttr'.
304+renderAttrs :: [Attr] -> Text
305+renderAttrs = T.unwords . fmap renderAttr . reverse
306+
307+-- | Does not escape quotation in attribute values!
308+renderAttr :: Attr -> Text
309+renderAttr (Attr k v) = mconcat [k, "=\"", v, "\""]
310+
311+-- | Meld neighoring 'ContentChar' and 'ContentText' constructors together and drops empty text
312+-- elements.
313+canonicalizeTokens :: [Token] -> [Token]
314+canonicalizeTokens = filter (/= ContentText "") . meldTextTokens
315+
316+meldTextTokens :: [Token] -> [Token]
317+meldTextTokens = concatTexts . fmap charToText
318+ where
319+ charToText (ContentChar c) = ContentText (T.singleton c)
320+ charToText t = t
321+
322+ concatTexts = \case
323+ (ContentText t : ContentText t' : ts) -> concatTexts $ ContentText (t <> t') : ts
324+ (t : ts) -> t : concatTexts ts
325+ [] -> []