css-syntax 0.0.8 → 0.1.0.2
raw patch · 4 files changed
Files
- benchmark/Benchmark.hs +87/−0
- css-syntax.cabal +29/−11
- src/Data/CSS/Syntax/Tokens.hs +640/−321
- test/Test.hs +321/−186
+ benchmark/Benchmark.hs view
@@ -0,0 +1,87 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Control.Monad+import Criterion.Main+import System.Directory+import qualified Data.Text as T+import qualified Data.Text.IO as T+import Data.CSS.Syntax.Tokens+import Control.DeepSeq++-- We're benchmarking speed of processing of 1MB input.+-- So we repeat any input to fill 1MB text.++fill :: T.Text -> T.Text+fill t = T.take size $ T.replicate (size `div` T.length t + 1) t+ where size = 1000000++instance NFData NumericValue where rnf x = seq x ()+instance NFData Token where rnf x = seq x ()++fileBenchmarks :: NFData b => (a -> b) -> (T.Text -> a) -> IO [Benchmark]+fileBenchmarks f preprocess = do+ cwd <- getCurrentDirectory+ websites <- drop 2 <$> getDirectoryContents (cwd ++ "/benchmark/fixtures")+ forM websites $ \website -> do+ files <- drop 2 <$> getDirectoryContents (cwd ++ "/benchmark/fixtures/" ++ website)+ benchmarks <- forM files $ \file -> do+ body <- T.readFile $ cwd ++ "/benchmark/fixtures/" ++ website ++ "/" ++ file+ pure $ bench file $ nf f $ preprocess $ fill body++ pure $ bgroup website benchmarks++main :: IO ()+main = do+ tokenizeBenchmarks <- fileBenchmarks tokenize id+ serializeBenchmarks <- fileBenchmarks serialize tokenize+ serializeTokenizeBenchmarks <- fileBenchmarks (serialize . tokenize) id++ defaultMain [+ bgroup "tokenize" tokenizeBenchmarks,+ bgroup "serialize" serializeBenchmarks,+ bgroup "serialize/tokenize" serializeTokenizeBenchmarks,++ bgroup "tokenize"+ [ tBench' "whitespace" " "+ , bench "comment" $ nf tokenize $ "/*" <> fill " " <> "*/"+ , numBench 1000+ , numBench 10000+ , numBench 100000+ , numBench 1000000+ , tBench' "aaa.." "a"+ , tBench' "esc2" "\\ab"+ , tBench' "esc5" "\\abcde"+ , bench "url(aaa..)" $ nf tokenize $ "url(" <> fill "a" <> ")"+ , bench "url(esc2)" $ nf tokenize $ "url(" <> fill "\\ab" <> ")"+ , bench "badUrl(aaa..)"$ nf tokenize $ "url((" <> fill "a" <> ")"+ , bench "badUrl(esc2)" $ nf tokenize $ "url((" <> fill "\\ab" <> ")"+ , tBench ";"+ , tBench "||"+ , tBench "a;"+ , tBench "1;"+ , tBench "a:1;"+ , tBench "1234567890;"+ , tBench "z-index:1;"+ , tBench "#123456 "+ , tBench "#FFFFFF "+ ],++ bgroup "serialize"+ [ sBench "aaa"+ , sBench ";"+ , sBench "||"+ , sBench "1;"+ , sBench "1234567890;"+ , sBench "a:1;"+ , sBench "z-index:1;"+ ]+ ]+ where sBench n =+ let t = tokenize $ fill $ T.pack n in+ rnf t `seq` bench n $ nf serialize t+ tBench n = tBench' n n+ tBench' nm n = bench nm $ nf tokenize $ fill $ T.pack n+ numBench n = bench ("1e" ++ show n) $+ nf tokenize $ fill $ "1" <> T.replicate n "0" <> " "
css-syntax.cabal view
@@ -1,7 +1,7 @@ name: css-syntax-version: 0.0.8+version: 0.1.0.2 -synopsis: This package implments a parser for the CSS syntax+synopsis: High-performance CSS tokenizer and serializer. description: See https://drafts.csswg.org/css-syntax/ @@ -9,8 +9,8 @@ license: MIT license-file: LICENSE -author: Tomas Carnecky-maintainer: tomas.carnecky@gmail.com+author: Tomas Carnecky <tomas.carnecky@gmail.com>, Vladimir Shabanov <dev@vshabanov.com>+maintainer: Tomas Carnecky <tomas.carnecky@gmail.com> category: Data @@ -32,18 +32,16 @@ build-depends: base >=4 && <5- , attoparsec >=0.13- , bytestring , scientific- , text+ , text >=2.0 && <2.1 - ghc-options: -Wall+ ghc-options: -Wall -O2 test-suite spec hs-source-dirs: test src default-language: Haskell2010- ghc-options: -Wall+ ghc-options: -Wall -Wno-orphans type: exitcode-stdio-1.0 main-is: Test.hs@@ -53,10 +51,30 @@ build-depends: base >=4 && <5- , attoparsec >=0.13- , bytestring , scientific , text , hspec+ , directory+ , QuickCheck++benchmark benchmark+ hs-source-dirs: benchmark src+ default-language: Haskell2010+ ghc-options: -Wall -Wno-orphans -O2 -rtsopts+ -- -fprof-auto -fprof-cafs++ type: exitcode-stdio-1.0+ main-is: Benchmark.hs++ other-modules:+ Data.CSS.Syntax.Tokens++ build-depends:+ base >=4 && <5+ , scientific+ , text++ , criterion+ , deepseq , directory
src/Data/CSS/Syntax/Tokens.hs view
@@ -1,4 +1,5 @@-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedStrings, RankNTypes, PatternSynonyms, ViewPatterns,+ BangPatterns, MagicHash #-} module Data.CSS.Syntax.Tokens ( Token(..)@@ -14,9 +15,9 @@ import Control.Applicative import Control.Monad -import Data.Text (Text) import qualified Data.Text as T-import Data.Attoparsec.Text as AP+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Builder as TLB import Data.Monoid import Data.Char import Data.Scientific@@ -24,6 +25,14 @@ import Prelude +import Data.Text.Internal (Text(..))+import Data.Text.Unsafe (inlineInterleaveST)+import qualified Data.Text.Array as A+import Control.Monad.ST (ST)+import GHC.Exts+import GHC.Base (unsafeChr)+import GHC.Word (Word8(..))+import Data.Bits data Token@@ -51,15 +60,15 @@ | Column - | String !Char !Text- | BadString !Char !Text+ | String !Text+ | BadString | Number !Text !NumericValue | Percentage !Text !NumericValue | Dimension !Text !NumericValue !Unit | Url !Text- | BadUrl !Text+ | BadUrl | Ident !Text @@ -75,8 +84,8 @@ data NumericValue- = NVInteger !Scientific- | NVNumber !Scientific+ = NVInteger !Integer -- ^ number without dot '.' or exponent 'e'+ | NVNumber !Scientific -- ^ number with dot '.' or exponent 'e' deriving (Show, Eq) data HashFlag = HId | HUnrestricted@@ -94,8 +103,8 @@ -- -- https://drafts.csswg.org/css-syntax/#tokenization -tokenize :: Text -> Either String [Token]-tokenize = parseOnly (many' parseToken) . preprocessInputStream+tokenize :: Text -> [Token]+tokenize = parseTokens . preprocessInputStream @@ -105,409 +114,719 @@ -- https://drafts.csswg.org/css-syntax/#input-preprocessing preprocessInputStream :: Text -> Text-preprocessInputStream = T.pack . f . T.unpack- where- f [] = []+preprocessInputStream t0@(Text _ _ len) = withNewA (len*3) $ \ dst -> do+ let go t d = case t of+ '\x0D' :. '\x0A' :. t' ->+ put '\x0A' t'+ '\x0D' :. t' ->+ put '\x0A' t'+ '\x0C' :. t' ->+ put '\x0A' t'+ '\x00' :. t' -> do+ d' <- writeFFFD dst d+ go t' d'+ c :. t' ->+ put c t'+ _ ->+ return d+ where put x t' = do+ write dst d x+ go t' (d + 1)+ go t0 0 - f ('\x000D':'\x000A':r) = '\x000A' : f r - f ('\x000D':r) = '\x000A' : f r- f ('\x000C':r) = '\x000A' : f r+-- Low level utilities+------------------------------------------------------------------------------- - f ('\x0000':r) = '\xFFFD' : f r+pattern (:.) :: Char -> Text -> Text+pattern x :. xs <- (uncons -> Just (x, xs)) - f (x:r) = x : f r+infixr 5 :. +-- | uncons first Word8 from Text without trying to decode UTF-8 sequence+uncons :: Text -> Maybe (Char, Text)+uncons (Text src offs len)+ | len <= 0 = Nothing+ | otherwise =+ Just (w2c (A.unsafeIndex src offs), Text src (offs+1) (len-1))+{-# INLINE uncons #-} +-- | write replacement character+writeFFFD :: A.MArray s -> Int -> ST s Int+writeFFFD dst d = writeChar dst d '\xFFFD' --- Serialization--------------------------------------------------------------------------------+-- | write 8bit character+write :: A.MArray s -> Int -> Char -> ST s ()+write dst d x = A.unsafeWrite dst d (c2w x)+{-# INLINE write #-} +-- | write character that could have more than 8bit+-- code from Data.Text.Internal.Unsafe.Char.unsafeWrite+writeChar :: A.MArray s -> Int -> Char -> ST s Int+writeChar marr i c = case utf8Length c of+ 1 -> do+ let n0 = intToWord8 (ord c)+ A.unsafeWrite marr i n0+ return (i+1)+ 2 -> do+ let (n0, n1) = ord2 c+ A.unsafeWrite marr i n0+ A.unsafeWrite marr (i+1) n1+ return (i+2)+ 3 -> do+ let (n0, n1, n2) = ord3 c+ A.unsafeWrite marr i n0+ A.unsafeWrite marr (i+1) n1+ A.unsafeWrite marr (i+2) n2+ return (i+3)+ _ -> do+ let (n0, n1, n2, n3) = ord4 c+ A.unsafeWrite marr i n0+ A.unsafeWrite marr (i+1) n1+ A.unsafeWrite marr (i+2) n2+ A.unsafeWrite marr (i+3) n3+ return (i+4)+{-# INLINE writeChar #-} --- | Serialize a list of 'Token's back into 'Text'. Round-tripping is not--- guaranteed to be identity. The tokenization step drops some information--- from the source.------ https://drafts.csswg.org/css-syntax/#serialization+utf8Length :: Char -> Int+utf8Length (C# c) = I# ((1# +# geChar# c (chr# 0x80#)) +# (geChar# c (chr# 0x800#) +# geChar# c (chr# 0x10000#)))+{-# INLINE utf8Length #-} -serialize :: [Token] -> Text-serialize = mconcat . map renderToken+ord2 :: Char -> (Word8,Word8)+ord2 c = (x1,x2)+ where+ n = ord c+ x1 = intToWord8 $ (n `shiftR` 6) + 0xC0+ x2 = intToWord8 $ (n .&. 0x3F) + 0x80+{-# INLINE ord2 #-} +ord3 :: Char -> (Word8,Word8,Word8)+ord3 c = (x1,x2,x3)+ where+ n = ord c+ x1 = intToWord8 $ (n `shiftR` 12) + 0xE0+ x2 = intToWord8 $ ((n `shiftR` 6) .&. 0x3F) + 0x80+ x3 = intToWord8 $ (n .&. 0x3F) + 0x80+{-# INLINE ord3 #-} -renderToken :: Token -> Text-renderToken (Whitespace) = " "+ord4 :: Char -> (Word8,Word8,Word8,Word8)+ord4 c = (x1,x2,x3,x4)+ where+ n = ord c+ x1 = intToWord8 $ (n `shiftR` 18) + 0xF0+ x2 = intToWord8 $ ((n `shiftR` 12) .&. 0x3F) + 0x80+ x3 = intToWord8 $ ((n `shiftR` 6) .&. 0x3F) + 0x80+ x4 = intToWord8 $ (n .&. 0x3F) + 0x80+{-# INLINE ord4 #-} -renderToken (CDO) = "<!--"-renderToken (CDC) = "-->"+intToWord8 :: Int -> Word8+intToWord8 = fromIntegral -renderToken (Comma) = ","-renderToken (Colon) = ":"-renderToken (Semicolon) = ";" -renderToken (LeftParen) = "("-renderToken (RightParen) = ")"-renderToken (LeftSquareBracket) = "["-renderToken (RightSquareBracket) = "]"-renderToken (LeftCurlyBracket) = "{"-renderToken (RightCurlyBracket) = "}"+type Writer' s = (A.MArray s -> Int -> ST s Int, Text)+type Writer s = A.MArray s -> Int -> ST s (Int, Text) -renderToken (SuffixMatch) = "$="-renderToken (SubstringMatch) = "*="-renderToken (PrefixMatch) = "^="-renderToken (DashMatch) = "|="-renderToken (IncludeMatch) = "~="+-- | no-op for convenient pattern matching+w2c :: Word8 -> Char+w2c = unsafeChr . fromIntegral+{-# INLINE w2c #-} -renderToken (Column) = "||"+c2w :: Char -> Word8+c2w = fromIntegral . ord+{-# INLINE c2w #-} -renderToken (String d x) = T.singleton d <> renderString x <> T.singleton d-renderToken (BadString d x) = T.singleton d <> renderString x <> T.singleton d+withNewA :: Int -> (forall s . A.MArray s -> ST s Int) -> Text+withNewA len act = Text a 0 l+ where (a, l) = A.run2 $ do+ dst <- A.new len+ dLen <- act dst+ return (dst, dLen) -renderToken (Number x _) = x-renderToken (Percentage x _) = x <> "%"-renderToken (Dimension x _ u) = x <> u -renderToken (Url x) = "url(" <> x <> ")"-renderToken (BadUrl x) = "url(" <> x <> ")"+-- Serialization+------------------------------------------------------------------------------- -renderToken (Ident x) = x -renderToken (AtKeyword x) = "@" <> x+-- | Serialize a list of 'Token's back into 'Text'.+--+-- Serialization "round-trips" with parsing:+--+-- tokenize (serialize (tokenize s)) == tokenize s+--+-- https://drafts.csswg.org/css-syntax/#serialization -renderToken (Function x) = x <> "(" -renderToken (Hash _ x) = "#" <> x+serialize :: [Token] -> Text+serialize = TL.toStrict . TLB.toLazyText . go+ where go [] = ""+ go [Delim '\\'] = "\\" -- do not add newline in last token+ go [x] = renderToken x+ go (x:xs@(y:_))+ | needComment x y = renderToken x <> "/**/" <> go xs+ | otherwise = renderToken x <> go xs -renderToken (Delim x) = T.singleton x+{-# INLINE renderToken #-}+{-# INLINE needComment #-} +needComment :: Token -> Token -> Bool+needComment a CDC = case a of+ -- Can't be parsed that way but may exists in generated `Token` list.+ -- It's also possible to make Delim 'a' which will be parsed as Ident+ -- but we can't do much in this case since it's impossible to+ -- create Delim 'a' tokens in parser.+ Delim '!' -> True+ Delim '@' -> True+ Delim '#' -> True+ Delim '-' -> True+ Number {} -> True+ Dimension {} -> True+ Ident _ -> True+ AtKeyword _ -> True+ Function _ -> True+ Hash {} -> True+ _ -> False+needComment a b = case a of+ Whitespace -> b == Whitespace+ Ident "--" -> b == Delim '>' -- Looks like a CDC+ Ident _ -> idn || b == CDC || b == LeftParen+ AtKeyword _ -> idn || b == CDC+ Hash {} -> idn || b == CDC+ Dimension {} -> idn || b == CDC+ Delim '#' -> idn+ Delim '-' -> idn+ Number {} -> i || num || b == Delim '%'+ Delim '@' -> i || b == Delim '-'+ Delim '.' -> num+ Delim '+' -> num+ Delim '/' -> b == Delim '*' || b == SubstringMatch+ Delim '|' -> b == Delim '='+ || b == Delim '|' || b == Column || b == DashMatch+ Delim '$' -> b == Delim '='+ Delim '*' -> b == Delim '='+ Delim '^' -> b == Delim '='+ Delim '~' -> b == Delim '='+ _ -> False+ where idn = i || b == Delim '-' || num+ i = case b of+ Ident _ -> True+ Function _ -> True+ Url _ -> True+ BadUrl -> True+ _ -> False+ num = case b of+ Number {} -> True+ Percentage {} -> True+ Dimension {} -> True+ _ -> False -renderString :: Text -> Text-renderString = T.pack . concatMap f . T.unpack- where- nonPrintableCodePoint c- | c >= '\x0000' && c <= '\x0008' = True -- NULL through BACKSPACE- | c == '\x000B' = True -- LINE TABULATION- | c >= '\x000E' && c <= '\x001F' = True -- SHIFT OUT through INFORMATION SEPARATOR ONE- | c == '\x007F' = True -- DELETE- | otherwise = False+renderToken :: Token -> TLB.Builder+renderToken token = case token of+ Whitespace -> c ' ' - nonASCIICodePoint c = c >= '\x0080' -- control+ CDO -> "<!--"+ CDC -> "-->" - f c = if nonPrintableCodePoint c || nonASCIICodePoint c- then "\\" <> showHex (ord c) ""- else [c]+ Comma -> c ','+ Colon -> c ':'+ Semicolon -> c ';' + LeftParen -> c '('+ RightParen -> c ')'+ LeftSquareBracket -> c '['+ RightSquareBracket -> c ']'+ LeftCurlyBracket -> c '{'+ RightCurlyBracket -> c '}' -parseComment :: Parser ()-parseComment = do- void $ AP.string "/*"- void $ AP.manyTill' AP.anyChar (void (AP.string "*/") <|> AP.endOfInput)+ SuffixMatch -> "$="+ SubstringMatch -> "*="+ PrefixMatch -> "^="+ DashMatch -> "|="+ IncludeMatch -> "~=" -parseWhitespace :: Parser Token-parseWhitespace = do- void $ AP.takeWhile1 isWhitespace- return Whitespace+ Column -> "||" -parseChar :: Token -> Char -> Parser Token-parseChar t c = do- _ <- AP.char c- return t+ String x -> string x+ BadString -> "\"\n" -parseStr :: Token -> Text -> Parser Token-parseStr t str = AP.string str *> return t+ Number x _ -> t x+ Percentage x _ -> t x <> c '%'+ Dimension x _ u -> t x <> t (renderDimensionUnit x u) -escapedCodePoint :: Parser Char-escapedCodePoint = do- mbChar <- AP.peekChar- case mbChar of- Nothing -> return $ '\xFFFD'- Just ch -> do- if isHexChar ch- then do- (t, _) <- AP.runScanner [] f- case unhex (T.unpack t) of- Nothing -> fail $ "escapedCodePoint: unable to parse hex " ++ (T.unpack t)- Just cp -> do- AP.peekChar >>= \c -> case c of- Just nc -> if isWhitespace nc then void AP.anyChar else return ()- _ -> return ()- return $ if cp == 0 || cp > 0x10FFFF- then chr 0xFFFD- else chr cp- else do- if ch == '\n'- then fail "A newline"- else AP.anyChar >> return ch+ Url x -> "url(" <> t (renderUrl x) <> c ')'+ BadUrl -> "url(()" - where- f :: String -> Char -> Maybe String- f acc c =- if length acc < 6 && isHexChar c- then Just $ c:acc- else Nothing+ Ident x -> ident x + AtKeyword x -> c '@' <> ident x -nextInputCodePoint :: Parser Char-nextInputCodePoint = escapedCodePoint' <|> AP.anyChar+ Function x -> ident x <> c '(' + Hash HId x -> c '#' <> ident x+ Hash HUnrestricted x -> c '#' <> t (renderUnrestrictedHash x) -whenNext :: Char -> a -> Parser a-whenNext c a = do- mbChar <- AP.peekChar- if mbChar == Just c- then return a- else fail "whenNext"+ Delim '\\' -> "\\\n"+ Delim x -> c x+ where c = TLB.singleton+ t = TLB.fromText+ q = c '"'+ string x = q <> t (renderString x) <> q+ ident = t . renderIdent --- 4.3.4. Consume a string token-parseString :: Char -> Parser Token-parseString endingCodePoint = do- _ <- AP.char endingCodePoint- go []+-- https://www.w3.org/TR/cssom-1/#serialize-a-string +renderString :: Text -> Text+renderString t0@(Text _ _ l)+ | T.any needEscape t0 = withNewA (l*8) $ go t0 0+ | otherwise = t0 where- go acc = choice- [ (AP.endOfInput <|> void (AP.char endingCodePoint)) *> return (String endingCodePoint $ fromAcc acc)- , AP.string "\\\n" *> go acc- , whenNext '\n' (BadString endingCodePoint $ fromAcc acc)- , nextInputCodePoint >>= \ch -> go (ch:acc)- ]+ needEscape c = c <= '\x1F' || c == '\x7F' || c == '"' || c == '\\'+ go t d dst = case T.uncons t of+ Nothing -> return d+ Just (c, t')+ | c == '\x0' -> do+ d' <- writeFFFD dst d+ -- spec says it should be escaped, but we loose+ -- serialize->tokenize->serialize roundtrip that way+ go t' d' dst+ | (c >= '\x1' && c <= '\x1F') || c == '\x7F' -> do+ d' <- escapeAsCodePoint dst d c+ go t' d' dst+ | c == '"' || c == '\\' -> do+ -- strings are always in double quotes, so '\'' aren't escaped+ write dst d '\\'+ write dst (d+1) c+ go t' (d+2) dst+ | otherwise -> do+ d' <- writeChar dst d c+ go t' d' dst -fromAcc :: [Char] -> Text-fromAcc = T.pack . reverse+renderUrl :: Text -> Text+renderUrl t0@(Text _ _ l)+ | T.any needEscape t0 = withNewA (l*8) $ go t0 0+ | otherwise = t0+ where+ needEscape c = c <= '\x1F' || c == '\x7F' || isWhitespace c+ || c == '\\' || c == ')' || c == '"' || c == '\'' || c == '('+ go t d dst = case T.uncons t of+ Nothing -> return d+ Just (c, t')+ | c == '\x0' -> do+ d' <- writeFFFD dst d+ go t' d' dst+ | needEscape c -> do+ d' <- escapeAsCodePoint dst d c+ go t' d' dst+ | otherwise -> do+ d' <- writeChar dst d c+ go t' d' dst -parseHash :: Parser Token-parseHash = do- _ <- AP.char '#'- name <- parseName- return $ Hash HId name+renderDimensionUnit :: Text -> Text -> Text+renderDimensionUnit num t0@(Text _ _ l)+ | not (T.any isExponent num)+ , c :. t' <- t0+ , isExponent c && validExp t' =+ withNewA (l*8) $ \ dst -> do+ d' <- escapeAsCodePoint dst 0 c+ renderUnrestrictedHash' t' d' dst+ | otherwise =+ renderIdent t0+ where validExp (s :. d :. _) | (s == '+' || s == '-') = isDigit d+ validExp (d :. _) = isDigit d+ validExp _ = False -isNameStartCodePoint :: Char -> Bool-isNameStartCodePoint c = isLetter c || c >= '\x0080' || c == '_'+renderIdent :: Text -> Text+renderIdent "-" = "\\-"+renderIdent t0@(Text _ _ l) = case t0 of+ c :. t'+ | isDigit c -> withNewA (l*8) $ \ dst -> do+ d' <- escapeAsCodePoint dst 0 c+ renderUnrestrictedHash' t' d' dst+ '-' :. c :. t'+ | isDigit c -> withNewA (l*8) $ \ dst -> do+ write dst 0 '-'+ d' <- escapeAsCodePoint dst 1 c+ renderUnrestrictedHash' t' d' dst+ _ -> renderUnrestrictedHash t0 -isNameCodePoint :: Char -> Bool-isNameCodePoint c = isNameStartCodePoint c || isDigit c || c == '-'+renderUnrestrictedHash :: Text -> Text+renderUnrestrictedHash t0@(Text _ _ l)+ | T.any (not . nameCodePoint) t0 =+ withNewA (l*8) $ renderUnrestrictedHash' t0 0+ | otherwise = t0 -parseNumeric :: Parser Token-parseNumeric = do- (repr, nv) <- parseNumericValue- dimNum repr nv <|> pctNum repr nv <|> return (Number repr nv)- where- dimNum repr nv = do- unit <- parseName- return $ Dimension repr nv unit- pctNum repr nv = do- _ <- AP.char '%'- return $ Percentage repr nv+renderUnrestrictedHash' :: Text -> Int -> A.MArray s -> ST s Int+renderUnrestrictedHash' = go+ where go t d dst = case T.uncons t of+ Nothing -> return d+ Just (c, t')+ | c == '\x0' -> do+ d' <- writeFFFD dst d+ go t' d' dst+ | (c >= '\x1' && c <= '\x1F') || c == '\x7F' -> do+ d' <- escapeAsCodePoint dst d c+ go t' d' dst+ | nameCodePoint c -> do+ d' <- writeChar dst d c+ go t' d' dst+ | otherwise -> do+ write dst d '\\'+ d' <- writeChar dst (d+1) c+ go t' d' dst -nameCodePoint :: Parser Char-nameCodePoint = AP.satisfy isNameCodePoint+escapeAsCodePoint :: A.MArray s -> Int -> Char -> ST s Int+escapeAsCodePoint dst d c = do+ write dst d '\\'+ d' <- foldM (\ o x -> write dst o x >> return (o+1))+ (d+1) (showHex (ord c) [])+ write dst d' ' '+ return (d' + 1) -escapedCodePoint' :: Parser Char-escapedCodePoint' = do- _ <- AP.char '\\'- escapedCodePoint -parseName :: Parser Text-parseName = do- chars <- AP.many1' $- nameCodePoint <|> escapedCodePoint'+-- | verify valid escape and consume escaped code point+escapedCodePoint :: Text -> Maybe (Writer' s)+escapedCodePoint t = case t of+ (hex -> Just d) :. ts -> go 5 d ts+ '\n' :. _ -> Nothing+ c :. ts -> Just (\ dst d -> write dst d c >> return (d+1), ts)+ _ -> Nothing+ where go :: Int -> Int -> Text -> Maybe (Writer' s)+ go 0 acc ts = ret acc ts+ go n acc ts = case ts of+ (hex -> Just d) :. ts' -> go (n-1) (acc*16 + d) ts'+ c :. ts' | isWhitespace c -> ret acc ts'+ _ -> ret acc ts+ ret c ts = Just+ (\ dst d ->+ if safe c+ then writeChar dst d (unsafeChr c)+ else writeFFFD dst d+ ,ts) - case chars of- '-':xs -> case xs of- _:_ -> return $ T.pack chars- _ -> fail "parseName: Not a valid name start"- _ -> return $ T.pack chars+safe :: Int -> Bool+safe x+ | x == 0 || x > 0x10FFFF = False+ | x .&. 0x1ff800 /= 0xd800 = True+ | otherwise = False -- UTF-16 surrogate code point +hex :: Char -> Maybe Int+hex c+ | c >= '0' && c <= '9' = Just (ord c - ord '0')+ | c >= 'a' && c <= 'f' = Just (ord c - ord 'a' + 10)+ | c >= 'A' && c <= 'F' = Just (ord c - ord 'A' + 10)+ | otherwise = Nothing -parseSign :: Parser (Text, Int)-parseSign = do- mbChar <- AP.peekChar- case mbChar of- Just '+' -> AP.anyChar >> return ("+", 1)- Just '-' -> AP.anyChar >> return ("-", (-1))- _ -> return ("", 1)+{-# INLINE safe #-}+{-# INLINE hex #-} -parseNumericValue :: Parser (Text, NumericValue)-parseNumericValue = do- -- Sign- (sS, s) <- parseSign+escapedCodePoint' :: Text -> Maybe (Writer' s)+escapedCodePoint' ('\\' :. ts) = escapedCodePoint ts+escapedCodePoint' _ = Nothing - -- Digits before the decimal dot. They are optional (".1em").- (iS, i) <- do- digits <- AP.takeWhile isDigit- return $ if (T.null digits)- then ("", 0)- else (digits, read $ T.unpack digits)+nameStartCodePoint :: Char -> Bool+nameStartCodePoint c =+ isAsciiLower c || isAsciiUpper c || c >= '\x0080' || c == '_' - -- Decimal dot and digits after it. If the decimal dot is there then it- -- MUST be followed by one or more digits. This is not allowed: "1.".- (fS, f, fB) <- option ("", 0, False) $ do- _ <- AP.char '.'- digits <- AP.takeWhile1 isDigit- return ("." <> digits, read $ T.unpack digits, True)+nameCodePoint :: Char -> Bool+nameCodePoint c = nameStartCodePoint c || isDigit c || c == '-' - -- Exponent (with optional sign).- (tS, t, eS, e, eB) <- option ("", 1, "", 0, False) $ do- e <- AP.char 'E' <|> AP.char 'e'- (tS, t) <- parseSign- eS <- AP.takeWhile1 isDigit+satisfyOrEscaped :: (Char -> Bool) -> Text -> Maybe (Writer' s)+satisfyOrEscaped p (c :. ts)+ | p c = Just (\ dst d -> write dst d c >> return (d+1), ts)+ | c == '\\' = escapedCodePoint ts+satisfyOrEscaped _ _ = Nothing - return (T.singleton e <> tS, t, eS, read $ T.unpack eS, True)+-- | Check if three code points would start an identifier and consume name+parseName :: Text -> Maybe (Writer s)+parseName t = case t of+ '-' :. ts -> consumeName' <$> satisfyOrEscaped (\ c -> nameStartCodePoint c || c == '-') ts+ ts -> consumeName <$> satisfyOrEscaped nameStartCodePoint ts+ where consumeName' n dst d = do+ write dst d '-'+ consumeName n dst (d + 1) - let repr = sS<>iS<>fS<>tS<>eS- if T.null repr || repr == "-" || repr == "+" || T.head repr == 'e' || T.head repr == 'E'- then fail "parseNumericValue: no parse"- else do- let v = fromIntegral s * (i + f*10^^(-(T.length fS - 1))) * 10^^(t*e)- return $ if fB || eB- then (repr, NVNumber v)- else (repr, NVInteger v) +consumeName :: Writer' s -> Writer s+consumeName (w0, ts0) dst d0 = do+ d' <- w0 dst d0+ loop ts0 d'+ where loop ts d = case satisfyOrEscaped nameCodePoint ts of+ Just (w, ts') -> do+ d' <- w dst d+ loop ts' d'+ Nothing -> return (d, ts) -parseUrl :: Parser Token-parseUrl = do- _ <- AP.takeWhile isWhitespace- go []+{-# INLINE parseName #-}+{-# INLINE consumeName #-}+{-# INLINE satisfyOrEscaped #-}+{-# INLINE escapedCodePoint #-}+{-# INLINE escapedCodePoint' #-} - where- endOfUrl acc = (AP.endOfInput <|> void (AP.char ')')) *> return (Url $ fromAcc acc)+parseNumericValue :: Text -> Maybe (Text, NumericValue, Text)+parseNumericValue t0@(Text a offs1 _) = case withSign start t0 of+ Just (nv, ts@(Text _ offs2 _)) ->+ Just (Text a offs1 (offs2 - offs1), nv, ts)+ Nothing -> Nothing+ where start sign t = case t of+ '.' :. (digit -> Just d) :. ts -> dot sign (startIR d) (-1) ts+ (digit -> Just d) :. ts -> digits sign (startIR d) ts+ _ -> Nothing+ digits sign !c t = case t of+ '.' :. (digit -> Just d) :. ts -> dot sign (accIR c d) (-1) ts+ (digit -> Just d) :. ts -> digits sign (accIR c d) ts+ _ -> Just $ expn True (sign $ readIR c) 0 t+ dot sign !c !e t = case t of+ (digit -> Just d) :. ts -> dot sign (accIR c d) (e-1) ts+ _ -> Just $ expn False (sign $ readIR c) e t+ expn int c e0 t = case t of+ x :. ts+ | isExponent x+ , Just r <- withSign (expStart c e0 0) ts -> r+ _ | int -> (NVInteger c, t)+ | otherwise -> (NVNumber $ scientific c e0, t)+ expStart c e0 e sign t = case t of+ (digit -> Just d) :. ts -> expDigits c e0 (e*10 + d) sign ts+ _ -> Nothing+ expDigits c e0 !e sign t = case t of+ (digit -> Just d) :. ts -> expDigits c e0 (e*10 + d) sign ts+ _ -> Just (NVNumber $ scientific c (sign e + e0), t)+ digit :: Enum a => Char -> Maybe a+ digit c+ | isDigit c = Just (toEnum $ ord c - ord '0')+ | otherwise = Nothing+ withSign :: Num a => ((a -> a) -> Text -> Maybe (b, Text))+ -> Text -> Maybe (b, Text)+ withSign f t = case t of+ '+' :. ts -> f id ts+ '-' :. ts -> f negate ts+ _ -> f id t - go acc = choice- [ endOfUrl acc- , (AP.char '"' <|> AP.char '\'' <|> AP.char '(') >>= \ch -> badUrl (ch:acc)- , AP.string "\\\n" *> badUrl ('\n':'\\':acc)- , AP.takeWhile1 isWhitespace >>= \c -> (endOfUrl acc <|> badUrl (reverse (T.unpack c) ++ acc))- , nextInputCodePoint >>= \ch -> go (ch:acc)- ]+-- Idea stolen from GHC implementation of `instance Read Integer`+-- http://hackage.haskell.org/package/base-4.11.1.0/docs/src/Text.Read.Lex.html#valInteger+-- A sub-quadratic algorithm for converting digits to Integer.+-- First we collect blocks of `blockDigits`-digit Integers+-- (so we don't do anything besides simple (acc*10+digit) on most inputs).+-- Then we combine them:+-- Pairs of adjacent radix b digits are combined into a single radix b^2 digit.+-- This process is repeated until we are left with a single digit. - badUrl acc = choice- [ (AP.endOfInput <|> void (AP.char ')')) *> return (BadUrl $ fromAcc acc)- , nextInputCodePoint >>= \ch -> badUrl (ch:acc)- ]+blockDigits :: Int+blockDigits = 40 +startBase :: Integer+startBase = 10^blockDigits -parseIdentLike :: Parser Token-parseIdentLike = do- name <- parseName- choice- [ do- -- Special handling of url() functions (they are not really- -- functions, they have their own Token type).- guard $ T.isPrefixOf "url" (T.map toLower name)+-- | (num digits in current block, blocks, current block's value)+type IntegerReader = (Int, [Integer], Integer) - void $ AP.char '('- void $ AP.takeWhile isWhitespace+startIR :: Integer -> IntegerReader+startIR d = (1, [], d) - whenNext '"' (Function name) <|> whenNext '\'' (Function name) <|> parseUrl+{-# INLINE startIR #-}+{-# INLINE accIR #-}+{-# INLINE readIR #-} - , AP.char '(' *> return (Function name)- , return (Ident name)- ]+accIR :: IntegerReader -> Integer -> IntegerReader+accIR (n, blocks, !cd) d+ | n < blockDigits = (n+1, blocks, cd*10 + d)+ | otherwise = (1, cd:blocks, d) +readIR :: IntegerReader -> Integer+readIR (_, [], cd) = cd+readIR (n, blocks, cd) =+ go startBase ((cd * padding):blocks) `div` padding+ where padding = 10^(blockDigits-n)+ go :: Integer -> [Integer] -> Integer+ go _ [] = 0+ go _ [x] = x+ go b xs = go (b*b) (combine b xs)+ combine :: Integer -> [Integer] -> [Integer]+ combine _ [] = []+ combine _ [x] = [x]+ combine b (x0:x1:xs) = x' : combine b xs+ where !x' = x0 + x1*b -parseEscapedIdentLike :: Parser Token-parseEscapedIdentLike = do- mbChar <- AP.peekChar- case mbChar of- Just '\\' -> parseIdentLike <|> (AP.anyChar >> return (Delim '\\'))- _ -> fail "parseEscapedIdentLike: Does not start with an escape code"+skipComment :: Text -> Text+skipComment t = case t of+ '*' :. '/' :. ts -> ts+ _ :. ts -> skipComment ts+ ts -> ts -parseAtKeyword :: Parser Token-parseAtKeyword = do- _ <- AP.char '@'- name <- parseName- return $ AtKeyword name+skipWhitespace :: Text -> Text+skipWhitespace t = case t of+ c :. ts+ | isWhitespace c -> skipWhitespace ts+ | otherwise -> t+ ts -> ts +parseTokens :: Text -> [Token]+parseTokens t0@(Text _ _ len) = snd $ A.run2 $ do+ dst <- A.new len+ dsta <- A.unsafeFreeze dst+ let go' !t d tgo = do+ ts <- inlineInterleaveST $ go d tgo+ return (t : ts)+ go d tgo = case tgo of+ c :. ts | isWhitespace c ->+ go' Whitespace d (skipWhitespace ts)+ '/' :. '*' :. ts -> go d (skipComment ts) -parseToken :: Parser Token-parseToken = AP.many' parseComment *> choice- [ parseWhitespace+ '<' :. '!' :. '-' :. '-' :. ts -> token CDO ts+ '-' :. '-' :. '>' :. ts -> token CDC ts - , AP.string "<!--" *> return CDO- , AP.string "-->" *> return CDC+ ',' :. ts -> token Comma ts+ ':' :. ts -> token Colon ts+ ';' :. ts -> token Semicolon ts+ '(' :. ts -> token LeftParen ts+ ')' :. ts -> token RightParen ts+ '[' :. ts -> token LeftSquareBracket ts+ ']' :. ts -> token RightSquareBracket ts+ '{' :. ts -> token LeftCurlyBracket ts+ '}' :. ts -> token RightCurlyBracket ts - , parseChar Comma ','- , parseChar Colon ':'- , parseChar Semicolon ';'- , parseChar LeftParen '('- , parseChar RightParen ')'- , parseChar LeftSquareBracket '['- , parseChar RightSquareBracket ']'- , parseChar LeftCurlyBracket '{'- , parseChar RightCurlyBracket '}'+ '$' :. '=' :. ts -> token SuffixMatch ts+ '*' :. '=' :. ts -> token SubstringMatch ts+ '^' :. '=' :. ts -> token PrefixMatch ts+ '|' :. '=' :. ts -> token DashMatch ts+ '~' :. '=' :. ts -> token IncludeMatch ts - , parseStr SuffixMatch "$="- , parseStr SubstringMatch "*="- , parseStr PrefixMatch "^="- , parseStr DashMatch "|="- , parseStr IncludeMatch "~="+ '|' :. '|' :. ts -> token Column ts - , parseStr Column "||"+ (parseNumericValue -> Just (repr, nv, ts))+ | '%' :. ts' <- ts ->+ go' (Percentage repr nv) d ts'+ | Just u <- parseName ts -> do+ (unit, d', ts') <- mkText dst d u+ go' (Dimension repr nv unit) d' ts'+ | otherwise ->+ go' (Number repr nv) d ts - , parseNumeric+ -- ident like+ (parseName -> Just n) -> do+ (name, d', ts) <- mkText dst d n+ if isUrl name then+ -- Special handling of url() functions (they are not really+ -- functions, they have their own Token type).+ case ts of+ '(' :. (skipWhitespace -> ts') ->+ case ts' of+ '"' :. _ -> go' (Function name) d' ts'+ '\'' :. _ -> go' (Function name) d' ts'+ _ -> parseUrl d' ts'+ _ -> go' (Ident name) d' ts+ else+ case ts of+ '(' :. ts' -> go' (Function name) d' ts'+ _ -> go' (Ident name) d' ts - , parseEscapedIdentLike- , parseIdentLike- , parseHash+ '"' :. ts -> parseString '"' d ts+ '\'' :. ts -> parseString '\'' d ts - , parseString '"'- , parseString '\''+ '@' :. (parseName -> Just n) -> do+ (name, d', ts) <- mkText dst d n+ go' (AtKeyword name) d' ts - , parseAtKeyword+ '#' :. (parseName -> Just n) -> do+ (name, d', ts) <- mkText dst d n+ go' (Hash HId name) d' ts - , AP.anyChar >>= return . Delim- ] <?> "token"+ '#' :. (satisfyOrEscaped nameCodePoint -> Just n) -> do+ (name, d', ts) <- mkText dst d (consumeName n)+ go' (Hash HUnrestricted name) d' ts + c :. ts ->+ token (Delim c) ts+ _ -> return [] + where token t ts = go' t d ts -isWhitespace :: Char -> Bool-isWhitespace '\x0009' = True-isWhitespace '\x000A' = True-isWhitespace '\x0020' = True-isWhitespace _ = False+ isUrl t@(Text _ _ 3)+ | u :. r :. l :. _ <- t =+ (u == 'u' || u == 'U') &&+ (r == 'r' || r == 'R') &&+ (l == 'l' || l == 'L')+ isUrl _ = False + -- https://drafts.csswg.org/css-syntax-3/#consume-string-token+ parseString endingCodePoint d0 = string d0+ where string d t = case t of+ c :. ts | c == endingCodePoint -> ret d ts+ '\\' :. ts+ | Just (p, ts') <- escapedCodePoint ts -> do+ d' <- p dst d+ string d' ts'+ | '\n' :. ts' <- ts ->+ string d ts'+ | Text _ _ 0 <- ts ->+ string d ts+ '\n' :. _ -> go' BadString d t+ c :. ts -> do+ write dst d c+ string (d+1) ts+ _ -> ret d t+ ret d t = go' (String $ Text dsta d0 (d-d0)) d t -isHexChar :: Char -> Bool-isHexChar ch- | ch >= '0' && ch <= '9' = True- | ch >= 'A' && ch <= 'F' = True- | ch >= 'a' && ch <= 'f' = True- | otherwise = False+ -- https://drafts.csswg.org/css-syntax/#consume-url-token+ parseUrl d0 tUrl = url d0 (skipWhitespace tUrl)+ where ret d ts = go' (Url (Text dsta d0 (d-d0))) d ts+ url d t = case t of+ ')' :. ts -> ret d ts+ c :. ts+ | c == '"' || c == '\'' || c == '('+ || nonPrintableCodePoint c -> do+ badUrl d ts+ | isWhitespace c ->+ whitespace d ts+ '\\' :. ts+ | Just (p, ts') <- escapedCodePoint ts -> do+ d' <- p dst d+ url d' ts'+ | otherwise ->+ badUrl d ts+ c :. ts -> do+ write dst d c+ url (d+1) ts+ _ ->+ ret d t+ whitespace d t = case t of+ c :. ts -> do+ if isWhitespace c then+ whitespace d ts+ else if c == ')' then+ ret d ts+ else+ badUrl d ts+ _ ->+ ret d t+ badUrl d t = case t of+ ')' :. ts -> go' BadUrl d ts+ (escapedCodePoint' -> Just (_, ts)) -> do+ badUrl d ts+ _ :. ts ->+ badUrl d ts+ _ -> go' BadUrl d t+ mkText :: A.MArray s -> Int -> Writer s -> ST s (Text, Int, Text)+ mkText dest d w = do+ (d', ts) <- w dest d+ return (Text dsta d (d' - d), d', ts) + r <- go 0 t0+ return (dst, r) -unhex :: (Functor m, Monad m) => String -> m Int-unhex = fmap toInt . go []- where - go :: Monad m => [Int] -> String -> m [Int]- go acc [] = return acc- go acc (a:r) = do- x <- c a- go (x:acc) r+isWhitespace :: Char -> Bool+isWhitespace '\x0009' = True+isWhitespace '\x000A' = True+isWhitespace '\x0020' = True+isWhitespace _ = False - toInt = sum . map (\(e, x) -> 16 ^ e * x) . zip [(0::Int)..]+nonPrintableCodePoint :: Char -> Bool+nonPrintableCodePoint c+ | c >= '\x0000' && c <= '\x0008' = True -- NULL through BACKSPACE+ | c == '\x000B' = True -- LINE TABULATION+ | c >= '\x000E' && c <= '\x001F' = True -- SHIFT OUT through INFORMATION SEPARATOR ONE+ | c == '\x007F' = True -- DELETE+ | otherwise = False - c :: Monad m => Char -> m Int- c '0' = return 0- c '1' = return 1- c '2' = return 2- c '3' = return 3- c '4' = return 4- c '5' = return 5- c '6' = return 6- c '7' = return 7- c '8' = return 8- c '9' = return 9- c 'A' = return 10- c 'B' = return 11- c 'C' = return 12- c 'D' = return 13- c 'E' = return 14- c 'F' = return 15- c 'a' = return 10- c 'b' = return 11- c 'c' = return 12- c 'd' = return 13- c 'e' = return 14- c 'f' = return 15- c _ = fail "Invalid hex digit!"+isExponent :: Char -> Bool+isExponent c = c == 'e' || c == 'E'
test/Test.hs view
@@ -1,15 +1,28 @@-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedStrings, ViewPatterns #-} module Main where +import qualified Data.Text as T import Data.Monoid import Data.CSS.Syntax.Tokens import Test.Hspec+import Test.Hspec.QuickCheck import Prelude+import Test.QuickCheck+import Data.Scientific +testTokenize :: HasCallStack => T.Text -> [Token] -> Expectation+testTokenize s t = do+ tokenize s `shouldBe` t+ tokenize (serialize (tokenize s)) `shouldBe` t +testSerialize :: HasCallStack => [Token] -> T.Text -> Expectation+testSerialize t s = do+ serialize t `shouldBe` s+ serialize (tokenize (serialize t)) `shouldBe` s + main :: IO () main = hspec spec @@ -18,213 +31,220 @@ describe "Data.CSS.Syntax.Tokens" $ do it "Single Character" $ do- tokenize "(" `shouldBe` Right [LeftParen]- tokenize ")" `shouldBe` Right [RightParen]- tokenize "[" `shouldBe` Right [LeftSquareBracket]- tokenize "]" `shouldBe` Right [RightSquareBracket]- tokenize ",," `shouldBe` Right [Comma, Comma]+ testTokenize "(" [LeftParen]+ testTokenize ")" [RightParen]+ testTokenize "[" [LeftSquareBracket]+ testTokenize "]" [RightSquareBracket]+ testTokenize ",," [Comma, Comma] it "Multiple Character" $ do- tokenize "~=" `shouldBe` Right [IncludeMatch]- tokenize "||" `shouldBe` Right [Column]- tokenize "|||" `shouldBe` Right [Column, Delim '|']- tokenize "<!--" `shouldBe` Right [CDO]- tokenize "<!---" `shouldBe` Right [CDO, Delim '-']- tokenize "<!---->" `shouldBe` Right [CDO, CDC]- tokenize "<!-- -->" `shouldBe` Right [CDO, Whitespace, CDC]+ testTokenize "~=" [IncludeMatch]+ testTokenize "||" [Column]+ testTokenize "|||" [Column, Delim '|']+ testTokenize "<!--" [CDO]+ testTokenize "<!---" [CDO, Delim '-']+ testTokenize "<!---->" [CDO, CDC]+ testTokenize "<!-- -->" [CDO, Whitespace, CDC] it "Delimiter" $ do- tokenize "^" `shouldBe` Right [Delim '^']- tokenize "|" `shouldBe` Right [Delim '|']- tokenize "\x7f" `shouldBe` Right [Delim '\x7f']- tokenize "\1" `shouldBe` Right [Delim '\x1']- tokenize "~-" `shouldBe` Right [Delim '~', Delim '-']- tokenize "*^" `shouldBe` Right [Delim '*', Delim '^']+ testTokenize "^" [Delim '^']+ testTokenize "|" [Delim '|']+ testTokenize "\x7f" [Delim '\x7f']+ testTokenize "\1" [Delim '\x1']+ testTokenize "~-" [Delim '~', Delim '-']+ testTokenize "*^" [Delim '*', Delim '^'] it "Whitespace" $ do- tokenize " " `shouldBe` Right [Whitespace]- tokenize "\n\rS" `shouldBe` Right [Whitespace, Ident "S"]- tokenize " *" `shouldBe` Right [Whitespace, Delim '*']- tokenize "\n\r\f2" `shouldBe` Right [Whitespace, Number "2" (NVInteger 2)]+ testTokenize " " [Whitespace]+ testTokenize "\n\rS" [Whitespace, Ident "S"]+ testTokenize " *" [Whitespace, Delim '*']+ testTokenize "\n\r\f2" [Whitespace, Number "2" (NVInteger 2)] it "Escapes" $ do- tokenize "hel\\6Co" `shouldBe` Right [Ident "hello"]- tokenize "\\26 B" `shouldBe` Right [Ident "&B"]- tokenize "'hel\\6c o'" `shouldBe` Right [String '\'' "hello"]- tokenize "'spac\\65\r\ns'" `shouldBe` Right [String '\'' "spaces"]- tokenize "spac\\65\r\ns" `shouldBe` Right [Ident "spaces"]- tokenize "spac\\65\n\rs" `shouldBe` Right [Ident "space", Whitespace, Ident "s"]- tokenize "sp\\61\tc\\65\fs" `shouldBe` Right [Ident "spaces"]- tokenize "hel\\6c o" `shouldBe` Right [Ident "hell", Whitespace, Ident "o"]- tokenize "test\\\n" `shouldBe` Right [Ident "test", Delim '\\', Whitespace]- tokenize "test\\D799" `shouldBe` Right [Ident "test\xD799"]- tokenize "\\E000" `shouldBe` Right [Ident "\xe000"]- tokenize "te\\s\\t" `shouldBe` Right [Ident "test"]- tokenize "spaces\\ in\\\tident" `shouldBe` Right [Ident "spaces in\tident"]- tokenize "\\.\\,\\:\\!" `shouldBe` Right [Ident ".,:!"]- tokenize "\\\r" `shouldBe` Right [Delim '\\', Whitespace]- tokenize "\\\f" `shouldBe` Right [Delim '\\', Whitespace]- tokenize "\\\r\n" `shouldBe` Right [Delim '\\', Whitespace]+ testTokenize "hel\\6Co" [Ident "hello"]+ testTokenize "\\26 B" [Ident "&B"]+ testTokenize "'hel\\6c o'" [String "hello"]+ testTokenize "'spac\\65\r\ns'" [String "spaces"]+ testTokenize "spac\\65\r\ns" [Ident "spaces"]+ testTokenize "spac\\65\n\rs" [Ident "space", Whitespace, Ident "s"]+ testTokenize "sp\\61\tc\\65\fs" [Ident "spaces"]+ testTokenize "hel\\6c o" [Ident "hell", Whitespace, Ident "o"]+ testTokenize "test\\\n" [Ident "test", Delim '\\', Whitespace]+ testTokenize "test\\D799" [Ident "test\xD799"]+ testTokenize "\\E000" [Ident "\xe000"]+ testTokenize "te\\s\\t" [Ident "test"]+ testTokenize "spaces\\ in\\\tident" [Ident "spaces in\tident"]+ testTokenize "\\.\\,\\:\\!" [Ident ".,:!"]+ testTokenize "\\\r" [Delim '\\', Whitespace]+ testTokenize "\\\f" [Delim '\\', Whitespace]+ testTokenize "\\\r\n" [Delim '\\', Whitespace] -- let replacement = "\xFFFD"- tokenize "null\\\0" `shouldBe` Right [Ident "null\xfffd"]- tokenize "null\\\0\0" `shouldBe` Right [Ident $ "null" <> "\xfffd" <> "\xfffd"]- tokenize "null\\0" `shouldBe` Right [Ident $ "null" <> "\xfffd"]- tokenize "null\\0000" `shouldBe` Right [Ident $ "null" <> "\xfffd"]- tokenize "large\\110000" `shouldBe` Right [Ident $ "large" <> "\xfffd"]- tokenize "large\\23456a" `shouldBe` Right [Ident $ "large" <> "\xfffd"]- tokenize "surrogate\\D800" `shouldBe` Right [Ident $ "surrogate" <> "\xfffd"]- tokenize "surrogate\\0DABC" `shouldBe` Right [Ident $ "surrogate" <> "\xfffd"]- tokenize "\\00DFFFsurrogate" `shouldBe` Right [Ident $ "\xfffd" <> "surrogate"]- tokenize "\\10fFfF" `shouldBe` Right [Ident "\x10ffff"]- tokenize "\\10fFfF0" `shouldBe` Right [Ident $ "\x10ffff" <> "0"]- tokenize "\\10000000" `shouldBe` Right [Ident $ "\x100000" <> "00"]- tokenize "eof\\" `shouldBe` Right [Ident "eof\xfffd"]+ testTokenize "null\\\0" [Ident "null\xfffd"]+ testTokenize "null\\\0\0" [Ident $ "null" <> "\xfffd" <> "\xfffd"]+ testTokenize "null\\0" [Ident $ "null" <> "\xfffd"]+ testTokenize "null\\0000" [Ident $ "null" <> "\xfffd"]+ testTokenize "large\\110000" [Ident $ "large" <> "\xfffd"]+ testTokenize "large\\23456a" [Ident $ "large" <> "\xfffd"]+ testTokenize "surrogate\\D800" [Ident $ "surrogate" <> "\xfffd"]+ testTokenize "surrogate\\0DABC" [Ident $ "surrogate" <> "\xfffd"]+ testTokenize "\\00DFFFsurrogate" [Ident $ "\xfffd" <> "surrogate"]+ testTokenize "\\10fFfF" [Ident "\x10ffff"]+ testTokenize "\\10fFfF0" [Ident $ "\x10ffff" <> "0"]+ testTokenize "\\10000000" [Ident $ "\x100000" <> "00"]+ testTokenize "eof\\" [Ident "eof", Delim '\\'] it "Ident" $ do- tokenize "simple-ident" `shouldBe` Right [Ident "simple-ident"]- tokenize "testing123" `shouldBe` Right [Ident "testing123"]- tokenize "hello!" `shouldBe` Right [Ident "hello", Delim '!']- tokenize "world\5" `shouldBe` Right [Ident "world", Delim '\5']- tokenize "_under score" `shouldBe` Right [Ident "_under", Whitespace, Ident "score"]- tokenize "-_underscore" `shouldBe` Right [Ident "-_underscore"]- tokenize "-text" `shouldBe` Right [Ident "-text"]- tokenize "-\\6d" `shouldBe` Right [Ident "-m"]- tokenize "--abc" `shouldBe` Right [Ident "--abc"]- tokenize "--" `shouldBe` Right [Ident "--"]- tokenize "--11" `shouldBe` Right [Ident "--11"]- tokenize "---" `shouldBe` Right [Ident "---"]- tokenize "\x2003" `shouldBe` Right [Ident "\x2003"] -- em-space- tokenize "\xA0" `shouldBe` Right [Ident "\xA0"] -- non-breaking space- tokenize "\x1234" `shouldBe` Right [Ident "\x1234"]- tokenize "\x12345" `shouldBe` Right [Ident "\x12345"]- tokenize "\0" `shouldBe` Right [Ident "\xfffd"]- tokenize "ab\0c" `shouldBe` Right [Ident $ "ab\xfffd" <> "c"]+ testTokenize "simple-ident" [Ident "simple-ident"]+ testTokenize "testing123" [Ident "testing123"]+ testTokenize "hello!" [Ident "hello", Delim '!']+ testTokenize "world\5" [Ident "world", Delim '\5']+ testTokenize "_under score" [Ident "_under", Whitespace, Ident "score"]+ testTokenize "-_underscore" [Ident "-_underscore"]+ testTokenize "-text" [Ident "-text"]+ testTokenize "-\\6d" [Ident "-m"]+ testTokenize "--abc" [Ident "--abc"]+ testTokenize "--" [Ident "--"]+ testTokenize "--11" [Ident "--11"]+ testTokenize "---" [Ident "---"]+ testTokenize "\x2003" [Ident "\x2003"] -- em-space+ testTokenize "\xA0" [Ident "\xA0"] -- non-breaking space+ testTokenize "\x1234" [Ident "\x1234"]+ testTokenize "\x12345" [Ident "\x12345"]+ testTokenize "\0" [Ident "\xfffd"]+ testTokenize "ab\0c" [Ident $ "ab\xfffd" <> "c"] it "Function" $ do- tokenize "scale(2)" `shouldBe` Right [Function "scale", Number "2" (NVInteger 2), RightParen]- tokenize "foo-bar\\ baz(" `shouldBe` Right [Function "foo-bar baz"]- tokenize "fun\\(ction(" `shouldBe` Right [Function "fun(ction"]- tokenize "-foo(" `shouldBe` Right [Function "-foo"]- tokenize "url(\"foo.gif\"" `shouldBe` Right [Function "url", String '"' "foo.gif"]- tokenize "foo( \'bar.gif\'" `shouldBe` Right [Function "foo", Whitespace, String '\'' "bar.gif"]+ testTokenize "scale(2)" [Function "scale", Number "2" (NVInteger 2), RightParen]+ testTokenize "foo-bar\\ baz(" [Function "foo-bar baz"]+ testTokenize "fun\\(ction(" [Function "fun(ction"]+ testTokenize "-foo(" [Function "-foo"]+ testTokenize "url(\"foo.gif\"" [Function "url", String "foo.gif"]+ testTokenize "foo( \'bar.gif\'" [Function "foo", Whitespace, String "bar.gif"] -- // To simplify implementation we drop the whitespace in function(url),whitespace,string()- tokenize "url( \'bar.gif\'" `shouldBe` Right [Function "url", String '\'' "bar.gif"]+ testTokenize "url( \'bar.gif\'" [Function "url", String "bar.gif"] it "AtKeyword" $ do- tokenize "@at-keyword" `shouldBe` Right [AtKeyword "at-keyword"]- tokenize "@hello!" `shouldBe` Right [AtKeyword "hello", Delim '!']- tokenize "@-text" `shouldBe` Right [AtKeyword "-text"]- tokenize "@--abc" `shouldBe` Right [AtKeyword "--abc"]- tokenize "@--" `shouldBe` Right [AtKeyword "--"]- tokenize "@--11" `shouldBe` Right [AtKeyword "--11"]- tokenize "@---" `shouldBe` Right [AtKeyword "---"]- tokenize "@\\ " `shouldBe` Right [AtKeyword " "]- tokenize "@-\\ " `shouldBe` Right [AtKeyword "- "]- tokenize "@@" `shouldBe` Right [ Delim '@', Delim '@']- -- tokenize "@2" `shouldBe` Right [ Delim '@', Number "2" (NVInteger 2)]- -- tokenize "@-1" `shouldBe` Right [ Delim '@', Number "-1" (NVInteger (-1))]+ testTokenize "@at-keyword" [AtKeyword "at-keyword"]+ testTokenize "@hello!" [AtKeyword "hello", Delim '!']+ testTokenize "@-text" [AtKeyword "-text"]+ testTokenize "@--abc" [AtKeyword "--abc"]+ testTokenize "@--" [AtKeyword "--"]+ testTokenize "@--11" [AtKeyword "--11"]+ testTokenize "@---" [AtKeyword "---"]+ testTokenize "@\\ " [AtKeyword " "]+ testTokenize "@-\\ " [AtKeyword "- "]+ testTokenize "@@" [Delim '@', Delim '@']+ testTokenize "@2" [Delim '@', Number "2" (NVInteger 2)]+ testTokenize "@-1" [Delim '@', Number "-1" (NVInteger (-1))] it "Url" $ do- tokenize "url(foo.gif)" `shouldBe` Right [Url "foo.gif"]- tokenize "urL(https://example.com/cats.png)" `shouldBe` Right [Url "https://example.com/cats.png"]- tokenize "uRl(what-a.crazy^URL~this\\ is!)" `shouldBe` Right [Url "what-a.crazy^URL~this is!"]- tokenize "uRL(123#test)" `shouldBe` Right [Url "123#test"]- tokenize "Url(escapes\\ \\\"\\'\\)\\()" `shouldBe` Right [Url "escapes \"')("]- tokenize "UrL( whitespace )" `shouldBe` Right [Url "whitespace"]- tokenize "URl( whitespace-eof " `shouldBe` Right [Url "whitespace-eof"]- tokenize "URL(eof" `shouldBe` Right [Url "eof"]- tokenize "url(not/*a*/comment)" `shouldBe` Right [Url "not/*a*/comment"]- tokenize "urL()" `shouldBe` Right [Url ""]- tokenize "uRl(white space)," `shouldBe` Right [BadUrl "white space", Comma]- tokenize "Url(b(ad)," `shouldBe` Right [BadUrl "b(ad", Comma]- tokenize "uRl(ba'd):" `shouldBe` Right [BadUrl "ba'd", Colon]- tokenize "urL(b\"ad):" `shouldBe` Right [BadUrl "b\"ad", Colon]- tokenize "uRl(b\"ad):" `shouldBe` Right [BadUrl "b\"ad", Colon]- tokenize "Url(b\\\rad):" `shouldBe` Right [BadUrl "b\\\nad", Colon]- tokenize "url(b\\\nad):" `shouldBe` Right [BadUrl "b\\\nad", Colon]- tokenize "url(/*'bad')*/" `shouldBe` Right [BadUrl "/*'bad'", Delim '*', Delim '/']- tokenize "url(ba'd\\\\))" `shouldBe` Right [BadUrl "ba'd\\", RightParen]+ testTokenize "url(foo.gif)" [Url "foo.gif"]+ testTokenize "urL(https://example.com/cats.png)" [Url "https://example.com/cats.png"]+ testTokenize "uRl(what-a.crazy^URL~this\\ is!)" [Url "what-a.crazy^URL~this is!"]+ testTokenize "uRL(123#test)" [Url "123#test"]+ testTokenize "Url(escapes\\ \\\"\\'\\)\\()" [Url "escapes \"')("]+ testTokenize "UrL( whitespace )" [Url "whitespace"]+ testTokenize "URl( whitespace-eof " [Url "whitespace-eof"]+ testTokenize "URL(eof" [Url "eof"]+ testTokenize "url(not/*a*/comment)" [Url "not/*a*/comment"]+ testTokenize "urL()" [Url ""]+ testTokenize "uRl(white space)," [BadUrl, Comma]+ testTokenize "Url(b(ad)," [BadUrl, Comma]+ testTokenize "uRl(ba'd):" [BadUrl, Colon]+ testTokenize "urL(b\"ad):" [BadUrl, Colon]+ testTokenize "uRl(b\"ad):" [BadUrl, Colon]+ testTokenize "Url(b\\\rad):" [BadUrl, Colon]+ testTokenize "url(b\\\nad):" [BadUrl, Colon]+ testTokenize "url(/*'bad')*/" [BadUrl, Delim '*', Delim '/']+ testTokenize "url(ba'd\\\\))" [BadUrl, RightParen] it "String" $ do- tokenize "'text'" `shouldBe` Right [String '\'' "text"]- tokenize "\"text\"" `shouldBe` Right [String '"' "text"]- tokenize "'testing, 123!'" `shouldBe` Right [String '\'' "testing, 123!"]- tokenize "'es\\'ca\\\"pe'" `shouldBe` Right [String '\'' "es'ca\"pe"]- tokenize "'\"quotes\"'" `shouldBe` Right [String '\'' "\"quotes\""]- tokenize "\"'quotes'\"" `shouldBe` Right [String '"' "'quotes'"]- tokenize "\"mismatch'" `shouldBe` Right [String '"' "mismatch'"]- tokenize "'text\5\t\xb'" `shouldBe` Right [String '\'' "text\5\t\xb"]- tokenize "\"end on eof" `shouldBe` Right [String '"' "end on eof"]- tokenize "'esca\\\nped'" `shouldBe` Right [String '\'' "escaped"]- tokenize "\"esc\\\faped\"" `shouldBe` Right [String '"' "escaped"]- tokenize "'new\\\rline'" `shouldBe` Right [String '\'' "newline"]- tokenize "\"new\\\r\nline\"" `shouldBe` Right [String '"' "newline"]- tokenize "'bad\nstring" `shouldBe` Right [BadString '\'' "bad", Whitespace, Ident "string"]- tokenize "'bad\rstring" `shouldBe` Right [BadString '\'' "bad", Whitespace, Ident "string"]- tokenize "'bad\r\nstring" `shouldBe` Right [BadString '\'' "bad", Whitespace, Ident "string"]- tokenize "'bad\fstring" `shouldBe` Right [BadString '\'' "bad", Whitespace, Ident "string"]- tokenize "'\0'" `shouldBe` Right [String '\'' "\xFFFD"]- tokenize "'hel\0lo'" `shouldBe` Right [String '\'' "hel\xfffdlo"]- tokenize "'h\\65l\0lo'" `shouldBe` Right [String '\'' "hel\xfffdlo"]+ testTokenize "'text'" [String "text"]+ testTokenize "\"text\"" [String "text"]+ testTokenize "'testing, 123!'" [String "testing, 123!"]+ testTokenize "'es\\'ca\\\"pe'" [String "es'ca\"pe"]+ testTokenize "'\"quotes\"'" [String "\"quotes\""]+ testTokenize "\"'quotes'\"" [String "'quotes'"]+ testTokenize "\"mismatch'" [String "mismatch'"]+ testTokenize "'text\5\t\xb'" [String "text\5\t\xb"]+ testTokenize "\"end on eof" [String "end on eof"]+ testTokenize "'esca\\\nped'" [String "escaped"]+ testTokenize "\"esc\\\faped\"" [String "escaped"]+ testTokenize "'new\\\rline'" [String "newline"]+ testTokenize "\"new\\\r\nline\"" [String "newline"]+ testTokenize "'bad\nstring" [BadString, Whitespace, Ident "string"]+ testTokenize "'bad\rstring" [BadString, Whitespace, Ident "string"]+ testTokenize "'bad\r\nstring" [BadString, Whitespace, Ident "string"]+ testTokenize "'bad\fstring" [BadString, Whitespace, Ident "string"]+ testTokenize "'\0'" [String "\xFFFD"]+ testTokenize "'hel\0lo'" [String "hel\xfffdlo"]+ testTokenize "'h\\65l\0lo'" [String "hel\xfffdlo"]+ testTokenize "'ignore backslash at eof\\" [String "ignore backslash at eof"] it "Hash" $ do- tokenize "#id-selector" `shouldBe` Right [Hash HId "id-selector"]- tokenize "#FF7700" `shouldBe` Right [Hash HId "FF7700"]- -- tokenize "#3377FF" `shouldBe` Right [Hash HUnrestricted "3377FF"]- tokenize "#\\ " `shouldBe` Right [Hash HId " "]- tokenize "# " `shouldBe` Right [Delim '#', Whitespace]- tokenize "#\\\n" `shouldBe` Right [Delim '#', Delim '\\', Whitespace]- tokenize "#\\\r\n" `shouldBe` Right [Delim '#', Delim '\\', Whitespace]- tokenize "#!" `shouldBe` Right [Delim '#', Delim '!']+ testTokenize "#id-selector" [Hash HId "id-selector"]+ testTokenize "#FF7700" [Hash HId "FF7700"]+ testTokenize "#3377FF" [Hash HUnrestricted "3377FF"]+ testTokenize "#\\ " [Hash HId " "]+ testTokenize "# " [Delim '#', Whitespace]+ testTokenize "#\\\n" [Delim '#', Delim '\\', Whitespace]+ testTokenize "#\\\r\n" [Delim '#', Delim '\\', Whitespace]+ testTokenize "#!" [Delim '#', Delim '!'] it "Number" $ do- tokenize "10" `shouldBe` Right [Number "10" (NVInteger 10)]- tokenize "12.0" `shouldBe` Right [Number "12.0" (NVNumber 12)]- tokenize "+45.6" `shouldBe` Right [Number "+45.6" (NVNumber 45.6)]- tokenize "-7" `shouldBe` Right [Number "-7" (NVInteger (-7))]- tokenize "010" `shouldBe` Right [Number "010" (NVInteger 10)]- tokenize "10e0" `shouldBe` Right [Number "10e0" (NVNumber 10)]- tokenize "12e3" `shouldBe` Right [Number "12e3" (NVNumber 12000)]- tokenize "3e+1" `shouldBe` Right [Number "3e+1" (NVNumber 30)]- tokenize "12E-1" `shouldBe` Right [Number "12E-1" (NVNumber 1.2)]- tokenize ".7" `shouldBe` Right [Number ".7" (NVNumber 0.7)]- tokenize "-.3" `shouldBe` Right [Number "-.3" (NVNumber (-0.3))]- tokenize "+637.54e-2" `shouldBe` Right [Number "+637.54e-2" (NVNumber 6.3754)]- tokenize "-12.34E+2" `shouldBe` Right [Number "-12.34E+2" (NVNumber (-1234))]- tokenize "+ 5" `shouldBe` Right [Delim '+', Whitespace, Number "5" (NVInteger 5)]- tokenize "-+12" `shouldBe` Right [Delim '-', Number "+12" (NVInteger 12)]- tokenize "+-21" `shouldBe` Right [Delim '+', Number "-21" (NVInteger (-21))]- tokenize "++22" `shouldBe` Right [Delim '+', Number "+22" (NVInteger 22)]- tokenize "13." `shouldBe` Right [Number "13" (NVInteger 13), Delim ('.')]- tokenize "1.e2" `shouldBe` Right [Number "1" (NVInteger 1), Delim '.', Ident "e2"]- tokenize "2e3.5" `shouldBe` Right [Number "2e3" (NVNumber 2e3), Number ".5" (NVNumber 0.5)]- tokenize "2e3." `shouldBe` Right [Number "2e3" (NVNumber 2e3), Delim '.']- tokenize "1000000000000000000000000" `shouldBe` Right [Number "1000000000000000000000000" (NVInteger 1e24)]+ testTokenize "10" [Number "10" (NVInteger 10)]+ testTokenize "12.0" [Number "12.0" (NVNumber 12)]+ testTokenize "+45.6" [Number "+45.6" (NVNumber 45.6)]+ testTokenize "-7" [Number "-7" (NVInteger (-7))]+ testTokenize "010" [Number "010" (NVInteger 10)]+ testTokenize "10e0" [Number "10e0" (NVNumber 10)]+ testTokenize "12e3" [Number "12e3" (NVNumber 12000)]+ testTokenize "3e+1" [Number "3e+1" (NVNumber 30)]+ testTokenize "12E-1" [Number "12E-1" (NVNumber 1.2)]+ testTokenize ".7" [Number ".7" (NVNumber 0.7)]+ testTokenize "-.3" [Number "-.3" (NVNumber (-0.3))]+ testTokenize "+637.54e-2" [Number "+637.54e-2" (NVNumber 6.3754)]+ testTokenize "-12.34E+2" [Number "-12.34E+2" (NVNumber (-1234))]+ testTokenize "+ 5" [Delim '+', Whitespace, Number "5" (NVInteger 5)]+ testTokenize "-+12" [Delim '-', Number "+12" (NVInteger 12)]+ testTokenize "+-21" [Delim '+', Number "-21" (NVInteger (-21))]+ testTokenize "++22" [Delim '+', Number "+22" (NVInteger 22)]+ testTokenize "13." [Number "13" (NVInteger 13), Delim ('.')]+ testTokenize "1.e2" [Number "1" (NVInteger 1), Delim '.', Ident "e2"]+ testTokenize "2e3.5" [Number "2e3" (NVNumber 2e3), Number ".5" (NVNumber 0.5)]+ testTokenize "2e3." [Number "2e3" (NVNumber 2e3), Delim '.']+ testTokenize "1000000000000000000000000" [Number "1000000000000000000000000" (NVInteger 1000000000000000000000000)]+ testTokenize "123456789223456789323456789423456789523456789623456789723456789823456789923456789" [Number "123456789223456789323456789423456789523456789623456789723456789823456789923456789" (NVInteger 123456789223456789323456789423456789523456789623456789723456789823456789923456789)]+ testTokenize "1.797693134862315708145274237317043567980705675258449965989174768031572607800285387605895586327668781715404589535143824642e308" [Number "1.797693134862315708145274237317043567980705675258449965989174768031572607800285387605895586327668781715404589535143824642e308" (NVNumber 1.797693134862315708145274237317043567980705675258449965989174768031572607800285387605895586327668781715404589535143824642e308)] it "Dimension" $ do- tokenize "10px" `shouldBe` Right [Dimension "10" (NVInteger 10) "px"]- tokenize "12.0em" `shouldBe` Right [Dimension "12.0" (NVNumber 12) "em"]- tokenize "-12.0em" `shouldBe` Right [Dimension "-12.0" (NVNumber (-12)) "em"]- tokenize "+45.6__qem" `shouldBe` Right [Dimension "+45.6" (NVNumber 45.6) "__qem"]- tokenize "5e" `shouldBe` Right [Dimension "5" (NVInteger 5) "e"]- tokenize "5px-2px" `shouldBe` Right [Dimension "5" (NVInteger 5) "px-2px"]- tokenize "5e-" `shouldBe` Right [Dimension "5" (NVInteger 5) "e-"]- tokenize "5\\ " `shouldBe` Right [Dimension "5" (NVInteger 5) " "]- tokenize "40\\70\\78" `shouldBe` Right [Dimension "40" (NVInteger 40) "px"]- tokenize "4e3e2" `shouldBe` Right [Dimension "4e3" (NVNumber 4e3) "e2"]- tokenize "0x10px" `shouldBe` Right [Dimension "0" (NVInteger 0) "x10px"]- tokenize "4unit " `shouldBe` Right [Dimension "4" (NVInteger 4) "unit", Whitespace]- tokenize "5e+" `shouldBe` Right [Dimension "5" (NVInteger 5) "e", Delim '+']- tokenize "2e.5" `shouldBe` Right [Dimension "2" (NVInteger 2) "e", Number ".5" (NVNumber 0.5)]- tokenize "2e+.5" `shouldBe` Right [Dimension "2" (NVInteger 2) "e", Number "+.5" (NVNumber 0.5)]+ testTokenize "10px" [Dimension "10" (NVInteger 10) "px"]+ testTokenize "12.0em" [Dimension "12.0" (NVNumber 12) "em"]+ testTokenize "-12.0em" [Dimension "-12.0" (NVNumber (-12)) "em"]+ testTokenize "+45.6__qem" [Dimension "+45.6" (NVNumber 45.6) "__qem"]+ testTokenize "5e" [Dimension "5" (NVInteger 5) "e"]+ testTokenize "5px-2px" [Dimension "5" (NVInteger 5) "px-2px"]+ testTokenize "5e-" [Dimension "5" (NVInteger 5) "e-"]+ testTokenize "5\\ " [Dimension "5" (NVInteger 5) " "]+ testTokenize "40\\70\\78" [Dimension "40" (NVInteger 40) "px"]+ testTokenize "4e3e2" [Dimension "4e3" (NVNumber 4e3) "e2"]+ testTokenize "0x10px" [Dimension "0" (NVInteger 0) "x10px"]+ testTokenize "4unit " [Dimension "4" (NVInteger 4) "unit", Whitespace]+ testTokenize "5e+" [Dimension "5" (NVInteger 5) "e", Delim '+']+ testTokenize "2e.5" [Dimension "2" (NVInteger 2) "e", Number ".5" (NVNumber 0.5)]+ testTokenize "2e+.5" [Dimension "2" (NVInteger 2) "e", Number "+.5" (NVNumber 0.5)]+ testTokenize "1-2" [Number "1" (NVInteger 1), Number "-2" (NVInteger (-2))]+ testTokenize "1\\65 1" [Dimension "1" (NVInteger 1) "e1"]+ testTokenize "1\\31 em" [Dimension "1" (NVInteger 1) "1em"]+ testTokenize "1e\\31 em" [Dimension "1" (NVInteger 1) "e1em"] it "Percentage" $ do- tokenize "10%" `shouldBe` Right [Percentage "10" $ NVInteger 10]- tokenize "+12.0%" `shouldBe` Right [Percentage "+12.0" $ NVNumber 12.0]- tokenize "-48.99%" `shouldBe` Right [Percentage "-48.99" $ NVNumber (-48.99)]- tokenize "6e-1%" `shouldBe` Right [Percentage "6e-1" $ NVNumber 6e-1]- tokenize "5%%" `shouldBe` Right [Percentage "5" (NVInteger 5), Delim '%']+ testTokenize "10%" [Percentage "10" $ NVInteger 10]+ testTokenize "+12.0%" [Percentage "+12.0" $ NVNumber 12.0]+ testTokenize "-48.99%" [Percentage "-48.99" $ NVNumber (-48.99)]+ testTokenize "6e-1%" [Percentage "6e-1" $ NVNumber 6e-1]+ testTokenize "5%%" [Percentage "5" (NVInteger 5), Delim '%'] -- 402 TEST(CSSTokenizerTest, UnicodeRangeToken)@@ -251,10 +271,125 @@ -- 423 TEST_TOKENS("u+-543", ident("u"), delim('+'), number(IntegerValueType, -543, MinusSign)); it "Comment" $ do- tokenize "/*comment*/a" `shouldBe` Right [Ident "a"]- tokenize "/**\\2f**//" `shouldBe` Right [Delim '/']- tokenize "/**y*a*y**/ " `shouldBe` Right [Whitespace]- tokenize ",/* \n :) \n */)" `shouldBe` Right [Comma, RightParen]- tokenize ":/*/*/" `shouldBe` Right [Colon]- tokenize "/**/*" `shouldBe` Right [Delim '*']- tokenize ";/******" `shouldBe` Right [Semicolon]+ testTokenize "/*comment*/a" [Ident "a"]+ testTokenize "/**\\2f**//" [Delim '/']+ testTokenize "/**y*a*y**/ " [Whitespace]+ testTokenize ",/* \n :) \n */)" [Comma, RightParen]+ testTokenize ":/*/*/" [Colon]+ testTokenize "/**/*" [Delim '*']+ testTokenize ";/******" [Semicolon]++ it "Serialization" $ do+ testSerialize [String "hello,\x0world"] "\"hello,\xFFFDworld\""+ testSerialize [String "\x10\x7f\""] "\"\\10 \\7f \\\"\""+ testSerialize [String "\xABCDE"] "\"\xABCDE\""+ testSerialize [Ident "-"] "\\-"+ testSerialize [Ident "5"] "\\35 "+ testSerialize [Ident "-5"] "-\\35 "+ testSerialize [Ident "-9f\\oo()"] "-\\39 f\\\\oo\\(\\)"+ testSerialize [Ident "\xABCDE"] "\xABCDE"+ testSerialize [Ident "a", Ident "b"] "a/**/b"+ testSerialize [Dimension "1" (NVInteger 1) "em", Ident "b"] "1em/**/b"++ it "Doesn't regress Tokenize=>serialize=>tokenize roundtrip" $ do+ let a = tokenize "\\-->"+ tokenize (serialize a) `shouldBe` a++ modifyMaxSize (const 500) $ modifyMaxSuccess (const 100000) $+ prop "Tokenize=>serialize=>tokenize roundtrip"+ prop_tstRoundTrip+ modifyMaxSize (const 50) $ modifyMaxSuccess (const 100000) $+ prop "Serialize=>tokenize roundtrip"+ prop_stRoundTrip++prop_tstRoundTrip :: String -> Bool+prop_tstRoundTrip s = tokenize (serialize t) == t+ where t = tokenize $ T.pack s++prop_stRoundTrip :: TestTokens -> Bool+prop_stRoundTrip (TestTokens t) = tokenize (serialize t) == t++newtype TestTokens = TestTokens [Token]+ deriving (Show, Eq)++instance Arbitrary TestTokens where+ arbitrary = TestTokens . go <$> arbitrary+ where go [] = []+ go (x : xs@(Whitespace : _)) | needWhitespace x = x : go xs+ -- we can only have [BadString, Whitespace]+ -- or [Delim '\\', Whitespace] sequences+ go (x : xs) | needWhitespace x = x : Whitespace : go xs+ go (x@(Function (T.toLower -> "url")) : xs) =+ x : String "argument" : go xs+ go (x : xs) = x : go xs+ needWhitespace x = case x of+ BadString -> True+ Delim '\\' -> True+ _ -> False+++instance Arbitrary Token where+ arbitrary = oneof $+ map return+ [ Whitespace+ , CDO+ , CDC++ , Comma+ , Colon+ , Semicolon++ , LeftParen+ , RightParen+ , LeftSquareBracket+ , RightSquareBracket+ , LeftCurlyBracket+ , RightCurlyBracket++ , SuffixMatch+ , SubstringMatch+ , PrefixMatch+ , DashMatch+ , IncludeMatch++ , Column++ , BadString+ , BadUrl+ ]+ +++ [ String <$> text+ , num Number+ , num Percentage+ , num Dimension <*> ident++ , Url <$> text++ , Ident <$> ident++ , AtKeyword <$> ident++ , Function <$> ident+ , return $ Function "url"++ , Hash HId <$> ident++ , Delim <$> elements possibleDelimiters+ ]+ where ident = notEmpty text+ notEmpty x = do+ r <- x+ if r /= "" then return r else notEmpty x+ text = T.replace "\NUL" "\xFFFD" . T.pack <$> arbitrary+ num token = do+ c <- arbitrary+ e <- arbitrary+ let (t, n)+ | e /= 0 = nv NVNumber (scientific c e)+ | otherwise = nv NVInteger c+ nv f x = (T.pack $ show x, f x)+ return $ token t n+ possibleDelimiters =+ [d | c <- ['\0'..'\xff']+ , [Delim d] <- [tokenize (T.pack [c])]]+ ++ ['\\']