1{-# LANGUAGE OverloadedStrings #-}
2{-# LANGUAGE DeriveGeneric #-}
3{-# LANGUAGE LambdaCase #-}
5-- | This is a performance-oriented HTML tokenizer aim at web-crawling
6-- applications. It follows the HTML5 parsing specification quite closely,
7-- so it behaves reasonable well on ill-formed documents from the open Web.
16 , TagName, AttrName, AttrValue
18 -- * Rendering, text canonicalization
26import Data.Char hiding (isSpace)
27import Data.List (unfoldr)
29import Control.Applicative
31import Control.Monad (guard)
34import Data.Attoparsec.Text
35import qualified Data.Attoparsec.Text.Lazy as AL
36import Data.Text (Text)
37import qualified Data.Text as T
38import qualified Data.Text.Lazy as TL
39import Data.Text.Lazy.Builder (Builder)
40import qualified Data.Text.Lazy.Builder as B
41import Prelude hiding (take, takeWhile)
43-- | A tag name (e.g. @body@)
46-- | An attribute name (e.g. @href@)
49-- | The value of an attribute
54 -- | An opening tag. Attribute ordering is arbitrary.
55 = TagOpen !TagName [Attr]
58 -- | The content between tags.
60 -- | A single character of content
62 -- | Contents of a comment.
66 deriving (Show, Ord, Eq, Generic)
68-- | An attribute of a tag
69data Attr = Attr !AttrName !AttrValue
70 deriving (Show, Eq, Ord)
72instance NFData Token where
73 rnf (Comment b) = rnf $ B.toLazyText b
76-- | Parse a single 'Token'.
78token = dataState -- Start in the data state.
80-- | /§8.2.4.1/: Data state
81dataState :: Parser Token
83 content <- takeWhile (/= '<')
84 if not $ T.null content
85 then return $ ContentText content
86 else char '<' >> tagOpen
88-- | /§8.2.4.3/: Tag open state
89tagOpen :: Parser Token
91 (char '!' >> markupDeclOpen)
92 <|> (char '/' >> endTagOpen)
93 <|> (char '?' >> bogusComment)
98 return $ ContentChar '<'
100-- | /§8.2.4.9/: End tag open state
101endTagOpen :: Parser Token
102endTagOpen = tagNameClose
104-- | /§8.2.4.10/: Tag name state: the open case
106-- deviation: no lower-casing
107tagNameOpen :: Parser Token
110 id $ (satisfy (inClass "\x09\x0a\x0c ") >> beforeAttrName tag [])
111 <|> (char '/' >> selfClosingStartTag tag [])
112 <|> (char '>' >> return (TagOpen tag []))
114-- | /§8.2.4.10/: Tag name state: close case
115tagNameClose :: Parser Token
118 char '>' >> return (TagClose tag)
120-- | /§8.2.4.10/: Tag name state: common code
122-- deviation: no lower-casing
123tagName' :: Parser Text
126 guard $ isAsciiUpper c || isAsciiLower c
127 takeWhile $ notInClass "\x09\x0a\x0c /<>"
129-- | /§8.2.4.43/: Self-closing start tag state
130selfClosingStartTag :: TagName -> [Attr] -> Parser Token
131selfClosingStartTag tag attrs = do
132 (char '>' >> return (TagOpen tag attrs))
133 <|> beforeAttrName tag attrs
135-- | /§8.2.4.34/: Before attribute name state
137-- deviation: no lower-casing
138beforeAttrName :: TagName -> [Attr] -> Parser Token
139beforeAttrName tag attrs = do
140 skipWhile $ inClass "\x09\x0a\x0c "
141 id $ (char '/' >> selfClosingStartTag tag attrs)
142 <|> (char '>' >> return (TagOpen tag attrs))
143 -- <|> (char '\x00' >> attrName tag attrs) -- TODO: NULL
144 <|> attrName tag attrs
146-- | /§8.2.4.35/: Attribute name state
147attrName :: TagName -> [Attr] -> Parser Token
148attrName tag attrs = do
149 name <- takeWhile $ notInClass "\x09\x0a\x0c /=<>\x00"
150 id $ (satisfy (inClass "\x09\x0a\x0c ") >> afterAttrName tag attrs name)
151 <|> (char '/' >> selfClosingStartTag tag attrs)
152 <|> (char '=' >> beforeAttrValue tag attrs name)
153 <|> (char '>' >> return (TagOpen tag (Attr name T.empty : attrs)))
156-- | /§8.2.4.36/: After attribute name state
157afterAttrName :: TagName -> [Attr] -> AttrName -> Parser Token
158afterAttrName tag attrs name = do
159 skipWhile $ inClass "\x09\x0a\x0c "
160 id $ (char '/' >> selfClosingStartTag tag attrs)
161 <|> (char '=' >> beforeAttrValue tag attrs name)
162 <|> (char '>' >> return (TagOpen tag (Attr name T.empty : attrs)))
163 <|> attrName tag (Attr name T.empty : attrs) -- not exactly sure this is right
165-- | /§8.2.4.37/: Before attribute value state
166beforeAttrValue :: TagName -> [Attr] -> AttrName -> Parser Token
167beforeAttrValue tag attrs name = do
168 skipWhile $ inClass "\x09\x0a\x0c "
169 id $ (char '"' >> attrValueDQuoted tag attrs name)
170 <|> (char '\'' >> attrValueSQuoted tag attrs name)
171 <|> (char '>' >> return (TagOpen tag (Attr name T.empty : attrs)))
172 <|> attrValueUnquoted tag attrs name
174-- | /§8.2.4.38/: Attribute value (double-quoted) state
175attrValueDQuoted :: TagName -> [Attr] -> AttrName -> Parser Token
176attrValueDQuoted tag attrs name = do
177 value <- takeWhile (/= '"')
179 afterAttrValueQuoted tag attrs name value
181-- | /§8.2.4.39/: Attribute value (single-quoted) state
182attrValueSQuoted :: TagName -> [Attr] -> AttrName -> Parser Token
183attrValueSQuoted tag attrs name = do
184 value <- takeWhile (/= '\'')
186 afterAttrValueQuoted tag attrs name value
188-- | /§8.2.4.40/: Attribute value (unquoted) state
189attrValueUnquoted :: TagName -> [Attr] -> AttrName -> Parser Token
190attrValueUnquoted tag attrs name = do
191 value <- takeTill (inClass "\x09\x0a\x0c >")
192 id $ (satisfy (inClass "\x09\x0a\x0c ") >> beforeAttrName tag attrs) -- unsure: don't emit?
193 <|> (char '>' >> return (TagOpen tag (Attr name value : attrs)))
195-- | /§8.2.4.42/: After attribute value (quoted) state
196afterAttrValueQuoted :: TagName -> [Attr] -> AttrName -> AttrValue -> Parser Token
197afterAttrValueQuoted tag attrs name value =
198 (satisfy (inClass "\x09\x0a\x0c ") >> beforeAttrName tag attrs')
199 <|> (char '/' >> selfClosingStartTag tag attrs')
200 <|> (char '>' >> return (TagOpen tag attrs'))
201 where attrs' = Attr name value : attrs
203-- | /§8.2.4.45/: Markup declaration open state
204markupDeclOpen :: Parser Token
208 -- TODO: Fix the rest
210 comment = string "--" >> commentStart
212 -- switching this to asciiCI slowed things down by a factor of two
214 guard $ T.toLower s == "doctype"
217-- | /§8.2.4.46/: Comment start state
218commentStart :: Parser Token
220 (char '-' >> commentStartDash)
221 <|> (char '>' >> return (Comment mempty))
224-- | /§8.2.4.47/: Comment start dash state
225commentStartDash :: Parser Token
227 (char '-' >> commentEnd mempty)
228 <|> (char '>' >> return (Comment mempty))
230 comment (B.singleton '-' <> B.singleton c) )
232-- | /§8.2.4.48/: Comment state
233comment :: Builder -> Parser Token
235 content <- B.fromText <$> takeWhile (notInClass "-")
236 id $ (char '-' >> commentEndDash (content0 <> content))
237 <|> (char '\x00' >> comment (content0 <> content <> B.singleton '\xfffd'))
239-- | /§8.2.4.49/: Comment end dash state
240commentEndDash :: Builder -> Parser Token
241commentEndDash content = do
242 (char '-' >> commentEnd content)
243 <|> (char '\x00' >> comment (content <> "-\xfffd"))
244 <|> (anyChar >>= \c -> comment (content <> "-" <> B.singleton c))
246-- | /§8.2.4.50/: Comment end state
247commentEnd :: Builder -> Parser Token
248commentEnd content = do
249 (char '>' >> return (Comment content))
250 <|> (char '\x00' >> comment (content <> "-\xfffd"))
251 -- <|> () TODO: other cases
252 <|> (anyChar >>= \c -> comment (content <> "-" <> B.singleton c))
254-- | /§8.2.4.52/: DOCTYPE state
256doctype :: Parser Token
258 content <- takeTill (=='>')
260 return $ Doctype content
262-- | /§8.2.4.44/: Bogus comment state
263bogusComment :: Parser Token
264bogusComment = fail "Bogus comment"
266-- | Parse a lazy list of tokens from strict 'Text'.
267parseTokens :: Text -> [Token]
268parseTokens = unfoldr f
270 f :: Text -> Maybe (Token, Text)
274 case parse token t of
275 Done rest tok -> Just (tok, rest)
278 Done rest tok -> Just (tok, rest)
282-- | Parse a lazy list of tokens from lazy 'TL.Text'.
283parseTokensLazy :: TL.Text -> [Token]
284parseTokensLazy = unfoldr f
286 f :: TL.Text -> Maybe (Token, TL.Text)
288 | TL.null t = Nothing
290 case AL.parse token t of
291 AL.Done rest tok -> Just (tok, rest)
294-- | See 'renderToken'.
295renderTokens :: [Token] -> TL.Text
296renderTokens = mconcat . fmap renderToken
298-- | (Somewhat) canonical string representation of 'Token'.
299renderToken :: Token -> TL.Text
300renderToken = TL.fromStrict . mconcat . \case
301 (TagOpen n []) -> ["<", n, ">"]
302 (TagOpen n attrs) -> ["<", n, " ", renderAttrs attrs, ">"]
303 (TagClose n) -> ["</", n, ">"]
304 (ContentChar c) -> [T.singleton c]
305 (ContentText t) -> [t]
306 (Comment builder) -> ["<!--", TL.toStrict $ B.toLazyText builder, "-->"]
307 (Doctype t) -> ["<!DOCTYPE", t, ">"]
309-- | See 'renderAttr'.
310renderAttrs :: [Attr] -> Text
311renderAttrs = T.unwords . fmap renderAttr . reverse
313-- | Does not escape quotation in attribute values!
314renderAttr :: Attr -> Text
315renderAttr (Attr k v) = mconcat [k, "=\"", v, "\""]
317-- | Meld neighoring 'ContentChar' and 'ContentText' constructors together and drops empty text
319canonicalizeTokens :: [Token] -> [Token]
320canonicalizeTokens = filter (/= ContentText "") . meldTextTokens
322meldTextTokens :: [Token] -> [Token]
323meldTextTokens = concatTexts . fmap charToText
325 charToText (ContentChar c) = ContentText (T.singleton c)
329 (ContentText t : ContentText t' : ts) -> concatTexts $ ContentText (t <> t') : ts
330 (t : ts) -> t : concatTexts ts
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list