1{-# LANGUAGE OverloadedStrings #-}
2{-# LANGUAGE DeriveGeneric #-}
3{-# LANGUAGE LambdaCase #-}
7-- | This is a performance-oriented HTML tokenizer aim at web-crawling
8-- applications. It follows the HTML5 parsing specification quite closely,
9-- so it behaves reasonable well on ill-formed documents from the open Web.
11module Text.HTML.Parser
18 , TagName, AttrName, AttrValue
20 -- * Rendering, text canonicalization
28import Data.Char hiding (isSpace)
29import Data.List (unfoldr)
31import Control.Applicative
33import Control.Monad (guard)
36import Data.Attoparsec.Text
37import qualified Data.Attoparsec.Text.Lazy as AL
38import Data.Text (Text)
39import qualified Data.Text as T
40import qualified Data.Text.Lazy as TL
41import Data.Text.Lazy.Builder (Builder)
42import qualified Data.Text.Lazy.Builder as B
43import Prelude hiding (take, takeWhile)
45-- Section numbers refer to W3C HTML 5.2 specification.
47-- | A tag name (e.g. @body@)
50-- | An attribute name (e.g. @href@)
53-- | The value of an attribute
58 -- | An opening tag. Attribute ordering is arbitrary.
59 = TagOpen !TagName [Attr]
60 -- | A self-closing tag.
61 | TagSelfClose !TagName [Attr]
64 -- | The content between tags.
66 -- | A single character of content
68 -- | Contents of a comment.
72 deriving (Show, Ord, Eq, Generic)
74-- | This is a bit of a hack
75endOfFileToken :: Token
76endOfFileToken = ContentText ""
78-- | An attribute of a tag
79data Attr = Attr !AttrName !AttrValue
80 deriving (Show, Eq, Ord)
82instance NFData Token where
83 rnf (Comment b) = rnf $ B.toLazyText b
86-- | Parse a single 'Token'.
88token = dataState -- Start in the data state.
90-- | /§8.2.4.1/: Data state
91dataState :: Parser Token
93 content <- takeWhile (/= '<')
94 if not $ T.null content
95 then return $ ContentText content
96 else char '<' >> tagOpen
98-- | /§8.2.4.6/: Tag open state
99tagOpen :: Parser Token
101 (char '!' >> markupDeclOpen)
102 <|> (char '/' >> endTagOpen)
103 <|> (char '?' >> bogusComment mempty)
108 return $ ContentChar '<'
110-- | /§8.2.4.7/: End tag open state
111endTagOpen :: Parser Token
112endTagOpen = tagNameClose
114-- | Equivalent to @inClass "\x09\x0a\x0c "@
115isWhitespace :: Char -> Bool
116isWhitespace '\x09' = True
117isWhitespace '\x0a' = True
118isWhitespace '\x0c' = True
119isWhitespace ' ' = True
120isWhitespace _ = False
122orC :: (Char -> Bool) -> (Char -> Bool) -> Char -> Bool
123orC f g c = f c || g c
126isC :: Char -> Char -> Bool
130-- | /§8.2.4.8/: Tag name state: the open case
132-- deviation: no lower-casing, don't handle NULL characters
133tagNameOpen :: Parser Token
136 id $ (satisfy isWhitespace >> beforeAttrName tag [])
137 <|> (char '/' >> selfClosingStartTag tag [])
138 <|> (char '>' >> return (TagOpen tag []))
140-- | /§8.2.4.10/: Tag name state: close case
141tagNameClose :: Parser Token
144 char '>' >> return (TagClose tag)
146-- | /§8.2.4.10/: Tag name state: common code
148-- deviation: no lower-casing, don't handle NULL characters
149tagName' :: Parser Text
152 guard $ isAsciiUpper c || isAsciiLower c
153 takeWhile $ not . (isWhitespace `orC` isC '/' `orC` isC '<' `orC` isC '>')
155-- | /§8.2.4.40/: Self-closing start tag state
156selfClosingStartTag :: TagName -> [Attr] -> Parser Token
157selfClosingStartTag tag attrs = do
158 (char '>' >> return (TagSelfClose tag attrs))
159 <|> (endOfInput >> return endOfFileToken)
160 <|> beforeAttrName tag attrs
162-- | /§8.2.4.32/: Before attribute name state
164-- deviation: no lower-casing
165beforeAttrName :: TagName -> [Attr] -> Parser Token
166beforeAttrName tag attrs = do
167 skipWhile isWhitespace
168 id $ (char '/' >> selfClosingStartTag tag attrs)
169 <|> (char '>' >> return (TagOpen tag attrs))
170 -- <|> (char '\x00' >> attrName tag attrs) -- TODO: NULL
171 <|> attrName tag attrs
173-- | /§8.2.4.33/: Attribute name state
174attrName :: TagName -> [Attr] -> Parser Token
175attrName tag attrs = do
176 name <- takeWhile $ not . (isWhitespace `orC` isC '/' `orC` isC '=' `orC` isC '>')
177 id $ (endOfInput >> afterAttrName tag attrs name)
178 <|> (char '=' >> beforeAttrValue tag attrs name)
179 <|> try (do mc <- peekChar
181 Just c | notNameChar c -> afterAttrName tag attrs name
184 where notNameChar = isWhitespace `orC` isC '/' `orC` isC '>'
186-- | /§8.2.4.34/: After attribute name state
187afterAttrName :: TagName -> [Attr] -> AttrName -> Parser Token
188afterAttrName tag attrs name = do
189 skipWhile isWhitespace
190 id $ (char '/' >> selfClosingStartTag tag attrs)
191 <|> (char '=' >> beforeAttrValue tag attrs name)
192 <|> (char '>' >> return (TagOpen tag (Attr name T.empty : attrs)))
193 <|> (endOfInput >> return endOfFileToken)
194 <|> attrName tag (Attr name T.empty : attrs) -- not exactly sure this is right
196-- | /§8.2.4.35/: Before attribute value state
197beforeAttrValue :: TagName -> [Attr] -> AttrName -> Parser Token
198beforeAttrValue tag attrs name = do
199 skipWhile isWhitespace
200 id $ (char '"' >> attrValueDQuoted tag attrs name)
201 <|> (char '\'' >> attrValueSQuoted tag attrs name)
202 <|> (char '>' >> return (TagOpen tag (Attr name T.empty : attrs)))
203 <|> attrValueUnquoted tag attrs name
205-- | /§8.2.4.36/: Attribute value (double-quoted) state
206attrValueDQuoted :: TagName -> [Attr] -> AttrName -> Parser Token
207attrValueDQuoted tag attrs name = do
208 value <- takeWhile (/= '"')
210 afterAttrValueQuoted tag attrs name value
212-- | /§8.2.4.37/: Attribute value (single-quoted) state
213attrValueSQuoted :: TagName -> [Attr] -> AttrName -> Parser Token
214attrValueSQuoted tag attrs name = do
215 value <- takeWhile (/= '\'')
217 afterAttrValueQuoted tag attrs name value
219-- | /§8.2.4.38/: Attribute value (unquoted) state
220attrValueUnquoted :: TagName -> [Attr] -> AttrName -> Parser Token
221attrValueUnquoted tag attrs name = do
222 value <- takeTill $ isWhitespace `orC` isC '>'
223 id $ (satisfy isWhitespace >> beforeAttrName tag attrs) -- unsure: don't emit?
224 <|> (char '>' >> return (TagOpen tag (Attr name value : attrs)))
225 <|> (endOfInput >> return endOfFileToken)
227-- | /§8.2.4.39/: After attribute value (quoted) state
228afterAttrValueQuoted :: TagName -> [Attr] -> AttrName -> AttrValue -> Parser Token
229afterAttrValueQuoted tag attrs name value =
230 (satisfy isWhitespace >> beforeAttrName tag attrs')
231 <|> (char '/' >> selfClosingStartTag tag attrs')
232 <|> (char '>' >> return (TagOpen tag attrs'))
233 <|> (endOfInput >> return endOfFileToken)
234 where attrs' = Attr name value : attrs
236-- | /§8.2.4.41/: Bogus comment state
237bogusComment :: Builder -> Parser Token
238bogusComment content = do
239 (char '>' >> return (Comment content))
240 <|> (endOfInput >> return (Comment content))
241 <|> (char '\x00' >> bogusComment (content <> "\xfffd"))
242 <|> (anyChar >>= \c -> bogusComment (content <> B.singleton c))
244-- | /§8.2.4.42/: Markup declaration open state
245markupDeclOpen :: Parser Token
249 <|> bogusComment mempty
251 comment_ = char '-' >> char '-' >> commentStart
253 -- switching this to asciiCI slowed things down by a factor of two
255 guard $ T.toLower s == "doctype"
258-- | /§8.2.4.43/: Comment start state
259commentStart :: Parser Token
261 (char '-' >> commentStartDash)
262 <|> (char '>' >> return (Comment mempty))
265-- | /§8.2.4.44/: Comment start dash state
266commentStartDash :: Parser Token
268 (char '-' >> commentEnd mempty)
269 <|> (char '>' >> return (Comment mempty))
270 <|> (endOfInput >> return (Comment mempty))
271 <|> (comment (B.singleton '-'))
273-- | /§8.2.4.45/: Comment state
274comment :: Builder -> Parser Token
276 content <- B.fromText <$> takeWhile (not . (isC '-' `orC` isC '\x00' `orC` isC '<'))
277 id $ (char '<' >> commentLessThan (content0 <> content <> "<"))
278 <|> (char '-' >> commentEndDash (content0 <> content))
279 <|> (char '\x00' >> comment (content0 <> content <> B.singleton '\xfffd'))
280 <|> (endOfInput >> return (Comment $ content0 <> content))
282-- | /§8.2.46/: Comment less-than sign state
283commentLessThan :: Builder -> Parser Token
284commentLessThan content =
285 (char '!' >> commentLessThanBang (content <> "!"))
286 <|> (char '<' >> commentLessThan (content <> "<"))
289-- | /§8.2.47/: Comment less-than sign bang state
290commentLessThanBang :: Builder -> Parser Token
291commentLessThanBang content =
292 (char '-' >> commentLessThanBangDash content)
295-- | /§8.2.48/: Comment less-than sign bang dash state
296commentLessThanBangDash :: Builder -> Parser Token
297commentLessThanBangDash content =
298 (char '-' >> commentLessThanBangDashDash content)
299 <|> commentEndDash content
301-- | /§8.2.49/: Comment less-than sign bang dash dash state
302commentLessThanBangDashDash :: Builder -> Parser Token
303commentLessThanBangDashDash content =
304 (char '>' >> comment content)
305 <|> (endOfInput >> comment content)
306 <|> commentEnd content
308-- | /§8.2.4.50/: Comment end dash state
309commentEndDash :: Builder -> Parser Token
310commentEndDash content = do
311 (char '-' >> commentEnd content)
312 <|> (endOfInput >> return (Comment content))
313 <|> (comment (content <> "-"))
315-- | /§8.2.4.51/: Comment end state
316commentEnd :: Builder -> Parser Token
317commentEnd content = do
318 (char '>' >> return (Comment content))
319 <|> (char '!' >> commentEndBang content)
320 <|> (char '-' >> commentEnd (content <> "-"))
321 <|> (endOfInput >> return (Comment content))
322 <|> (comment (content <> "--"))
324-- | /§8.2.4.52/: Comment end bang state
325commentEndBang :: Builder -> Parser Token
326commentEndBang content = do
327 (char '-' >> commentEndDash (content <> "--!"))
328 <|> (char '>' >> return (Comment content))
329 <|> (endOfInput >> return (Comment content))
330 <|> (comment (content <> "--!"))
332-- | /§8.2.4.53/: DOCTYPE state
334doctype :: Parser Token
336 content <- takeTill (=='>')
338 return $ Doctype content
340-- | Parse a lazy list of tokens from strict 'Text'.
341parseTokens :: Text -> [Token]
342parseTokens = unfoldr f
344 f :: Text -> Maybe (Token, Text)
348 case parse token t of
349 Done rest tok -> Just (tok, rest)
352 Done rest tok -> Just (tok, rest)
356-- | Parse a lazy list of tokens from lazy 'TL.Text'.
357parseTokensLazy :: TL.Text -> [Token]
358parseTokensLazy = unfoldr f
360 f :: TL.Text -> Maybe (Token, TL.Text)
362 | TL.null t = Nothing
364 case AL.parse token t of
365 AL.Done rest tok -> Just (tok, rest)
368-- | See 'renderToken'.
369renderTokens :: [Token] -> TL.Text
370renderTokens = mconcat . fmap renderToken
372-- | (Somewhat) canonical string representation of 'Token'.
373renderToken :: Token -> TL.Text
374renderToken = TL.fromStrict . mconcat . \case
375 (TagOpen n []) -> ["<", n, ">"]
376 (TagOpen n attrs) -> ["<", n, " ", renderAttrs attrs, ">"]
377 (TagSelfClose n attrs) -> ["<", n, " ", renderAttrs attrs, " />"]
378 (TagClose n) -> ["</", n, ">"]
379 (ContentChar c) -> [T.singleton c]
380 (ContentText t) -> [t]
381 (Comment builder) -> ["<!--", TL.toStrict $ B.toLazyText builder, "-->"]
382 (Doctype t) -> ["<!DOCTYPE", t, ">"]
384-- | See 'renderAttr'.
385renderAttrs :: [Attr] -> Text
386renderAttrs = T.unwords . fmap renderAttr . reverse
388-- | Does not escape quotation in attribute values!
389renderAttr :: Attr -> Text
390renderAttr (Attr k v) = mconcat [k, "=\"", v, "\""]
392-- | Meld neighoring 'ContentChar' and 'ContentText' constructors together and drops empty text
394canonicalizeTokens :: [Token] -> [Token]
395canonicalizeTokens = filter (/= ContentText "") . meldTextTokens
397meldTextTokens :: [Token] -> [Token]
398meldTextTokens = concatTexts . fmap charToText
400 charToText (ContentChar c) = ContentText (T.singleton c)
404 (ContentText t : ContentText t' : ts) -> concatTexts $ ContentText (t <> t') : ts
405 (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