packages feed

mangrove-0.1.0.0: test/Test/Mangrove/Html5Lib/TreeConstruction/Parser.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}

module Test.Mangrove.Html5Lib.TreeConstruction.Parser
    ( parseTestFile
    , TreeTest ( .. )
    ) where


import qualified Data.ByteString.Char8 as BS.C
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 Data.Text.Encoding as T

import Web.Mangrove.Parse.Tree
import Web.Willow.DOM

import Test.Mangrove.Html5Lib.Common

#if MIN_VERSION_base(4,11,0)
#else
import Data.Semigroup ( (<>) )
#endif

import System.FilePath ( (</>), (<.>) )


data TreeTest = TreeTest
    { input :: BS.C.ByteString
    , errors :: [(Word, Word, T.Text)]
    , output :: Tree
    , state :: TreeState
    }
  deriving ( Eq, Show, Read )

data RawTest = RawTest
    { input' :: [BS.C.ByteString]
    , errors' :: [BS.C.ByteString]
    , output' :: [BS.C.ByteString]
    , state' :: Maybe BS.C.ByteString
    , scripting' :: Maybe Bool
    }
  deriving ( Eq, Show, Read )


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

parseTestFile :: FilePath -> IO [TreeTest]
parseTestFile p = fmap parse $ testFile (p <.> "dat") >>= BS.C.readFile

