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-- Section numbers refer to W3C HTML 5.2 specification.
45-- | A tag name (e.g. @body@)
48-- | An attribute name (e.g. @href@)
51-- | The value of an attribute
56 -- | An opening tag. Attribute ordering is arbitrary.
57 = TagOpen !TagName [Attr]
58 -- | A self-closing tag.
59 | TagSelfClose !TagName [Attr]
62 -- | The content between tags.
64 -- | A single character of content
66 -- | Contents of a comment.
70 deriving (Show, Ord, Eq, Generic)
72-- | This is a bit of a hack
73endOfFileToken :: Token
74endOfFileToken = ContentText ""
76-- | An attribute of a tag
77data Attr = Attr !AttrName !AttrValue
78 deriving (Show, Eq, Ord)
80instance NFData Token where
81 rnf (Comment b) = rnf $ B.toLazyText b
84-- | Parse a single 'Token'.
86token = dataState -- Start in the data state.
88-- | /§8.2.4.1/: Data state
89dataState :: Parser Token
91 content <- takeWhile (/= '<')
92 if not $ T.null content
93 then return $ ContentText content
94 else char '<' >> tagOpen
96-- | /§8.2.4.6/: Tag open state
97tagOpen :: Parser Token
99 (char '!' >> markupDeclOpen)
100 <|> (char '/' >> endTagOpen)
101 <|> (char '?' >> bogusComment mempty)
106 return $ ContentChar '<'
108-- | /§8.2.4.7/: End tag open state
109endTagOpen :: Parser Token
110endTagOpen = tagNameClose
112-- | /§8.2.4.8/: Tag name state: the open case
114-- deviation: no lower-casing, don't handle NULL characters
115tagNameOpen :: Parser Token
118 id $ (satisfy (inClass "\x09\x0a\x0c ") >> beforeAttrName tag [])
119 <|> (char '/' >> selfClosingStartTag tag [])
120 <|> (char '>' >> return (TagOpen tag []))
122-- | /§8.2.4.10/: Tag name state: close case
123tagNameClose :: Parser Token
126 char '>' >> return (TagClose tag)
128-- | /§8.2.4.10/: Tag name state: common code
130-- deviation: no lower-casing, don't handle NULL characters
131tagName' :: Parser Text
134 guard $ isAsciiUpper c || isAsciiLower c
135 takeWhile $ notInClass "\x09\x0a\x0c /<>"
137-- | /§8.2.4.40/: Self-closing start tag state
138selfClosingStartTag :: TagName -> [Attr] -> Parser Token
139selfClosingStartTag tag attrs = do
140 (char '>' >> return (TagSelfClose tag attrs))
141 <|> (endOfInput >> return endOfFileToken)
142 <|> beforeAttrName tag attrs
144-- | /§8.2.4.32/: Before attribute name state
146-- deviation: no lower-casing
147beforeAttrName :: TagName -> [Attr] -> Parser Token
148beforeAttrName tag attrs = do
149 skipWhile $ inClass "\x09\x0a\x0c "
150 id $ (char '/' >> selfClosingStartTag tag attrs)
151 <|> (char '>' >> return (TagOpen tag attrs))
152 -- <|> (char '\x00' >> attrName tag attrs) -- TODO: NULL
153 <|> attrName tag attrs
155-- | /§8.2.4.33/: Attribute name state
156attrName :: TagName -> [Attr] -> Parser Token
157attrName tag attrs = do
158 name <- takeWhile $ notInClass "\x09\x0a\x0c /=>"
159 id $ (endOfInput >> afterAttrName tag attrs name)
160 <|> (char '=' >> beforeAttrValue tag attrs name)
161 <|> try (do mc <- peekChar
163 Just c | inClass "\x09\x0a\x0c />" c -> afterAttrName tag attrs name
167-- | /§8.2.4.34/: After attribute name state
168afterAttrName :: TagName -> [Attr] -> AttrName -> Parser Token
169afterAttrName tag attrs name = do
170 skipWhile $ inClass "\x09\x0a\x0c "
171 id $ (char '/' >> selfClosingStartTag tag attrs)
172 <|> (char '=' >> beforeAttrValue tag attrs name)
173 <|> (char '>' >> return (TagOpen tag (Attr name T.empty : attrs)))
174 <|> (endOfInput >> return endOfFileToken)
175 <|> attrName tag (Attr name T.empty : attrs) -- not exactly sure this is right
177-- | /§8.2.4.35/: Before attribute value state
178beforeAttrValue :: TagName -> [Attr] -> AttrName -> Parser Token
179beforeAttrValue tag attrs name = do
180 skipWhile $ inClass "\x09\x0a\x0c "
181 id $ (char '"' >> attrValueDQuoted tag attrs name)
182 <|> (char '\'' >> attrValueSQuoted tag attrs name)
183 <|> (char '>' >> return (TagOpen tag (Attr name T.empty : attrs)))
184 <|> attrValueUnquoted tag attrs name
186-- | /§8.2.4.36/: Attribute value (double-quoted) state
187attrValueDQuoted :: TagName -> [Attr] -> AttrName -> Parser Token
188attrValueDQuoted tag attrs name = do
189 value <- takeWhile (/= '"')
191 afterAttrValueQuoted tag attrs name value
193-- | /§8.2.4.37/: Attribute value (single-quoted) state
194attrValueSQuoted :: TagName -> [Attr] -> AttrName -> Parser Token
195attrValueSQuoted tag attrs name = do
196 value <- takeWhile (/= '\'')
198 afterAttrValueQuoted tag attrs name value
200-- | /§8.2.4.38/: Attribute value (unquoted) state
201attrValueUnquoted :: TagName -> [Attr] -> AttrName -> Parser Token
202attrValueUnquoted tag attrs name = do
203 value <- takeTill (inClass "\x09\x0a\x0c >")
204 id $ (satisfy (inClass "\x09\x0a\x0c ") >> beforeAttrName tag attrs) -- unsure: don't emit?
205 <|> (char '>' >> return (TagOpen tag (Attr name value : attrs)))
206 <|> (endOfInput >> return endOfFileToken)
208-- | /§8.2.4.39/: After attribute value (quoted) state
209afterAttrValueQuoted :: TagName -> [Attr] -> AttrName -> AttrValue -> Parser Token
210afterAttrValueQuoted tag attrs name value =
211 (satisfy (inClass "\x09\x0a\x0c ") >> beforeAttrName tag attrs')
212 <|> (char '/' >> selfClosingStartTag tag attrs')
213 <|> (char '>' >> return (TagOpen tag attrs'))
214 <|> (endOfInput >> return endOfFileToken)
215 where attrs' = Attr name value : attrs
217-- | /§8.2.4.41/: Bogus comment state
218bogusComment :: Builder -> Parser Token
219bogusComment content = do
220 (char '>' >> return (Comment content))
221 <|> (endOfInput >> return (Comment content))
222 <|> (char '\x00' >> bogusComment (content <> "\xfffd"))
223 <|> (anyChar >>= \c -> bogusComment (content <> B.singleton c))
225-- | /§8.2.4.42/: Markup declaration open state
226markupDeclOpen :: Parser Token
230 <|> bogusComment mempty
232 comment_ = string "--" >> commentStart
234 -- switching this to asciiCI slowed things down by a factor of two
236 guard $ T.toLower s == "doctype"
239-- | /§8.2.4.43/: Comment start state
240commentStart :: Parser Token
242 (char '-' >> commentStartDash)
243 <|> (char '>' >> return (Comment mempty))
246-- | /§8.2.4.44/: Comment start dash state
247commentStartDash :: Parser Token
249 (char '-' >> commentEnd mempty)
250 <|> (char '>' >> return (Comment mempty))
251 <|> (endOfInput >> return (Comment mempty))
252 <|> (comment (B.singleton '-'))
254-- | /§8.2.4.45/: Comment state
255comment :: Builder -> Parser Token
257 content <- B.fromText <$> takeWhile (notInClass "-\x00<")
258 id $ (char '<' >> commentLessThan (content0 <> content <> "<"))
259 <|> (char '-' >> commentEndDash (content0 <> content))
260 <|> (char '\x00' >> comment (content0 <> content <> B.singleton '\xfffd'))
261 <|> (endOfInput >> return (Comment $ content0 <> content))
263-- | /§8.2.46/: Comment less-than sign state
264commentLessThan :: Builder -> Parser Token
265commentLessThan content =
266 (char '!' >> commentLessThanBang (content <> "!"))
267 <|> (char '<' >> commentLessThan (content <> "<"))
270-- | /§8.2.47/: Comment less-than sign bang state
271commentLessThanBang :: Builder -> Parser Token
272commentLessThanBang content =
273 (char '-' >> commentLessThanBangDash content)
276-- | /§8.2.48/: Comment less-than sign bang dash state
277commentLessThanBangDash :: Builder -> Parser Token
278commentLessThanBangDash content =
279 (char '-' >> commentLessThanBangDashDash content)
280 <|> commentEndDash content
282-- | /§8.2.49/: Comment less-than sign bang dash dash state
283commentLessThanBangDashDash :: Builder -> Parser Token
284commentLessThanBangDashDash content =
285 (char '>' >> comment content)
286 <|> (endOfInput >> comment content)
287 <|> commentEnd content
289-- | /§8.2.4.50/: Comment end dash state
290commentEndDash :: Builder -> Parser Token
291commentEndDash content = do
292 (char '-' >> commentEnd content)
293 <|> (endOfInput >> return (Comment content))
294 <|> (comment (content <> "-"))
296-- | /§8.2.4.51/: Comment end state
297commentEnd :: Builder -> Parser Token
298commentEnd content = do
299 (char '>' >> return (Comment content))
300 <|> (char '!' >> commentEndBang content)
301 <|> (char '-' >> commentEnd (content <> "-"))
302 <|> (endOfInput >> return (Comment content))
303 <|> (comment (content <> "--"))
305-- | /§8.2.4.52/: Comment end bang state
306commentEndBang :: Builder -> Parser Token
307commentEndBang content = do
308 (char '-' >> commentEndDash (content <> "--!"))
309 <|> (char '>' >> return (Comment content))
310 <|> (endOfInput >> return (Comment content))
311 <|> (comment (content <> "--!"))
313-- | /§8.2.4.53/: DOCTYPE state
315doctype :: Parser Token
317 content <- takeTill (=='>')
319 return $ Doctype content
321-- | Parse a lazy list of tokens from strict 'Text'.
322parseTokens :: Text -> [Token]
323parseTokens = unfoldr f
325 f :: Text -> Maybe (Token, Text)
329 case parse token t of
330 Done rest tok -> Just (tok, rest)
333 Done rest tok -> Just (tok, rest)
337-- | Parse a lazy list of tokens from lazy 'TL.Text'.
338parseTokensLazy :: TL.Text -> [Token]
339parseTokensLazy = unfoldr f
341 f :: TL.Text -> Maybe (Token, TL.Text)
343 | TL.null t = Nothing
345 case AL.parse token t of
346 AL.Done rest tok -> Just (tok, rest)
349-- | See 'renderToken'.
350renderTokens :: [Token] -> TL.Text
351renderTokens = mconcat . fmap renderToken
353-- | (Somewhat) canonical string representation of 'Token'.
354renderToken :: Token -> TL.Text
355renderToken = TL.fromStrict . mconcat . \case
356 (TagOpen n []) -> ["<", n, ">"]
357 (TagOpen n attrs) -> ["<", n, " ", renderAttrs attrs, ">"]
358 (TagSelfClose n attrs) -> ["<", n, " ", renderAttrs attrs, " />"]
359 (TagClose n) -> ["</", n, ">"]
360 (ContentChar c) -> [T.singleton c]
361 (ContentText t) -> [t]
362 (Comment builder) -> ["<!--", TL.toStrict $ B.toLazyText builder, "-->"]
363 (Doctype t) -> ["<!DOCTYPE", t, ">"]
365-- | See 'renderAttr'.
366renderAttrs :: [Attr] -> Text
367renderAttrs = T.unwords . fmap renderAttr . reverse
369-- | Does not escape quotation in attribute values!
370renderAttr :: Attr -> Text
371renderAttr (Attr k v) = mconcat [k, "=\"", v, "\""]
373-- | Meld neighoring 'ContentChar' and 'ContentText' constructors together and drops empty text
375canonicalizeTokens :: [Token] -> [Token]
376canonicalizeTokens = filter (/= ContentText "") . meldTextTokens
378meldTextTokens :: [Token] -> [Token]
379meldTextTokens = concatTexts . fmap charToText
381 charToText (ContentChar c) = ContentText (T.singleton c)
385 (ContentText t : ContentText t' : ts) -> concatTexts $ ContentText (t <> t') : ts
386 (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