packages feed

onama-0.1.0.0: Text/HTML/Onama.hs

{-|
  Module: Text.HTML.Onama
  Description: Parsec extended with functions to handle HTML parsing.
  Copyright: (c) William Yao, 2017
  License: BSD-3
  Maintainer: williamyaoh@gmail.com
  Stability: experimental

  Some extra primitives to parse HTMl with Parsec.

  You'll still need to import "Text.Parsec" along with this library. These
  primitives will work with all the combinators from Parsec. Note that you'll
  need to override Parsec's @satisfies@, since that one only works on
  character streams (for some reason).

  > testParser = dp
  >   tagOpen "b"
  >   bolded <- text
  >   tagClose "b"

  > testParser2 = do
  >   tagClose "div"
  >   tagOpen "p"
  >   inner <- text
  >   tagClose "p"
 -}
module Text.HTML.Onama
  ( Tag(..)
  , Position

  , parseTags

  , tag
  , satisfy
  , tagOpen, tagOpenAny
  , tagOpen_, tagOpenAny_
  , tagClose , tagCloseAny
  , tagClose_, tagCloseAny_

  , tagText , tagTextAny
  , text
  )
where

import qualified Text.HTML.TagSoup as TS
import Text.StringLike

import qualified Text.Parsec as P
import Text.Parsec
  ( (<|>), (<?>), label, labels
  , try, unexpected
  , choice
  , count, skipMany1, many1
  , sepBy, sepBy1, endBy, endBy1, sepEndBy, sepEndBy1
  , chainl, chainl1, chainr, chainr1
  , eof
  , notFollowedBy
  , manyTill
  , lookAhead
  , anyToken
  , between
  , option, optionMaybe, optional
  , unknownError, sysUnExpectError, mergeErrorReply
  )

type Position = (TS.Row, TS.Column)

data Tag str
  = TagOpen str [TS.Attribute str] Position
  | TagClose str Position
  | TagText str Position
  deriving (Eq, Show)

parseOptions :: StringLike str => TS.ParseOptions str
parseOptions = TS.parseOptions { TS.optTagPosition = True }

type CurrentPos str = (Position, [Tag str])

startPos :: CurrentPos str
startPos = ((1, 1), [])

-- | Return a list of tags parsed from some sort of string.
--   This list should then get fed into an Onama parser.
parseTags :: StringLike str => str -> [Tag str]
parseTags str =
  reverse $ snd $ foldl attachPos startPos $
    TS.canonicalizeTags $ TS.parseTagsOptions parseOptions str
    where attachPos (pos, tags) tag =
            case tag of
              TS.TagOpen name attrs  -> (pos, TagOpen name attrs pos : tags)
              TS.TagClose name       -> (pos, TagClose name pos : tags)
              TS.TagText text        -> (pos, TagText text pos : tags)
              TS.TagComment _        -> (pos, tags)
              TS.TagWarning _        -> (pos, tags)
              TS.TagPosition row col -> ((row, col), tags)

updatePos :: P.SourcePos -> Tag str -> [Tag str] -> P.SourcePos
updatePos pos tok _ =
  let (row, col) = case tok of
                     TagOpen _ _ pos -> pos
                     TagClose _ pos  -> pos
                     TagText _ pos   -> pos
  in flip P.setSourceLine row $ flip P.setSourceColumn col $ pos

-- | Primitive. Return the next input tag.
--   All other primitive parsers should be implemented in terms of this.
tag :: (Monad m, Show str) => P.ParsecT [Tag str] u m (Tag str)
tag = P.tokenPrim show updatePos Just

-- | Create a parser which parses a single HTML tag if it passes
--   the given predicate. Return the parsed tag.
satisfy :: (Monad m, Show str) => (Tag str -> Bool) -> P.ParsecT [Tag str] u m (Tag str)
satisfy f = P.tokenPrim show updatePos $ \tag ->
              if f tag then Just tag else Nothing

-- | Tag name should be lowercase.
--   Return the parsed tag.
tagOpen_ :: (Monad m, Show str, Eq str) => str -> P.ParsecT [Tag str] u m (Tag str)
tagOpen_ str = satisfy $ \tag ->
  case tag of
    TagOpen name _ _ -> str == name
    _                -> False

-- | Tag name should be lowercase.
--   Skips over any text nodes in the HTML before attempting to parse
--   an open tag. Usually this is what you want. If not, use 'tagOpen_'.
tagOpen :: (Monad m, Show str, Eq str) => str -> P.ParsecT [Tag str] u m (Tag str)
tagOpen str = optional text >> tagOpen_ str

tagOpenAny_ :: (Monad m, Show str) => P.ParsecT [Tag str] u m (Tag str)
tagOpenAny_ = satisfy $ \tag ->
  case tag of
    TagOpen _ _ _ -> True
    _             -> False

-- | Skips over any text nodes in the HTML before attempting to parse
--   an open tag. Usually this is what you want. If not, use 'tagOpenAny_'.
tagOpenAny :: (Monad m, Show str) => P.ParsecT [Tag str] u m (Tag str)
tagOpenAny = optional text >> tagOpenAny_

-- | Tag name should be lowercase.
--   Return the parsed tag.
tagClose_ :: (Monad m, Show str, Eq str) => str -> P.ParsecT [Tag str] u m (Tag str)
tagClose_ str = satisfy $ \tag ->
  case tag of
    TagClose name _ -> str == name
    _               -> False

-- | Tag name should be lowercase.
--   Skips over any text nodes in the HTML before attempting to parse
--   a close tag. Usually this is what you want. If not, use 'tagClose_'.
tagClose :: (Monad m, Show str, Eq str) => str -> P.ParsecT [Tag str] u m (Tag str)
tagClose str = optional text >> tagClose_ str

tagCloseAny_ :: (Monad m, Show str) => P.ParsecT [Tag str] u m (Tag str)
tagCloseAny_ = satisfy $ \tag ->
  case tag of
    TagClose _ _ -> True
    _            -> False

-- | Skips over any text nodes in the HTML before attempting to parse
--   a close tag. Usually this is what you want. If not, use 'tagCloseAny_'.
tagCloseAny :: (Monad m, Show str) => P.ParsecT [Tag str] u m (Tag str)
tagCloseAny = optional text >> tagCloseAny_

-- | Return the parsed tag.
tagText :: (Monad m, Show str, Eq str) => str -> P.ParsecT [Tag str] u m (Tag str)
tagText str = satisfy $ \tag ->
  case tag of
    TagText text _ -> str == text
    _              -> False

tagTextAny :: (Monad m, Show str) => P.ParsecT [Tag str] u m (Tag str)
tagTextAny = satisfy $ \tag ->
  case tag of
    TagText text _ -> True
    _              -> False

-- | Parse and return the text of a text tag.
text :: (Monad m, Show str) => P.ParsecT [Tag str] u m str
text = do
  tag <- tagTextAny
  case tag of
    TagText t _ -> return t
    _other      -> fail "Could not find a text tag."

-- | @skip p@ produces a parser which will ignore the output of @p@.
skip :: P.Stream s m t => P.ParsecT s u m a -> P.ParsecT s u m ()
skip p = p >> return ()