packages feed

mangrove-0.1.0.0: test/Test/Mangrove/Html5Lib/Tokenizer/JSON.hs

{-# LANGUAGE OverloadedStrings #-}

{-# OPTIONS_GHC -Wno-orphans #-}

module Test.Mangrove.Html5Lib.Tokenizer.JSON
    ( JsonTest ( .. )
    , TestError ( .. )
    , parseTestFile
    ) where


import qualified Data.Aeson as J
import qualified Data.Aeson.Types as J
import qualified Data.ByteString as BS
import qualified Data.ByteString.Builder as BS.B
import qualified Data.ByteString.Lazy as BS.L
import qualified Data.ByteString.Short as BS.S
import qualified Data.HashMap.Strict as M
import qualified Data.List as L
import qualified Data.Maybe as Y
import qualified Data.Text as T

import qualified Numeric as N

import qualified Text.Read as R

import Web.Mangrove.Parse.Common.Error
import Web.Mangrove.Parse.Tokenize
import Web.Willow.DOM hiding ( Node ( .. ) )

import Test.Mangrove.Html5Lib.Common

import Data.Aeson ( (.:), (.:?), (.!=) )
import Data.Vector ( (!?) )
import System.FilePath ( (</>), (<.>) )


testFile :: FilePath -> IO FilePath
testFile f = (</> f) <$> dataFile "tokenizer"

parseTestFile :: FilePath -> IO [JsonTest]
parseTestFile p = testFile (p <.> "test") >>= J.eitherDecodeFileStrict >>= either fail rewrap
  where rewrap (TestFile ts) = return ts


data JsonTest = JsonTest
    { description :: T.Text
    , input :: BS.ByteString
    , output :: [[Token]]
    , initialStates :: [CurrentTokenizerState]
    , lastStartTag :: Maybe T.Text
    , errors :: [TestError]
    }
  deriving ( Eq, Show, Read )

instance J.FromJSON JsonTest where
    parseJSON = J.withObject "test" $ \v -> do
        doubleEscaped <- v .:? "doubleEscaped" .!= False
        JsonTest
            <$> v .: "description"
            <*> fmap (BS.L.toStrict . BS.B.toLazyByteString . BS.B.stringUtf8 . unescape doubleEscaped) (v .: "input")
            <*> fmap (map $ unpackTokens doubleEscaped) (v .: "output")
            <*> v .:? "initialStates" .!= [DataState]
            <*> v .:? "lastStartTag"
            <*> fmap (L.sortOn (show . errorCode)) (v .:? "errors" .!= [])
      where unpackTokens doubleEscaped (TestToken (Right (Comment str))) =
                [Comment . T.pack $ unescape doubleEscaped str]
            unpackTokens _ (TestToken (Right tok)) = [tok]
            unpackTokens doubleEscaped (TestToken (Left str)) =
                map Character . unescape doubleEscaped $ T.pack str
            unescape False = T.unpack
            unescape True = unescape' . T.splitOn "\\u"
            unescape' [] = ""
            unescape' (t:ts) = T.unpack t ++ concatMap unescapeCode ts
            unescapeCode t = maybe '\NUL' (toEnum . fst) (Y.listToMaybe $ N.readHex code) : ts
              where (code, ts) = splitAt 4 $ T.unpack t


data TestError = TestError
    { errorCode :: ParseError
    , line :: Word
    , column :: Word
    }
  deriving ( Eq, Show, Read )

instance J.FromJSON TestError where
    parseJSON = J.withObject "error" $ \v -> TestError
        <$> v .: "code"
        <*> v .: "line"
        <*> v .: "col"


instance J.FromJSON CurrentTokenizerState where
    parseJSON = J.withText "initialState" $ \v -> case v of
        "Data state" -> return DataState
        "PLAINTEXT state" -> return PlainTextState
        "RCDATA state" -> return RCDataState
        "RAWTEXT state" -> return RawTextState
        "Script data state" -> return ScriptDataState
        "CDATA section state" -> return CDataState
        _ -> fail $ "Unknown tokenizer state '" ++ T.unpack v ++ "'"


newtype TestToken = TestToken (Either String Token)
  deriving ( Eq, Show, Read )

instance J.FromJSON TestToken where
    parseJSON = J.withArray "output" $ \v -> case v !? 0 of
        Just "DOCTYPE" -> do
            name <- maybe (return Nothing) parseTextOrNull $ v !? 1
            public <- maybe (return Nothing) parseTextOrNull $ v !? 2
            system <- maybe (return Nothing) parseTextOrNull $ v !? 3
            correctness <- maybe (return True) parseBool $ v !? 4
            return . TestToken . Right . Doctype $ emptyDoctypeParams
                { doctypeName = name
                , doctypePublicId = public
                , doctypeSystemId = system
                , doctypeQuirks = not correctness
                }
        Just "StartTag" -> do
            name <- maybe (return T.empty) parseText $ v !? 1
            attrs <- maybe (return []) parseAttributes $ v !? 2
            selfClosing <- maybe (return False) parseBool $ v !? 3
            return . TestToken . Right . StartTag $ emptyTagParams
                { tagName = name
                , tagIsSelfClosing = selfClosing
                , tagAttributes = M.fromList attrs
                }
        Just "EndTag" -> J.withText "EndTag" (return . TestToken . Right . packEndTag) $
            Y.fromMaybe (J.String T.empty) (v !? 1)
        Just "Comment" -> J.withText "Comment" (return . TestToken . Right . Comment) $
            Y.fromMaybe (J.String T.empty) (v !? 1)
        Just "Character" -> J.withText "Character" (return . TestToken . Left . T.unpack) $
            Y.fromMaybe (J.String T.empty) (v !? 1)
        Just _ -> fail $ "Unknown output token"
        Nothing -> fail $ "Empty output token"
      where packEndTag name = EndTag $ emptyTagParams
                { tagName = name
                }
            parseText = J.withText "text" return
            parseTextOrNull (J.String t) = return $ Just t
            parseTextOrNull J.Null = return Nothing
            parseTextOrNull v = J.typeMismatch "String or Null" v
            parseBool = J.withBool "bool" return
            parseAttributes = J.withObject "attributes" $ fmap M.toList . mapM parseText


instance J.FromJSON ParseError where
    parseJSON = J.withText "code" $ \v -> case v of
        "character-reference-outside-unicode-range" ->
            return $ CharacterReferenceOutsideUnicodeRange 0
        "duplicate-attribute" -> return $ DuplicateAttribute (T.empty, T.empty)
        "duplicate-singleton-element" -> return $ DuplicateSingletonElement emptyElementParams
        "frameset-in-body" -> return $ FramesetInBody emptyElementParams
        "malformed-table-structure" -> return $ MalformedTableStructure emptyElementParams
        "invalid-byte-sequence" -> return . InvalidByteSequence $ BS.S.pack []
        "noncharacter-character-reference" -> return $ NoncharacterCharacterReference '\NUL'
        "obsolete-tag-name" -> return $ ObsoleteTagName T.empty
        "surrogate-character-reference" -> return $ SurrogateCharacterReference '\NUL'
        "unexpected-character-after-doctype-system-identifier" ->
            return $ UnexpectedCharacterAfterDoctypeSystemIdentifier '\NUL'
        "unexpected-descendant-element" -> return $ UnexpectedDescendantElement emptyElementParams
        "unexpected-doctype" -> return $ UnexpectedDoctype emptyDocumentTypeParams
        "unexpected-end-tag" -> return $ UnexpectedEndTag emptyElementParams
        "unmatched-end-tag" -> return $ UnmatchedEndTag emptyElementParams
        _ -> maybe (fail $ err v) return . R.readMaybe . concat .
            map (T.unpack . conCase) $ T.split (== '-') v
      where conCase "cdata" = "CData"
            conCase "eof" = "EOF"
            conCase t = T.toTitle t
            err v = "Could not parse error code '" ++ T.unpack v ++ "'"


newtype TestFile = TestFile [JsonTest]
  deriving ( Eq, Show, Read )

instance J.FromJSON TestFile where
    parseJSON = J.withObject "file" $ \v -> TestFile
        <$> v .: "tests"