packages feed

seonbi-0.1.0: src/Text/Seonbi/Html/Scanner.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module Text.Seonbi.Html.Scanner
    ( Result (..)
    , scanHtml
    ) where

import Data.Char
import Prelude hiding (takeWhile)

import Data.Attoparsec.Text.Lazy
import Data.Map.Strict
import qualified Data.Text
import qualified Data.Text.Lazy

import Text.Seonbi.Html.Entity
import Text.Seonbi.Html.Tag
import Text.Seonbi.Html.TagStack

htmlFragments :: Parser [HtmlEntity]
htmlFragments = do
    result <- option [] $ fragments Text.Seonbi.Html.TagStack.empty
    txt <- htmlText Text.Seonbi.Html.TagStack.empty
    endOfInput
    return $ case txt of
        HtmlText { rawText = "" } -> result
        _ -> result ++ [txt]

fragments :: HtmlTagStack -> Parser [HtmlEntity]
fragments tagStack' = do
    txt <- htmlText tagStack'
    (entities, nextStack) <- htmlEntity tagStack'
    nextChunk <- option [] $ fragments nextStack
    let chunks = entities ++ nextChunk
    return $ case txt of
        HtmlText { rawText = "" } -> chunks
        txt' -> txt' : chunks

htmlText :: HtmlTagStack -> Parser HtmlEntity
htmlText tagStack' = do
    texts <- many' textFragment
    return $ mkText $ Data.Text.concat texts
  where
      mkText :: Data.Text.Text -> HtmlEntity
      mkText txt = HtmlText { tagStack = tagStack', rawText = txt }

textFragment :: Parser Data.Text.Text
textFragment = choice
    [ takeWhile1 (/= '<')
    , do
        a <- char '<'
        b <- satisfy $ \ c ->
            not (c == '!' || c == '/' || isAsciiUpper c || isAsciiLower c)
        return $ Data.Text.pack [a, b]
    ]

htmlEntity :: HtmlTagStack -> Parser ([HtmlEntity], HtmlTagStack)
htmlEntity tagStack' = choice
    [ htmlComment tagStack'
    , cdata tagStack'
    , startTag tagStack'
    , endTag tagStack'
    -- fallback:
    , (, tagStack') . (: []) . HtmlText tagStack' . Data.Text.singleton
        <$> anyChar
    ]

-- https://www.w3.org/TR/html5/syntax.html#comments
htmlComment :: HtmlTagStack -> Parser ([HtmlEntity], HtmlTagStack)
htmlComment tagStack' = do
    _ <- string "<!--"
    contents <- many' $ choice
        [ takeWhile1 (/= '-')
        , do
              a <- char '-'
              b <- notChar '-'
              return $ Data.Text.pack [a, b]
        , do
            a <- string "--"
            b <- notChar '>'
            return $ Data.Text.snoc a b
        ]
    _ <- string "-->"
    return
        ( [ HtmlComment
                { tagStack = tagStack'
                , comment = Data.Text.concat contents
                }
          ]
        , tagStack'
        )

-- https://www.w3.org/TR/html5/syntax.html#cdata-sections
cdata :: HtmlTagStack -> Parser ([HtmlEntity], HtmlTagStack)
cdata tagStack' = do
    _ <- string "<![CDATA["
    contents <- many' $ choice
        [ takeWhile1 (/= ']')
        , do
            a <- char ']'
            b <- notChar ']'
            return $ Data.Text.pack [a, b]
        , do
            a <- string "]]"
            b <- notChar '>'
            return $ Data.Text.snoc a b
        ]
    _ <- string "]]>"
    return
        ( [HtmlCdata { tagStack = tagStack', text = Data.Text.concat contents }]
        , tagStack'
        )

-- https://www.w3.org/TR/html5/syntax.html#start-tags
startTag :: HtmlTagStack -> Parser ([HtmlEntity], HtmlTagStack)
startTag tagStack' = do
    _ <- char '<'
    tag' <- htmlTag
    attributes <- many' $ choice
        [ do
            s <- char '"'
            c <- takeWhile (/= '"')
            e <- char '"'
            return (Data.Text.cons s $ Data.Text.snoc c e)
        , do
            s <- char '\''
            c <- takeWhile (/= '\'')
            e <- char '\''
            return (Data.Text.cons s $ Data.Text.snoc c e)
        , takeWhile1 $ \ c -> c /= '"' && c /= '\'' && c /= '/' && c /= '>'
        ]
    selfClosing <- option ' ' $ char '/'
    _ <- char '>'
    let (trailingEntities, nextTagStack) =
            if selfClosing == '/' || htmlTagKind tag' == Void
            then ([HtmlEndTag { tagStack = tagStack', tag = tag' }], tagStack')
            else ([], push tag' tagStack')
    return
        ( HtmlStartTag
            { tagStack = tagStack'
            , tag = tag'
            , rawAttributes = Data.Text.concat attributes
            } : trailingEntities
        , nextTagStack
        )

-- https://www.w3.org/TR/html5/syntax.html#end-tags
endTag :: HtmlTagStack -> Parser ([HtmlEntity], HtmlTagStack)
endTag tagStack' = do
    _ <- string "</"
    tag' <- htmlTag
    _ <- char '>'
    return $ case htmlTagKind tag' of
        Void -> ([], tagStack')
        _ ->
            let
                nextTagStack = pop tag' tagStack'
            in
                ( [HtmlEndTag { tagStack = nextTagStack, tag = tag' }]
                , nextTagStack
                )

htmlTag :: Parser HtmlTag
htmlTag = do
    name <- tagName
    case Data.Map.Strict.lookup (Data.Text.toLower name) htmlTagNames of
        Just t -> return t
        _ -> fail ("failed to parse; invalid tag: " ++ Data.Text.unpack name)

tagName :: Parser Data.Text.Text
tagName = do
    first <- satisfy $ \ c -> isAsciiUpper c || isAsciiLower c
    rest <- takeWhile $ \ c -> isAsciiUpper c || isAsciiLower c || isDigit c
    return $ Data.Text.cons first rest

scanHtml :: Data.Text.Lazy.Text -> Result [HtmlEntity]
scanHtml = parse htmlFragments