CssQuery.hs 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. {-# LANGUAGE OverloadedStrings #-}
  2. -- | Parsing CSS selectors into queries.
  3. module Yesod.Test.CssQuery
  4. ( SelectorGroup (..)
  5. , Selector (..)
  6. , parseQuery
  7. ) where
  8. import Prelude hiding (takeWhile)
  9. import Data.Text (Text)
  10. import Data.Attoparsec.Text
  11. import Control.Applicative
  12. import Data.Char
  13. import qualified Data.Text as T
  14. data SelectorGroup
  15. = DirectChildren [Selector]
  16. | DeepChildren [Selector]
  17. deriving (Show, Eq)
  18. data Selector
  19. = ById Text
  20. | ByClass Text
  21. | ByTagName Text
  22. | ByAttrExists Text
  23. | ByAttrEquals Text Text
  24. | ByAttrContains Text Text
  25. | ByAttrStarts Text Text
  26. | ByAttrEnds Text Text
  27. deriving (Show, Eq)
  28. -- The official syntax specification for CSS2 can be found here:
  29. -- http://www.w3.org/TR/CSS2/syndata.html
  30. -- but that spec is tricky to fully support. Instead we do the minimal and we
  31. -- can extend it as needed.
  32. -- | Parses a query into an intermediate format which is easy to feed to HXT
  33. --
  34. -- * The top-level lists represent the top level comma separated queries.
  35. --
  36. -- * SelectorGroup is a group of qualifiers which are separated
  37. -- with spaces or > like these three: /table.main.odd tr.even > td.big/
  38. --
  39. -- * A SelectorGroup as a list of Selector items, following the above example
  40. -- the selectors in the group are: /table/, /.main/ and /.odd/
  41. parseQuery :: Text -> Either String [[SelectorGroup]]
  42. parseQuery = parseOnly cssQuery
  43. -- Below this line is the Parsec parser for css queries.
  44. cssQuery :: Parser [[SelectorGroup]]
  45. cssQuery = many (char ' ') >> sepBy rules (char ',' >> many (char ' '))
  46. rules :: Parser [SelectorGroup]
  47. rules = many $ directChildren <|> deepChildren
  48. directChildren :: Parser SelectorGroup
  49. directChildren =
  50. string "> " >> (many (char ' ')) >> DirectChildren <$> pOptionalTrailingSpace parseSelectors
  51. deepChildren :: Parser SelectorGroup
  52. deepChildren = pOptionalTrailingSpace $ DeepChildren <$> parseSelectors
  53. parseSelectors :: Parser [Selector]
  54. parseSelectors = many1 $
  55. parseId <|> parseClass <|> parseTag <|> parseAttr
  56. parseId :: Parser Selector
  57. parseId = char '#' >> ById <$> pIdent
  58. parseClass :: Parser Selector
  59. parseClass = char '.' >> ByClass <$> pIdent
  60. parseTag :: Parser Selector
  61. parseTag = ByTagName <$> pIdent
  62. parseAttr :: Parser Selector
  63. parseAttr = pSquare $ choice
  64. [ ByAttrEquals <$> pIdent <*> (string "=" *> pAttrValue)
  65. , ByAttrContains <$> pIdent <*> (string "*=" *> pAttrValue)
  66. , ByAttrStarts <$> pIdent <*> (string "^=" *> pAttrValue)
  67. , ByAttrEnds <$> pIdent <*> (string "$=" *> pAttrValue)
  68. , ByAttrExists <$> pIdent
  69. ]
  70. -- | pIdent : Parse an identifier (not yet supporting escapes and unicode as
  71. -- part of the identifier). Basically the regex: [-]?[_a-zA-Z][_a-zA-Z0-9]*
  72. pIdent :: Parser Text
  73. pIdent = do
  74. leadingMinus <- string "-" <|> pure ""
  75. nmstart <- T.singleton <$> satisfy (\c -> isAlpha c || c == '_')
  76. nmchar <- takeWhile (\c -> isAlphaNum c || c == '_' || c == '-')
  77. return $ T.concat [ leadingMinus, nmstart, nmchar ]
  78. pAttrValue :: Parser Text
  79. pAttrValue = takeWhile (/= ']')
  80. pSquare :: Parser a -> Parser a
  81. pSquare p = char '[' *> p <* char ']'
  82. pOptionalTrailingSpace :: Parser a -> Parser a
  83. pOptionalTrailingSpace p = p <* many (char ' ')