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)
99 guard $ isAsciiUpper c || isAsciiLower c
103 return $ ContentChar '<'
105-- | /§8.2.4.9/: End tag open state
106-- TODO: This isn't right
107endTagOpen :: Parser Token
109 name <- takeWhile $ \c -> isAsciiUpper c || isAsciiLower c
111 return $ TagClose name
113-- | /§8.2.4.10/: Tag name state
115-- deviation: no lower-casing
116tagName :: Parser Token
118 tag <- takeWhile $ notInClass "\x09\x0a\x0c />"
119 id $ (satisfy (inClass "\x09\x0a\x0c ") >> beforeAttrName tag [])
120 <|> (char '/' >> selfClosingStartTag tag [])
121 <|> (char '>' >> return (TagOpen tag []))
123-- | /§8.2.4.43/: Self-closing start tag state
124selfClosingStartTag :: TagName -> [Attr] -> Parser Token
125selfClosingStartTag tag attrs = do
126 (char '>' >> return (TagOpen tag attrs))
127 <|> beforeAttrName tag attrs
129-- | /§8.2.4.34/: Before attribute name state
131-- deviation: no lower-casing
132beforeAttrName :: TagName -> [Attr] -> Parser Token
133beforeAttrName tag attrs = do
134 skipWhile $ inClass "\x09\x0a\x0c "
135 id $ (char '/' >> selfClosingStartTag tag attrs)
136 <|> (char '>' >> return (TagOpen tag attrs))
137 -- <|> (char '\x00' >> attrName tag attrs) -- TODO: NULL
138 <|> attrName tag attrs
140-- | /§8.2.4.35/: Attribute name state
141attrName :: TagName -> [Attr] -> Parser Token
142attrName tag attrs = do
143 name <- takeWhile $ notInClass "\x09\x0a\x0c /=>\x00"
144 id $ (satisfy (inClass "\x09\x0a\x0c ") >> afterAttrName tag attrs name)
145 <|> (char '/' >> selfClosingStartTag tag attrs)
146 <|> (char '=' >> beforeAttrValue tag attrs name)
147 <|> (char '>' >> return (TagOpen tag (Attr name T.empty : attrs)))
150-- | /§8.2.4.36/: After attribute name state
151afterAttrName :: TagName -> [Attr] -> AttrName -> Parser Token
152afterAttrName tag attrs name = do
153 skipWhile $ inClass "\x09\x0a\x0c "
154 id $ (char '/' >> selfClosingStartTag tag attrs)
155 <|> (char '=' >> beforeAttrValue tag attrs name)
156 <|> (char '>' >> return (TagOpen tag (Attr name T.empty : attrs)))
157 <|> attrName tag (Attr name T.empty : attrs) -- not exactly sure this is right
159-- | /§8.2.4.37/: Before attribute value state
160beforeAttrValue :: TagName -> [Attr] -> AttrName -> Parser Token
161beforeAttrValue tag attrs name = do
162 skipWhile $ inClass "\x09\x0a\x0c "
163 id $ (char '"' >> attrValueDQuoted tag attrs name)
164 <|> (char '\'' >> attrValueSQuoted tag attrs name)
165 <|> (char '>' >> return (TagOpen tag (Attr name T.empty : attrs)))
166 <|> attrValueUnquoted tag attrs name
168-- | /§8.2.4.38/: Attribute value (double-quoted) state
169attrValueDQuoted :: TagName -> [Attr] -> AttrName -> Parser Token
170attrValueDQuoted tag attrs name = do
171 value <- takeWhile (/= '"')
173 afterAttrValueQuoted tag attrs name value
175-- | /§8.2.4.39/: Attribute value (single-quoted) state
176attrValueSQuoted :: TagName -> [Attr] -> AttrName -> Parser Token
177attrValueSQuoted tag attrs name = do
178 value <- takeWhile (/= '\'')
180 afterAttrValueQuoted tag attrs name value
182-- | /§8.2.4.40/: Attribute value (unquoted) state
183attrValueUnquoted :: TagName -> [Attr] -> AttrName -> Parser Token
184attrValueUnquoted tag attrs name = do
185 value <- takeTill (inClass "\x09\x0a\x0c >")
186 id $ (satisfy (inClass "\x09\x0a\x0c ") >> beforeAttrName tag attrs) -- unsure: don't emit?
187 <|> (char '>' >> return (TagOpen tag (Attr name value : attrs)))
189-- | /§8.2.4.42/: After attribute value (quoted) state
190afterAttrValueQuoted :: TagName -> [Attr] -> AttrName -> AttrValue -> Parser Token
191afterAttrValueQuoted tag attrs name value =
192 (satisfy (inClass "\x09\x0a\x0c ") >> beforeAttrName tag attrs')
193 <|> (char '/' >> selfClosingStartTag tag attrs')
194 <|> (char '>' >> return (TagOpen tag attrs'))
195 where attrs' = Attr name value : attrs
197-- | /§8.2.4.45/: Markup declaration open state
198markupDeclOpen :: Parser Token
202 -- TODO: Fix the rest
204 comment = string "--" >> commentStart
206 -- switching this to asciiCI slowed things down by a factor of two
208 guard $ T.toLower s == "doctype"
211-- | /§8.2.4.46/: Comment start state
212commentStart :: Parser Token
214 (char '-' >> commentStartDash)
215 <|> (char '>' >> return (Comment mempty))
218-- | /§8.2.4.47/: Comment start dash state
219commentStartDash :: Parser Token
221 (char '-' >> commentEnd mempty)
222 <|> (char '>' >> return (Comment mempty))
224 comment (B.singleton '-' <> B.singleton c) )
226-- | /§8.2.4.48/: Comment state
227comment :: Builder -> Parser Token
229 content <- B.fromText <$> takeWhile (notInClass "-")
230 id $ (char '-' >> commentEndDash (content0 <> content))
231 <|> (char '\x00' >> comment (content0 <> content <> B.singleton '\xfffd'))
233-- | /§8.2.4.49/: Comment end dash state
234commentEndDash :: Builder -> Parser Token
235commentEndDash content = do
236 (char '-' >> commentEnd content)
237 <|> (char '\x00' >> comment (content <> "-\xfffd"))
238 <|> (anyChar >>= \c -> comment (content <> "-" <> B.singleton c))
240-- | /§8.2.4.50/: Comment end state
241commentEnd :: Builder -> Parser Token
242commentEnd content = do
243 (char '>' >> return (Comment content))
244 <|> (char '\x00' >> comment (content <> "-\xfffd"))
245 -- <|> () TODO: other cases
246 <|> (anyChar >>= \c -> comment (content <> "-" <> B.singleton c))
248-- | /§8.2.4.52/: DOCTYPE state
250doctype :: Parser Token
252 content <- takeTill (=='>')
254 return $ Doctype content
256-- | /§8.2.4.44/: Bogus comment state
257bogusComment :: Parser Token
258bogusComment = fail "Bogus comment"
260-- | Parse a lazy list of tokens from strict 'Text'.
261parseTokens :: Text -> [Token]
262parseTokens = unfoldr f
264 f :: Text -> Maybe (Token, Text)
268 case parse token t of
269 Done rest tok -> Just (tok, rest)
272 Done rest tok -> Just (tok, rest)
276-- | Parse a lazy list of tokens from lazy 'TL.Text'.
277parseTokensLazy :: TL.Text -> [Token]
278parseTokensLazy = unfoldr f
280 f :: TL.Text -> Maybe (Token, TL.Text)
282 | TL.null t = Nothing
284 case AL.parse token t of
285 AL.Done rest tok -> Just (tok, rest)
288-- | See 'renderToken'.
289renderTokens :: [Token] -> TL.Text
290renderTokens = mconcat . fmap renderToken
292-- | (Somewhat) canonical string representation of 'Token'.
293renderToken :: Token -> TL.Text
294renderToken = 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, ">"]
303-- | See 'renderAttr'.
304renderAttrs :: [Attr] -> Text
305renderAttrs = T.unwords . fmap renderAttr . reverse
307-- | Does not escape quotation in attribute values!
308renderAttr :: Attr -> Text
309renderAttr (Attr k v) = mconcat [k, "=\"", v, "\""]
311-- | Meld neighoring 'ContentChar' and 'ContentText' constructors together and drops empty text
313canonicalizeTokens :: [Token] -> [Token]
314canonicalizeTokens = filter (/= ContentText "") . meldTextTokens
316meldTextTokens :: [Token] -> [Token]
317meldTextTokens = concatTexts . fmap charToText
319 charToText (ContentChar c) = ContentText (T.singleton c)
323 (ContentText t : ContentText t' : ts) -> concatTexts $ ContentText (t <> t') : ts
324 (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