packages feed

seonbi-0.1.0: test/Text/Seonbi/Html/ScannerSpec.hs

{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
module Text.Seonbi.Html.ScannerSpec (spec) where

import Test.Hspec

import Text.Seonbi.Html.Entity
import Text.Seonbi.Html.Scanner
import Text.Seonbi.Html.Tag
import Text.Seonbi.Html.TextNormalizer

isDone :: Result a -> Bool
isDone Done {} = True
isDone Fail {} = False

shouldBeDone :: (Eq a, Show a) => Result a -> a -> Expectation
shouldBeDone result expected = do
    result `shouldSatisfy` isDone
    let Done "" r = result
    r `shouldBe` expected

spec :: Spec
spec =
    describe "scanHtml" $ do
        it "returns an empty list if the input is empty" $
            scanHtml "" `shouldBeDone` []

        it "parses text nodes" $
            scanHtml "foobar" `shouldBeDone`
                [HtmlText { tagStack = [], rawText = "foobar" }]

        it "parses HTML comments" $ do
            scanHtml "<!-- foo -->" `shouldBeDone`
                [HtmlComment { tagStack = [], comment = " foo " }]
            scanHtml "<!-- foo- -->" `shouldBeDone`
                [HtmlComment { tagStack = [], comment = " foo- " }]
            scanHtml "<!-- foo-> -->" `shouldBeDone`
                [HtmlComment { tagStack = [], comment = " foo-> " }]
            scanHtml "<!-- foo-- -->" `shouldBeDone`
                [HtmlComment { tagStack = [], comment = " foo-- " }]
            scanHtml "foo <!-- bar -->" `shouldBeDone`
                [ HtmlText { tagStack = [], rawText = "foo " }
                , HtmlComment { tagStack = [], comment = " bar " }
                ]
            scanHtml "<!-- foo --> bar" `shouldBeDone`
                [ HtmlComment { tagStack = [], comment = " foo " }
                , HtmlText { tagStack = [], rawText = " bar" }
                ]
            scanHtml "foo <!-- bar --> baz" `shouldBeDone`
                [ HtmlText { tagStack = [], rawText = "foo " }
                , HtmlComment { tagStack = [], comment = " bar " }
                , HtmlText { tagStack = [], rawText = " baz" }
                ]
            scanHtml "<p>foo <!-- bar baz --> qux</p>" `shouldBeDone`
                [ HtmlStartTag { tagStack = [], tag = P, rawAttributes = "" }
                , HtmlText { tagStack = [P], rawText = "foo " }
                , HtmlComment { tagStack = [P], comment = " bar baz " }
                , HtmlText { tagStack = [P], rawText = " qux" }
                , HtmlEndTag { tagStack = [], tag = P }
                ]
            scanHtml "<p>foo <!-- <b>bar</b> baz --> qux</p>" `shouldBeDone`
                [ HtmlStartTag { tagStack = [], tag = P, rawAttributes = "" }
                , HtmlText { tagStack = [P], rawText = "foo " }
                , HtmlComment { tagStack = [P], comment = " <b>bar</b> baz " }
                , HtmlText { tagStack = [P], rawText = " qux" }
                , HtmlEndTag { tagStack = [], tag = P }
                ]

        it "parses CDATA sections" $ do
            scanHtml "<![CDATA[foo]]>" `shouldBeDone`
                [HtmlCdata { tagStack = [], text = "foo" }]
            scanHtml "foo <![CDATA[bar]]>" `shouldBeDone`
                [ HtmlText { tagStack = [], rawText = "foo " }
                , HtmlCdata { tagStack = [], text = "bar" }
                ]
            scanHtml "<![CDATA[foo]]> bar" `shouldBeDone`
                [ HtmlCdata { tagStack = [], text = "foo" }
                , HtmlText { tagStack = [], rawText = " bar" }
                ]
            scanHtml "foo <![CDATA[bar]]> baz" `shouldBeDone`
                [ HtmlText { tagStack = [], rawText = "foo " }
                , HtmlCdata { tagStack = [], text = "bar" }
                , HtmlText { tagStack = [], rawText = " baz" }
                ]
            scanHtml "<![CDATA[foo] ]]>" `shouldBeDone`
                [HtmlCdata { tagStack = [], text = "foo] " }]
            scanHtml "<p>foo <![CDATA[bar baz]]> qux</p>" `shouldBeDone`
                [ HtmlStartTag { tagStack = [], tag = P, rawAttributes = "" }
                , HtmlText { tagStack = [P], rawText = "foo " }
                , HtmlCdata { tagStack = [P], text = "bar baz" }
                , HtmlText { tagStack = [P], rawText = " qux" }
                , HtmlEndTag { tagStack = [], tag = P }
                ]
            scanHtml "<p>foo <![CDATA[<b>bar</b> baz]]> qux</p>" `shouldBeDone`
                [ HtmlStartTag { tagStack = [], tag = P, rawAttributes = "" }
                , HtmlText { tagStack = [P], rawText = "foo " }
                , HtmlCdata { tagStack = [P], text = "<b>bar</b> baz" }
                , HtmlText { tagStack = [P], rawText = " qux" }
                , HtmlEndTag { tagStack = [], tag = P }
                ]

        it "treats malformed CDATA sections as text nodes" $
            (normalizeText <$> scanHtml "<![CDATA[foo") `shouldBeDone`
                [HtmlText { tagStack = [], rawText = "<![CDATA[foo" }]

        it "parses html start tags" $ do
            scanHtml "<p>" `shouldBeDone`
                [HtmlStartTag { tagStack = [], tag = P, rawAttributes = "" }]
            scanHtml "<p><em>" `shouldBeDone`
                [ HtmlStartTag { tagStack = [], tag = P, rawAttributes = "" }
                , HtmlStartTag { tagStack = [P], tag = Em, rawAttributes = "" }
                ]

        it "parses HTML start tags having attributes" $ do
            scanHtml "<p class=foo>" `shouldBeDone`
                [ HtmlStartTag
                    { tagStack = []
                    , tag = P
                    , rawAttributes = " class=foo"
                    }
                ]
            scanHtml "<a href=\"https://example.com/\">" `shouldBeDone`
                [ HtmlStartTag
                    { tagStack = []
                    , tag = A
                    , rawAttributes = " href=\"https://example.com/\""
                    }
                ]

        it "parses html end tags" $ do
            scanHtml "<p></p>" `shouldBeDone`
                [ HtmlStartTag { tagStack = [], tag = P, rawAttributes = "" }
                , HtmlEndTag { tagStack = [], tag = P }
                ]
            scanHtml "<p><em>test</em></p>" `shouldBeDone`
                [ HtmlStartTag { tagStack = [], tag = P, rawAttributes = "" }
                , HtmlStartTag { tagStack = [P], tag = Em, rawAttributes = "" }
                , HtmlText { tagStack = [P, Em], rawText = "test" }
                , HtmlEndTag { tagStack = [P], tag = Em }
                , HtmlEndTag { tagStack = [], tag = P }
                ]

        it "can parse even if tags end in wrong order" $ do
            scanHtml "<p><em>test</p></em>" `shouldBeDone`
                [ HtmlStartTag { tagStack = [], tag = P, rawAttributes = "" }
                , HtmlStartTag { tagStack = [P], tag = Em, rawAttributes = "" }
                , HtmlText { tagStack = [P, Em], rawText = "test" }
                , HtmlEndTag { tagStack = [Em], tag = P }
                , HtmlEndTag { tagStack = [], tag = Em }
                ]
            scanHtml "<p><b class=\"baz\">Hel<i>lo Wo</b>rld</i></p>"
                `shouldBeDone`
                [ HtmlStartTag { tagStack = [], tag = P, rawAttributes = "" }
                , HtmlStartTag
                    { tagStack = [P]
                    , tag = B
                    , rawAttributes = " class=\"baz\""
                    }
                , HtmlText { tagStack = [P, B], rawText = "Hel" }
                , HtmlStartTag
                    { tagStack = [P, B]
                    , tag = I
                    , rawAttributes = ""
                    }
                , HtmlText { tagStack = [P, B, I], rawText = "lo Wo" }
                , HtmlEndTag { tagStack = [P, I], tag = B }
                , HtmlText { tagStack = [P, I], rawText = "rld" }
                , HtmlEndTag { tagStack = [P], tag = I }
                , HtmlEndTag { tagStack = [], tag = P }
                ]

        it "can parse XHTML-style self-closing tags" $
            scanHtml "<p><em/>" `shouldBeDone`
                [ HtmlStartTag { tagStack = [], tag = P, rawAttributes = "" }
                , HtmlStartTag { tagStack = [P], tag = Em, rawAttributes = "" }
                , HtmlEndTag { tagStack = [P], tag = Em }
                ]

        it "emits both start and end tags for void tags (e.g., <hr>)" $ do
            scanHtml "<p><hr>" `shouldBeDone`
                [ HtmlStartTag { tagStack = [], tag = P, rawAttributes = "" }
                , HtmlStartTag { tagStack = [P], tag = HR, rawAttributes = "" }
                , HtmlEndTag { tagStack = [P], tag = HR }
                ]
            scanHtml
                "<div><p>foo</p><hr><p>bar <img src=\"a.jpg\"></p><hr></div>"
                `shouldBeDone`
                [ HtmlStartTag { tagStack = [], tag = Div, rawAttributes = "" }
                , HtmlStartTag { tagStack = [Div], tag = P, rawAttributes = "" }
                , HtmlText { tagStack = [Div, P], rawText = "foo" }
                , HtmlEndTag { tagStack = [Div], tag = P }
                , HtmlStartTag
                    { tagStack = [Div]
                    , tag = HR
                    , rawAttributes = ""
                    }
                , HtmlEndTag { tagStack = [Div], tag = HR }
                , HtmlStartTag { tagStack = [Div], tag = P, rawAttributes = "" }
                , HtmlText { tagStack = [Div, P], rawText = "bar " }
                , HtmlStartTag
                    { tagStack = [Div, P]
                    , tag = Img
                    , rawAttributes = " src=\"a.jpg\""
                    }
                , HtmlEndTag { tagStack = [Div, P], tag = Img }
                , HtmlEndTag { tagStack = [Div], tag = P }
                , HtmlStartTag
                    { tagStack = [Div]
                    , tag = HR
                    , rawAttributes = ""
                    }
                , HtmlEndTag { tagStack = [Div], tag = HR }
                , HtmlEndTag { tagStack = [], tag = Div }
                ]
            scanHtml "<embed></embed>" `shouldBeDone`
                [ HtmlStartTag
                    { tagStack = []
                    , tag = Embed
                    , rawAttributes = ""
                    }
                , HtmlEndTag { tagStack = [], tag = Embed }
                ]

        it "can parses a flat element" $
            scanHtml "<div>Hello</div>" `shouldBeDone`
                [ HtmlStartTag { tagStack = [], tag = Div, rawAttributes = "" }
                , HtmlText { tagStack = [Div], rawText = "Hello" }
                , HtmlEndTag { tagStack = [], tag = Div }
                ]

        it "can parses nested elements" $ do
            scanHtml "<div><p class=foo>Hello</p><p class='bar'>World</p></div>"
                `shouldBeDone`
                [ HtmlStartTag { tagStack = [], tag = Div, rawAttributes = "" }
                , HtmlStartTag
                    { tagStack = [Div]
                    , tag = P
                    , rawAttributes = " class=foo"
                    }
                , HtmlText { tagStack = [Div, P], rawText = "Hello" }
                , HtmlEndTag { tagStack = [Div], tag = P }
                , HtmlStartTag
                    { tagStack = [Div]
                    , tag = P
                    , rawAttributes = " class='bar'"
                    }
                , HtmlText { tagStack = [Div, P], rawText = "World" }
                , HtmlEndTag { tagStack = [Div], tag = P }
                , HtmlEndTag { tagStack = [], tag = Div }
                ]
            scanHtml "<p><b class=\"baz\">Hel<i>lo Wo</i>rld</b></p>"
                `shouldBeDone`
                [ HtmlStartTag { tagStack = [], tag = P, rawAttributes = "" }
                , HtmlStartTag
                    { tagStack = [P]
                    , tag = B
                    , rawAttributes = " class=\"baz\""
                    }
                , HtmlText { tagStack = [P, B], rawText = "Hel" }
                , HtmlStartTag
                    { tagStack = [P, B]
                    , tag = I
                    , rawAttributes = ""
                    }
                , HtmlText { tagStack = [P, B, I], rawText = "lo Wo" }
                , HtmlEndTag { tagStack = [P, B], tag = I }
                , HtmlText { tagStack = [P, B], rawText = "rld" }
                , HtmlEndTag { tagStack = [P], tag = B }
                , HtmlEndTag { tagStack = [], tag = P }
                ]

        it "can parses an HTML fragment having multiple root elements" $
            scanHtml
                "<p\nid=\"a\">Hello <b>world</b>!</p>\n<p>Second paragraph.</p>"
                `shouldBeDone`
                [ HtmlStartTag
                    { tagStack = []
                    , tag = P
                    , rawAttributes = "\nid=\"a\""
                    }
                , HtmlText { tagStack = [P], rawText = "Hello " }
                , HtmlStartTag { tagStack = [P], tag = B, rawAttributes = "" }
                , HtmlText { tagStack = [P, B], rawText = "world" }
                , HtmlEndTag { tagStack = [P], tag = B }
                , HtmlText { tagStack = [P], rawText = "!" }
                , HtmlEndTag { tagStack = [], tag = P }
                , HtmlText { tagStack = [], rawText = "\n" }
                , HtmlStartTag { tagStack = [], tag = P, rawAttributes = "" }
                , HtmlText { tagStack = [P], rawText = "Second paragraph." }
                , HtmlEndTag { tagStack = [], tag = P }
                ]

        it "treats an invalid tag as a text node" $
            (normalizeText <$> scanHtml "<invalid>") `shouldBeDone`
                [HtmlText { tagStack = [], rawText = "<invalid>" }]