1{-# LANGUAGE OverloadedStrings #-}
2{-# LANGUAGE DeriveGeneric #-}
4-- | This is a performance-oriented HTML tokenizer aim at web-crawling
5-- applications. It follows the HTML5 parsing specification quite closely,
6-- so it behaves reasonable well on ill-formed documents from the open Web.
15 , TagName, AttrName, AttrValue
19import Data.Char hiding (isSpace)
20import Data.List (unfoldr)
22import Control.Applicative
24import Control.Monad (guard)
27import Data.Attoparsec.Text
28import qualified Data.Attoparsec.Text.Lazy as AL
29import Data.Text (Text)
30import qualified Data.Text as T
31import qualified Data.Text.Lazy as TL
32import Data.Text.Lazy.Builder (Builder)
33import qualified Data.Text.Lazy.Builder as B
34import Prelude hiding (take, takeWhile)
36-- | A tag name (e.g. @body@)
39-- | An attribute name (e.g. @href@)
42-- | The value of an attribute
47 -- | An opening tag. Attribute ordering is arbitrary.
48 = TagOpen !TagName [Attr]
51 -- | The content between tags.
53 -- | A single character of content
55 -- | Contents of a comment.
59 deriving (Show, Ord, Eq, Generic)
61-- | An attribute of a tag
62data Attr = Attr !AttrName !AttrValue
63 deriving (Show, Eq, Ord)
65instance NFData Token where
66 rnf (Comment b) = rnf $ B.toLazyText b
69-- | Parse a single 'Token'.
71token = dataState -- Start in the data state.
73-- | /§8.2.4.1/: Data state
74dataState :: Parser Token
76 content <- takeWhile (/= '<')
77 if not $ T.null content
78 then return $ ContentText content
79 else char '<' >> tagOpen
81-- | /§8.2.4.3/: Tag open state
82tagOpen :: Parser Token
84 (char '!' >> markupDeclOpen)
85 <|> (char '/' >> endTagOpen)
86 <|> (char '?' >> bogusComment)
92 guard $ isAsciiUpper c || isAsciiLower c
96 return $ ContentChar '<'
98-- | /§8.2.4.9/: End tag open state
99-- TODO: This isn't right
100endTagOpen :: Parser Token
102 name <- takeWhile $ \c -> isAsciiUpper c || isAsciiLower c
104 return $ TagClose name
106-- | /§8.2.4.10/: Tag name state
108-- deviation: no lower-casing
109tagName :: Parser Token
111 tag <- takeWhile $ notInClass "\x09\x0a\x0c />"
112 id $ (satisfy (inClass "\x09\x0a\x0c ") >> beforeAttrName tag [])
113 <|> (char '/' >> selfClosingStartTag tag [])
114 <|> (char '>' >> return (TagOpen tag []))
116-- | /§8.2.4.43/: Self-closing start tag state
117selfClosingStartTag :: TagName -> [Attr] -> Parser Token
118selfClosingStartTag tag attrs = do
119 (char '>' >> return (TagOpen tag attrs))
120 <|> beforeAttrName tag attrs
122-- | /§8.2.4.34/: Before attribute name state
124-- deviation: no lower-casing
125beforeAttrName :: TagName -> [Attr] -> Parser Token
126beforeAttrName tag attrs = do
127 skipWhile $ inClass "\x09\x0a\x0c "
128 id $ (char '/' >> selfClosingStartTag tag attrs)
129 <|> (char '>' >> return (TagOpen tag attrs))
130 -- <|> (char '\x00' >> attrName tag attrs) -- TODO: NULL
131 <|> attrName tag attrs
133-- | /§8.2.4.35/: Attribute name state
134attrName :: TagName -> [Attr] -> Parser Token
135attrName tag attrs = do
136 name <- takeWhile $ notInClass "\x09\x0a\x0c /=>\x00"
137 id $ (satisfy (inClass "\x09\x0a\x0c ") >> afterAttrName tag attrs name)
138 <|> (char '/' >> selfClosingStartTag tag attrs)
139 <|> (char '=' >> beforeAttrValue tag attrs name)
140 <|> (char '>' >> return (TagOpen tag (Attr name T.empty : attrs)))
143-- | /§8.2.4.36/: After attribute name state
144afterAttrName :: TagName -> [Attr] -> AttrName -> Parser Token
145afterAttrName tag attrs name = do
146 skipWhile $ inClass "\x09\x0a\x0c "
147 id $ (char '/' >> selfClosingStartTag tag attrs)
148 <|> (char '=' >> beforeAttrValue tag attrs name)
149 <|> (char '>' >> return (TagOpen tag (Attr name T.empty : attrs)))
150 <|> attrName tag (Attr name T.empty : attrs) -- not exactly sure this is right
152-- | /§8.2.4.37/: Before attribute value state
153beforeAttrValue :: TagName -> [Attr] -> AttrName -> Parser Token
154beforeAttrValue tag attrs name = do
155 skipWhile $ inClass "\x09\x0a\x0c "
156 id $ (char '"' >> attrValueDQuoted tag attrs name)
157 <|> (char '\'' >> attrValueSQuoted tag attrs name)
158 <|> (char '>' >> return (TagOpen tag (Attr name T.empty : attrs)))
159 <|> attrValueUnquoted tag attrs name
161-- | /§8.2.4.38/: Attribute value (double-quoted) state
162attrValueDQuoted :: TagName -> [Attr] -> AttrName -> Parser Token
163attrValueDQuoted tag attrs name = do
164 value <- takeWhile (/= '"')
166 afterAttrValueQuoted tag attrs name value
168-- | /§8.2.4.39/: Attribute value (single-quoted) state
169attrValueSQuoted :: TagName -> [Attr] -> AttrName -> Parser Token
170attrValueSQuoted tag attrs name = do
171 value <- takeWhile (/= '\'')
173 afterAttrValueQuoted tag attrs name value
175-- | /§8.2.4.40/: Attribute value (unquoted) state
176attrValueUnquoted :: TagName -> [Attr] -> AttrName -> Parser Token
177attrValueUnquoted tag attrs name = do
178 value <- takeTill (inClass "\x09\x0a\x0c >")
179 id $ (satisfy (inClass "\x09\x0a\x0c ") >> beforeAttrName tag attrs) -- unsure: don't emit?
180 <|> (char '>' >> return (TagOpen tag (Attr name value : attrs)))
182-- | /§8.2.4.42/: After attribute value (quoted) state
183afterAttrValueQuoted :: TagName -> [Attr] -> AttrName -> AttrValue -> Parser Token
184afterAttrValueQuoted tag attrs name value =
185 (satisfy (inClass "\x09\x0a\x0c ") >> beforeAttrName tag attrs')
186 <|> (char '/' >> selfClosingStartTag tag attrs')
187 <|> (char '>' >> return (TagOpen tag attrs'))
188 where attrs' = Attr name value : attrs
190-- | /§8.2.4.45/: Markup declaration open state
191markupDeclOpen :: Parser Token
195 -- TODO: Fix the rest
197 comment = string "--" >> commentStart
199 -- switching this to asciiCI slowed things down by a factor of two
201 guard $ T.toLower s == "doctype"
204-- | /§8.2.4.46/: Comment start state
205commentStart :: Parser Token
207 (char '-' >> commentStartDash)
208 <|> (char '>' >> return (Comment mempty))
210-- | /§8.2.4.47/: Comment start dash state
211commentStartDash :: Parser Token
213 (char '-' >> commentEnd mempty)
214 <|> (char '>' >> return (Comment mempty))
216 comment (B.singleton '-' <> B.singleton c) )
218-- | /§8.2.4.48/: Comment state
219comment :: Builder -> Parser Token
221 content <- B.fromText <$> takeWhile (notInClass "-")
222 id $ (char '-' >> commentEndDash (content0 <> content))
223 <|> (char '\x00' >> comment (content0 <> content <> B.singleton '\xfffd'))
225-- | /§8.2.4.49/: Comment end dash state
226commentEndDash :: Builder -> Parser Token
227commentEndDash content = do
228 (char '-' >> commentEnd content)
229 <|> (char '\x00' >> comment (content <> "-\xfffd"))
230 <|> (anyChar >>= \c -> comment (content <> "-" <> B.singleton c))
232-- | /§8.2.4.50/: Comment end state
233commentEnd :: Builder -> Parser Token
234commentEnd content = do
235 (char '>' >> return (Comment content))
236 <|> (char '\x00' >> comment (content <> "-\xfffd"))
237 -- <|> () TODO: other cases
238 <|> (anyChar >>= \c -> comment (content <> "-" <> B.singleton c))
240-- | /§8.2.4.52/: DOCTYPE state
242doctype :: Parser Token
244 content <- takeTill (=='>')
246 return $ Doctype content
248-- | /§8.2.4.44/: Bogus comment state
249bogusComment :: Parser Token
250bogusComment = fail "Bogus comment"
252-- | Parse a lazy list of tokens from strict 'Text'.
253parseTokens :: Text -> [Token]
254parseTokens = unfoldr f
256 f :: Text -> Maybe (Token, Text)
260 case parse token t of
261 Done rest tok -> Just (tok, rest)
264-- | Parse a lazy list of tokens from lazy 'TL.Text'.
265parseTokensLazy :: TL.Text -> [Token]
266parseTokensLazy = unfoldr f
268 f :: TL.Text -> Maybe (Token, TL.Text)
270 | TL.null t = Nothing
272 case AL.parse token t of
273 AL.Done rest tok -> Just (tok, rest)
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