packages feed

mangrove-0.1.0.0: src/Web/Mangrove/Parse/Tokenize.hs

{-|
Description:    Extract basic semantic categories from a simple textual stream.

Copyright:      (c) 2020 Sam May
License:        MPL-2.0
Maintainer:     ag.eitilt@gmail.com

Stability:      provisional
Portability:    portable

This module and the internal branch it heads implement the "Tokenization"
section of the
__[HTML](https://html.spec.whatwg.org/multipage/parsing.html#tokenization)__
document parsing specification, processing a stream of text to add information
on, or group it by, semantic category.  This then allows the following stage to
base its logic on such higher-level concepts as "markup tag" or "comment"
without worrying about the (sometimes complex) escaping behaviour required to
parse them.
-}
module Web.Mangrove.Parse.Tokenize
    ( -- * Types
      -- ** Final
      Token ( .. )
    , BasicAttribute
    , TagParams ( .. )
    , emptyTagParams
    , DoctypeParams ( .. )
    , emptyDoctypeParams
      -- ** Intermediate
    , TokenizerState
    , CurrentTokenizerState ( .. )
    , Encoding ( .. )
      -- * Initialization
    , defaultTokenizerState
    , tokenizerMode
    , tokenizerStartTag
    , tokenizerEncoding
      -- * Transformations
    , tokenize
    , tokenizeStep
    , finalizeTokenizer
    ) where


import qualified Control.Applicative as A
import qualified Control.Monad.Trans.State as N.S

import qualified Data.Bifunctor as F.B
import qualified Data.ByteString as BS
import qualified Data.ByteString.Short as BS.SH
import qualified Data.Maybe as Y

import Web.Willow.DOM

import Web.Mangrove.Parse.Common.Error
import Web.Mangrove.Parse.Encoding.Preprocess
import Web.Mangrove.Parse.Tokenize.Common hiding ( setRemainder )
import Web.Mangrove.Parse.Tokenize.Dispatcher
import Web.Willow.Common.Encoding
import Web.Willow.Common.Encoding.Sniffer
import Web.Willow.Common.Parser


-- | __HTML:__
--      @[tokenization]
--      (https://html.spec.whatwg.org/multipage/parsing.html#tokenization)@
-- 
-- Given a starting environment, transform a binary document stream into a
-- stream of semantic atoms.  If the parse fails, returns all tokens before the
-- one which caused the error, but any trailing bytes are silently dropped.
tokenize
    :: TokenizerState
    -> BS.ByteString
    -> ([([ParseError], Token)], TokenizerState)
tokenize state stream = loop $ tokenizeStep state stream
  where loop (ts, state', output)
            | BS.null output = (ts, state')
            | otherwise = F.B.first (ts ++) . loop $ tokenizeStep state' output


-- | Parse a minimal number of bytes from an input stream, into a sequence of
-- semantic tokens.  Returns all data required to seamlessly resume parsing.
tokenizeStep
    :: TokenizerState
    -> BS.ByteString
    -> ([([ParseError], Token)], TokenizerState, BS.ByteString)
tokenizeStep state stream = case runParserT (N.S.runStateT dispatcher' state) stream of
    Just ((out, state'), stream') -> (out, state', stream')
    Nothing -> ([], stateEof, BS.empty)
  where stateEof = state
            { decoderState_ = Right $ setRemainder (BS.SH.toShort stream) <$>
                decoderDefaultState state stream
            }

-- | Explicitly indicate that the input stream will not contain any further
-- bytes, and perform any finalization processing based on that.
finalizeTokenizer :: TokenizerState -> [([ParseError], Token)]
finalizeTokenizer state = fst $ tokenize state' BS.empty
  where state' = state
            { tokenParserState = (tokenParserState state)
                { atEndOfStream = True
                }
            }


-- | Given a string as emitted by the decoder and the final state of that
-- parser, repack it into a single list with that final state encapsulated in a
-- 'Just'; the 'init' of the string is given 'Nothing's.
repackStream :: ([([ParseError], Char)], DecoderState, BS.ByteString) -> [TokenizerInput]
repackStream ([], _, _) = []
repackStream ([(errs, c)], dState, bs) =
    [TokenizerInput errs c $ Just (Just dState, bs)]
repackStream ((errs, c):cs, dState, bs) =
    TokenizerInput errs c Nothing : repackStream (cs, dState, bs)


-- | Wrap the standard dispatcher to operate over a raw 'BS.ByteString'
-- rather than the "Web.Mangrove.Parse.Encoding" output.
dispatcher' :: StateParser TokenizerState BS.ByteString [([ParseError], Token)]
dispatcher' = do
    state <- N.S.get
    stream <- abridge
    let dState = Y.fromMaybe (initialDecoderState Utf8) $ decoderDefaultState state stream
    recurse state $ preprocessStep' dState stream
  where preprocessStep' dState input = case preprocessStep dState input of
            ([], _, _) -> []
            cs'@(_, dState', input') -> repackStream cs' ++ preprocessStep' dState' input'

-- | Loop the tokenization dispatcher until it returns a set of tokens which
-- happens to coincide with a decoder breakpoint.  Relies on lazy evaluation in
-- the stream generation to avoid forcing the entire thing at once, while still
-- retaining the capability to consume as much input as necessary to get the
-- parsers to line up.
recurse
    :: TokenizerState
    -> [TokenizerInput]
    -> StateParser TokenizerState BS.ByteString [([ParseError], Token)]
recurse state stream = case runParserT (N.S.runStateT dispatcher $ tokenParserState state) stream of
    Nothing -> A.empty
    Just ((out, tokState'), stream') -> case Y.listToMaybe (reverse out) >>= tokenizedState of
        Nothing -> do
            let state' = state
                    { tokenParserState = tokState'
                    }
            N.S.put state'
            out' <- recurse state' stream'
            return $ map repackOut out ++ out'
        Just (dState, dStream) -> do
            pushChunk dStream
            N.S.put $ state
                { decoderState_ = Right dState
                , tokenParserState = tokState'
                }
            return $ map repackOut out
  where repackOut t' = (tokenizedErrs t', tokenizedOut t')


-- | Specify which section of the finite state machine describing the
-- tokenization algorithm should be active.
tokenizerMode :: CurrentTokenizerState -> TokenizerState -> TokenizerState
tokenizerMode mode state = state
    { tokenParserState = (tokenParserState state)
        { currentState = mode
        }
    }

-- | Specify the data to use as the previous tag which had been emitted by the
-- tokenizer.  This only has to be called when required for external algorithms
-- or constructions; the parser automatically updates as required for generated
-- 'StartTag' tokens.
tokenizerStartTag :: Maybe Namespace -> ElementName -> TokenizerState -> TokenizerState
tokenizerStartTag ns name state = state
    { tokenParserState = (tokenParserState state)
        { prevStartTag = Just name
        , currentNodeNamespace = ns
        }
    }

-- | Specify the encoding scheme used by a given parse environment to read from
-- the binary input stream.  Note that this will always use the initial state
-- for the respective decoder; intermediate states as returned by 'decodeStep'
-- are not supported.
tokenizerEncoding :: Either SnifferEnvironment (Maybe Encoding) -> TokenizerState -> TokenizerState
tokenizerEncoding enc' state = state
    { decoderState_ = case enc' of
        Right Nothing -> Right Nothing
        Right (Just enc) -> Left $ Right enc
        Left env -> Left $ Left env
    }