parse :: BS.C.ByteString -> [TreeTest]
parse = Y.mapMaybe (parseTest . rawTest) . breakTests
  where breakTests = breakTests' . BS.C.split '\n'
        breakTests' [] = []
        breakTests' bss = case break BS.C.null bss of
            (bs, []) -> [bs]
            (bs, bss') -> bs : breakTests' (drop 1 bss')

rawTest :: [BS.C.ByteString] -> RawTest
rawTest ("#data":bss) = (rawTest bss')
    { input' = d
    }
  where (d, bss') = break isInstruction bss
rawTest ("#errors":bss) = (rawTest bss')
    { errors' = e
    }
  where (e, bss') = break isInstruction bss
rawTest ("#document-fragment":bss) = (rawTest bss')
    { state' = Y.listToMaybe x
    }
  where (x, bss') = break isInstruction bss
rawTest ("#document":bss) = (rawTest bss')
    { output' = t
    }
  where (t, bss') = break isInstruction bss
rawTest ("#script-off":bss) = (rawTest bss')
    { scripting' = Just False
    }
  where (_, bss') = break isInstruction bss
rawTest ("#script-on":bss) = (rawTest bss')
    { scripting' = Just True
    }
  where (_, bss') = break isInstruction bss
rawTest (_:bss) = rawTest $ dropWhile (not . isInstruction) bss
rawTest [] = RawTest
    { input' = []
    , errors' = []
    , output' = []
    , state' = Nothing
    , scripting' = Nothing
    }

isInstruction :: BS.C.ByteString -> Bool
isInstruction bs = fmap fst (BS.C.uncons bs) == Just '#'


parseTest :: RawTest -> Maybe TreeTest
parseTest t
    | null $ input' t = Nothing
    | null $ output' t = Nothing
    | scripting' t == Just True = Nothing
    | otherwise = Just $ TreeTest
        { input = BS.C.intercalate "\n" $ input' t
        , errors = Y.mapMaybe parseError $ errors' t
        , output = parseOutput . map (BS.C.drop 2) . foldr joinMultiline [] $ output' t
        , state = case state' t of
            Just bs -> treeFragment (packElement bs) [] Nothing Nothing defaultTreeState
            Nothing -> defaultTreeState
        }
  where joinMultiline bs [] = [bs]
        joinMultiline bs (bs':bss) = if BS.C.take 2 bs' == "| "
            then bs : bs' : bss
            else bs <> "\n" <> bs' : bss
        htmlElement = emptyElementParams
            { elementName = "html"
            , elementNamespace = Just htmlNamespace
            }
        packElement = packElement' . parseNode . BS.C.cons '<' . flip BS.C.snoc '>'
        packElement' (Element d) = d
        packElement' _ = htmlElement

parseError :: BS.C.ByteString -> Maybe (Word, Word, T.Text)
parseError _ = Nothing --TODO

parseOutput :: [BS.C.ByteString] -> Tree
parseOutput bss = Tree
    { node = Document NoQuirks
    , children = map snd . foldr encapsulate [] $ map dropParents bss
    }
  where dropParents bs =
            let (ws, n) = BS.C.span (== ' ') bs
            in  (BS.C.length ws, parseNode n)
        encapsulate (d, e) es =
            let (cs, ss) = span ((d <) . fst) es
                (attrs, nonAttrs) = L.partition (\c -> nodeType (node $ snd c) == Just AttributeNode) cs
                (e', cs') = if nodeType e == Just ElementNode
                    then let Element t = e
                             t' = t
                                { elementAttributes =
                                    M.union (elementAttributes t) . fromAttrList $ Y.mapMaybe (unpack . node . snd) attrs
                                }
                             unpack (Attribute a) = Just a
                             unpack _ = Nothing
                         in  (Element t', nonAttrs)
                    else (e, cs)
            in  (d, Tree
                    { node = e'
                    , children = map snd cs'
                    }) : ss

parseNode :: BS.C.ByteString -> Node
parseNode bs
    | bs == "content" = DocumentFragment
    | BS.C.take 10 bs == "<!DOCTYPE " =
        let (n', bs') = BS.C.break (\c -> c == ' ' || c == '>') $ BS.C.drop 10 bs
            (n, p, s) = if BS.C.take 1 bs' == ">"
                then (repack 0 0 n', T.empty, T.empty)
                else let (p', s') = BS.C.break (== '"') $ BS.C.drop 2 bs'
                     in  (repack 0 0 n', repack 0 0 p', repack 3 2 s')
        in  DocumentType $ emptyDocumentTypeParams
                { documentTypeName = n
                , documentTypePublicId = p
                , documentTypeSystemId = s
                }
    | BS.C.take 5 bs == "<!-- " = Comment $ repack 5 4 bs
    | BS.C.take 1 bs == "<" && fmap snd (BS.C.unsnoc bs) == Just '>' =
        let (ns, n) = trimTag $ repack 1 1 bs
        in  Element $ emptyElementParams
                { elementNamespace = Just ns
                , elementName = n
                }
    | BS.C.take 1 bs == "\"" = Text $ repack 1 1 bs
    | BS.C.isInfixOf "=\"" bs && BS.C.last bs == '"' =
        let (n', v) = BS.C.break (== '=') bs
        in  Attribute $ (trimAttribute $ repack 0 0 n')
                { attrValue = repack 2 1 v
                }
    | otherwise = Document NoQuirks
  where repack h f bs' = T.decodeUtf8 . BS.C.drop h $ BS.C.take (BS.C.length bs' - f) bs'
        trimTag t
            | T.take 4 t == "svg " = (svgNamespace, T.drop 4 t)
            | T.take 5 t == "math " = (mathMLNamespace, T.drop 5 t)
            | otherwise = (htmlNamespace, t)
        trimAttribute t
            | T.take 6 t == "xlink " = emptyAttributeParams
                { attrNamespace = Just xlinkNamespace
                , attrPrefix = Just "xlink"
                , attrName = T.drop 6 t
                }
            | T.take 4 t == "xml " = emptyAttributeParams
                { attrNamespace = Just xmlNamespace
                , attrPrefix = Just "xml"
                , attrName = T.drop 4 t
                }
            | T.take 6 t == "xmlns " = emptyAttributeParams
                { attrNamespace = Just xmlnsNamespace
                , attrPrefix = Just "xmlns"
                , attrName = T.drop 6 t
                }
            | otherwise = emptyAttributeParams
                { attrNamespace = Nothing
                , attrPrefix = Nothing
                , attrName = t
                }