{-# LANGUAGE OverloadedStrings #-}
module Exercise04 where
import qualified Data.Text as T
import Data.Bifunctor (Bifunctor(second))
import Data.Char (isSpace)
import Data.List (intercalate)
{-WETT-}
xmlLight :: String -> Bool
xmlLight str = T.pack str `xmlLight'` []
xmlLight' :: T.Text -> [T.Text] -> Bool
-- str: Remaining string to parse.
-- tagIDs: List of tag IDs of previously opened tags.
-- This list is used like a stack, hence the last tag that was opened
-- is the head of the list.
xmlLight' str tagIDs
-- Tip: Have a look at the local definitions in the "where" part for
-- a better understanding!
-- The string is just plaintext or empty, i. e. is does not contain < or >.
-- Therefore, the XML is valid if all tags have been closed,
-- i. e. tagIDs is empty.
| not $ "<" `T.isInfixOf` str ||
">" `T.isInfixOf` str = null tagIDs
-- > before the tag
| ">" `T.isInfixOf` beforeTag
-- no > after a <
|| not (">" `T.isInfixOf` fromTag)
-- tagID contains not only trailing whitespace
|| T.any isSpace tagID
-- Closing tag does not close the last tag that was opened.
-- take 1 needs to be called instead of head because the list of opened
-- tags might be empty. In that case, False has to be returned because it
-- is not allowed to close a tag that was not opened.
|| tag /= tagID &&
take 1 tagIDs /= [tagID] = False
-- If the parsed tag is an opening tag, add it on top of the tagIDs stack.
-- If it was a valid closing tag, remove this tag from the tagIDs stack.
-- Call xmlLight' recursively to validate the remaining xml with the
-- updated stack.
-- It is okay to call tail here, since the previous two lines of code
-- assure that tagIDs is not empty.
| otherwise = xmlLight' xml $ if tag == tagID
then tagID : tagIDs
else tail tagIDs
where
-- beforeTag: string before the first <
-- fromTag: string after the first <
(beforeTag, fromTag) = split "<" str
-- tagWithSpaces: string between < and >
-- xml: string after >
(tagWithSpaces, xml) = split ">" fromTag
-- tag without trailing spaces
tag = T.stripEnd tagWithSpaces
-- tagID without leading / if the tag is a closing tag.
-- It is okay to call T.head here, since the tagID
-- will not be empty according to the task.
tagID = if T.head tag == '/'
then T.tail tag
else tag
-- Function that splits a text at a given delimiter.
-- split calls T.breakOn and removes the leading delimiter from the second
-- component of the resulting pair.
-- It is okay to use T.tail here because xmlLight' assures that it does not
-- use the second component of the result if str does not contain delim.
split :: T.Text -> T.Text -> (T.Text, T.Text)
split delim str = T.tail `second` T.breakOn delim str
{-MCCOMMENT
The solution above passes all tests on Artemis, yet it does not pass my own
tests. For example, it returns True for "<" although it should return
False according to rule 4. Therefore, I've got an alternative solution with
six more tokens that can handle such strings properly:
xmlLight :: String -> Bool
xmlLight str = T.pack str `xmlLight'` []
xmlLight' :: T.Text -> [T.Text] -> Bool
-- str: Remaining string to parse.
-- tagIDs: List of tag IDs of previously opened tags.
-- This list is used like a stack, hence the last tag that was opened
-- is the head of the list.
xmlLight' str tagIDs
-- Tip: Have a look at the local definitions in the "where" part for
-- a better understanding!
-- The string is just plaintext or empty, i. e. is does not contain < or >.
-- Therefore, the XML is valid if all tags have been closed,
-- i. e. tagIDs is empty.
| not $ "<" `T.isInfixOf` str ||
">" `T.isInfixOf` str = null tagIDs
-- > before the tag
| ">" `T.isInfixOf` beforeTag
-- no > after a <
|| not (">" `T.isInfixOf` fromTag)
-- tagID contains no reserved character
|| T.any (`elem` reservedChars) tagID
-- Closing tag does not close the last tag that was opened.
-- take 1 needs to be called instead of head because the list of opened
-- tags might be empty. In that case, False has to be returned because it
-- is not allowed to close a tag that was not opened.
|| tag /= tagID &&
take 1 tagIDs /= [tagID] = False
-- If the parsed tag is an opening tag, add it on top of the tagIDs stack.
-- If it was a valid closing tag, remove this tag from the tagIDs stack.
-- Call xmlLight' recursively to validate the remaining xml with the
-- updated stack.
-- It is okay to call tail here, since the previous two lines of code
-- assure that tagIDs is not empty.
| otherwise = xmlLight' xml $ if tag == tagID
then tagID : tagIDs
else tail tagIDs
where
reservedChars :: String
reservedChars = " <>/"
-- beforeTag: string before the first <
-- fromTag: string after the first <
(beforeTag, fromTag) = split "<" str
-- tagWithSpaces: string between < and >
-- xml: string after >
(tagWithSpaces, xml) = split ">" fromTag
-- tag without trailing spaces
tag = T.stripEnd tagWithSpaces
-- tagID without leading / if the tag is a closing tag.
-- It is okay to call T.head here, since the tagID
-- will not be empty according to the task.
tagID = if T.head tag == '/'
then T.tail tag
else tag
-- Function that splits a text at a given delimiter.
-- split calls T.breakOn and removes the leading delimiter from the second
-- component of the resulting pair.
-- It is okay to use T.tail here because xmlLight' assures that it does not
-- use the second component of the result if str does not contain delim.
split :: T.Text -> T.Text -> (T.Text, T.Text)
split delim str = T.tail `second` T.breakOn delim str
-}
{-TTEW-}
{- Test cases
tests :: [(String, Bool)]
tests = [
-- Rule 1
("", True),
(" ", True),
("", True),
("", True),
("", False),
("", False),
("", False),
("", False),
("", False),
-- Rule 3
("", True),
("", False),
("", True),
("< a > a >", False),
("", True),
-- Rule 4
("<", False),
(">", False),
("<", False),
("<<><>", False),
("abc", False),
("abc>def", False),
("<", False),
("<", False),
(">", False),
(">", False),
("<", False),
-- Rule 6
("", True),
("", False),
("", True),
-- Rule 7
("", True),
("abc", True),
("abc", True),
("abcabcabc", True),
("abcabcabcabcabc", True),
-- Rule 8
("", False)
]
boolToStr :: Bool -> String
boolToStr True = ""
boolToStr False = ""
testResults :: [String]
testResults = filter (not . null)
(map (\(xml, expected) ->
let actual = xmlLight xml
in if actual == expected
then ""
else xml ++ " \t-- expected "
++ boolToStr expected ++
" but got " ++ boolToStr actual) tests)
test :: IO ()
test
| null testResults = putStr "Congratulations! Passed all tests.\n"
| otherwise = putStr ("Failed the following tests:\n\t"
++ intercalate "\n\t" testResults ++ "\n")
-}