packages feed

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

{-|
Description:    Functions and objects used to build the tokenizer.

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

Stability:      provisional
Portability:    portable

The tokenizer stage is in a uniquely challenging position where the standard's
instructions are given as 'switch' statements (unlike
'Web.Mangrove.Parse.Encoding.Preprocess'), it operates over the output
---including errors and re-entrant state--- of a previous stage (unlike
'Web.Mangrove.Parse.Encoding'), and the conceptual input is of a type, 'Char',
with many constructors and a lot of standard library support (unlike
'Web.Mangrove.Parse.Tree').  That intersection means that it has proven best to
abstract the parser constructors themselves, rather than to abstract the
predicate tests and/or the combinator functions as in the other stages.
-}
module Web.Mangrove.Parse.Tokenize.Common
    ( -- * Types
      -- ** Parser
      Tokenizer
    , TokenizerState ( .. )
    , TokenParserState ( .. )
    , decoderState
    , decoderDefaultState
    , CurrentTokenizerState ( .. )
    , defaultTokenizerState
      -- *** Output
      -- **** Tokenizer
    , TokenizerOutput ( .. )
    , mapErrs
    , continueState
    , endState
    , finalStateList
    , Wrapped
    , WrappedOutput
    , WrappedOutputs
      -- **** Decoder
    , TokenizerInput ( .. )
    , DecoderOutputState
    , decodedRemainder
    , setRemainder
      -- ** Data
    , Token ( .. )
    , DoctypeParams ( .. )
    , emptyDoctypeParams
    , TagParams ( .. )
    , emptyTagParams
    , BasicAttribute
      -- * Parser building
      -- ** Input
      -- *** Single token
    , tokenizer
    , if_
    , ifChar
    , else_
    , elseChar
      -- *** Token list
    , tokenizers
    , ifs_
    , ifsChar
    , elses_
    , elsesChar
      -- *** Reconsume input
    , ifPush_
    , ifPushChar
    , elsePush_
    , elsePushChar
      -- ** Output
    , packToken
    , packState
    , emit
    , emit'
    , consEmit
    , consTokenError
    , consTokenErrors
    , consTokenErrorsList
    , consOut
    , consOuts
      -- ** Combinators
    , appropriateEndTag
    , changeState
    , chunk'
    ) where


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

import qualified Data.ByteString as BS
import qualified Data.ByteString.Short as BS.SH
import qualified Data.Either as E
import qualified Data.HashMap.Strict as M
import qualified Data.Maybe as Y
import qualified Data.Text as T

import Web.Willow.DOM

import Web.Mangrove.Parse.Common.Error
import Web.Willow.Common.Encoding hiding ( setRemainder )
import Web.Willow.Common.Encoding.Sniffer
import Web.Willow.Common.Parser
import Web.Willow.Common.Parser.Switch

import qualified Web.Willow.Common.Encoding as Willow

import Control.Applicative ( (<|>) )


-- | The smallest segment of data which carries semantic meaning.
data Token
    = Doctype DoctypeParams
        -- ^ __HTML:__
        --      @DOCTYPE token@
        -- 
        -- 'Web.Mangrove.DOM.DocumentType', describing the language used in the
        -- document.
    | StartTag TagParams
        -- ^ __HTML:__
        --      @start tag token@
        -- 
        -- 'Web.Mangrove.DOM.Element', marking the start of a markup section,
        -- or a point of markup which (per the specification) doesn't contain
        -- any content.
    | EndTag TagParams
        -- ^ __HTML:__
        --      @end tag token@
        -- 
        -- 'Web.Mangrove.DOM.Element' with a @'/'@ character before the name,
        -- marking the end of a section opened by 'StartTag'.
    | Comment T.Text
        -- ^ __HTML:__
        --      @comment token@
        -- 
        -- 'Web.Mangrove.DOM.Comment', marking author's notes or other text
        -- about the document itself, rather than being part of the content.
    | Character Char
        -- ^ __HTML:__
        --      @character token@
        -- 
        -- 'Web.Mangrove.DOM.Character', usually containing (a small portion
        -- of) text which should rendered for the user or included in the
        -- header metadata, but occasionally subject to further processing
        -- (i.e. the content of @\<script\>@ or @\<style\>@ sections).
    | EndOfStream
        -- ^ __HTML:__
        --      @end-of-file token@
        -- 
        -- Represents both an explicit mark of the end of the stream, when a
        -- simple @[]@ doesn't suffice, and provides a seat to carry
        -- 'ParseError's if no other token is emitted at the same time.
        -- 
        -- Note: the former role doesn't have any guarantees; a stream can end
        -- without an 'EndOfStream' termination, and 'EndOfStream' tokens can occur in
        -- places other than the end of the file.
  deriving ( Eq, Show, Read )


-- | __HTML:__
--      the data associated with a @doctype token@
-- 
-- All data comprising a document type declaration which may be obtained
-- directly from the raw document stream.  Values may be easily instantiated as
-- updates to 'emptyDoctypeParams'.
data DoctypeParams = DoctypeParams
    { doctypeName :: Maybe T.Text
        -- ^ The root element of the document, which may also identify the
        -- primary language used.
    , doctypePublicId :: Maybe T.Text
        -- ^ A globally-unique reference to the definition of the language.
    , doctypeSystemId :: Maybe T.Text
        -- ^ A system-dependant (but perhaps easier to access) reference to the
        -- definition of the language.
    , doctypeQuirks :: Bool
        -- ^ Whether the document should be read and rendered in a
        -- backwards-compatible manner, even if the other data in the token
        -- would match that expected by the specification.  Note that 'False'
        -- value is still subject to those expectations; this just provides an
        -- override in the case of, for example, a malformed declaration.
    }
  deriving ( Eq, Show, Read )

-- | A sane default collection for easy record initialization; namely,
-- 'Nothing's and 'False'.
emptyDoctypeParams :: DoctypeParams
emptyDoctypeParams = DoctypeParams
    { doctypeName = Nothing
    , doctypePublicId = Nothing
    , doctypeSystemId = Nothing
    , doctypeQuirks = False
    }


-- | __HTML:__
--      the data associated with a @start tag@ or an @end tag token@
-- 
-- All data comprising a markup tag which may be obtained directly from the raw
-- document stream.  Values may be easily instantiated as updates to
-- 'emptyTagParams'.
data TagParams = TagParams
    { tagName :: ElementName
        -- ^ The primary identifier of the markup tag, defining its behaviour
        -- during rendering, and providing a means of matching opening tags
        -- with closing ones.
    , tagIsSelfClosing :: Bool
        -- ^ Whether the tag was closed at the same point it was opened,
        -- according to the XML-style "@/>@" syntax.  HTML null elements are
        -- handled in the tree construction stage instead.
    , tagAttributes :: M.HashMap T.Text T.Text
        -- ^ Finer-grained metadata attached to the markup tag.
    }
  deriving ( Eq, Show, Read )

-- | A sane default collection for easy record initialization.
emptyTagParams :: TagParams
emptyTagParams = TagParams
    { tagName = T.empty
    , tagIsSelfClosing = False
    , tagAttributes = M.empty
    }


-- | Parser combinators written over the output of the
-- 'Web.Mangrove.Parse.Encoding.decoder' stage, segmenting the raw 'Char'
-- strings into semantic atoms.
type Tokenizer = StateParser TokenParserState [TokenizerInput]


-- | The collection of data required to extract a list of semantic atoms from a
-- binary document stream.  Values may be easily instantiated as updates to
-- 'defaultTokenizerState'.
data TokenizerState = TokenizerState
    { tokenParserState :: TokenParserState
        -- ^ The state of the current 'Web.Mangrove.Parse.Tokenize.tokenize'
        -- stage.
    , decoderState_ :: Either (Either SnifferEnvironment Encoding) (Maybe DecoderState)
        -- ^ The state of the previous 'Web.Mangrove.Parse.Encoding.decoder'
        -- stage, or the data used to initialize it.  For easy access to the
        -- 'DecoderState' itself, see 'decoderState'.
    }
  deriving ( Eq, Show, Read )

-- | All the data which needs to be tracked for correct behaviour in the
-- tokenization stage.
data TokenParserState = TokenParserState
    { prevStartTag :: Maybe ElementName
        -- ^ __HTML:__
        --      @[appropriate end tag token]
        --      (https://html.spec.whatwg.org/multipage/parsing.html#appropriate-end-tag-token)@
        -- 
        -- Certain states in the parser, which only emit 'Character' tokens,
        -- are able to collapse multiple tags which result in that behaviour by
        -- comparing a potential closing markup tag to the name on the
        -- 'StartTag' token which triggered the state.
    , currentState :: CurrentTokenizerState
        -- ^ The set of rules currently active in the state machine.
    , currentNodeNamespace :: Maybe Namespace
        -- ^ Certain states in the parser change behaviour if the @adjusted
        -- current node@ is not an HTML element.  Given the direction of
        -- visibility in the parser stack, this stage can't directly access
        -- that (tree construction level) datum, and so that needs to be
        -- tracked redundently.
    , atEndOfStream :: Bool
        -- ^ Whether the current input stream is known to be the final part of
        -- the document stream ('True') or whether additional input may still
        -- follow ('False') and thus any finalization should not be performed.
    }
  deriving ( Eq, Show, Read )

-- | The various fixed points in the tokenization algorithm, where the parser
-- may break and re-enter seamlessly.
data CurrentTokenizerState
    = DataState
        -- ^ __HTML:__
        --      @[data state]
        --      (https://html.spec.whatwg.org/multipage/parsing.html#data-state)@
        -- 
        -- The core rules, providing the most common tokenization behaviour.
    | RCDataState
        -- ^ __HTML:__
        --      @[RCDATA state]
        --      (https://html.spec.whatwg.org/multipage/parsing.html#rcdata-state)@
        -- 
        -- 'Character'-focused production while, unlike 'RawTextState',
        -- resolving character reference values.
    | RawTextState
        -- ^ __HTML:__
        --      @[RAWTEXT state]
        --      (https://html.spec.whatwg.org/multipage/parsing.html#rawtext-state)@
        -- 
        -- 'Character'-focused production which, unlike 'RCDataState', passes
        -- character reference sequences unchanged.
    | PlainTextState
        -- ^ __HTML:__
        --      @[PLAINTEXT state]
        --      (https://html.spec.whatwg.org/multipage/parsing.html#plaintext-state)@
        -- 
        -- Blind conversion of the entire document stream into 'Character'
        -- tokens.
    | ScriptDataState
        -- ^ __HTML:__
        --      @[script data state]
        --      (https://html.spec.whatwg.org/multipage/parsing.html#script-data-state)@
        -- 
        -- 'Character'-focused production according to the (occasionally
        -- complex) rules governing the handling of @\<script\>@ contents.
    | ScriptDataEscapedState
        -- ^ __HTML:__
        --      @[script data escaped state]
        --      (https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-state)@
        -- 
        -- 'Character'-focused production for data within a @\<!--@ / @--\>@
        -- section within 'ScriptDataState'.
    | ScriptDataDoubleEscapedState
        -- ^ __HTML:__
        --      @[script data double escaped state]
        --      (https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-state)@
        -- 
        -- 'Character'-focused production for data within a @\<script\>@
        -- section within 'ScriptDataEscapedState'.
    | CDataState
        -- ^ __HTML:__
        --      @[CDATA section state]
        --      (https://html.spec.whatwg.org/multipage/parsing.html#cdata-section-state)@
        -- 
        -- 'Character'-focused production for data within a foreign @\<[CDATA[@
        -- / @]]\>@ escape section.
  deriving ( Eq, Ord, Bounded, Enum, Show, Read )

-- | A sane default collection for easy record initialization; namely,
-- interpret the binary stream as 'Utf8' in the primary 'DataState'.
defaultTokenizerState :: TokenizerState
defaultTokenizerState = TokenizerState
    { decoderState_ = Left $ Right Utf8
    , tokenParserState = TokenParserState
        { prevStartTag = Nothing
        , currentState = DataState
        , currentNodeNamespace = Nothing
        , atEndOfStream = False
        }
    }


-- | The state of the previous 'Web.Mangrove.Parse.Encoding.decoder' stage.
-- Note that the high-level conceptual view of the parser stack is of each
-- stage moving along the 'BS.ByteString' as a (more or less) unified front,
-- rather than each stage independently running over the output of the
-- previous.
decoderState :: TokenizerState -> Maybe DecoderState
decoderState = E.fromRight Nothing . decoderState_

-- | As 'decoderState', but generating a default initial state if it doesn't
-- yet exist.
decoderDefaultState :: TokenizerState -> BS.ByteString -> Maybe DecoderState
decoderDefaultState state stream = case decoderState_ state of
    Right dState -> dState
    Left initialize -> Just $ either (flip sniffDecoderState stream) initialDecoderState initialize


-- | __HTML:__
--      @[appropriate end tag token]
--      (https://html.spec.whatwg.org/multipage/parsing.html#appropriate-end-tag-token)@
-- 
-- Check whether the last 'StartTag' token emitted by the tokenizer has a name
-- that matches the given 'String'.
appropriateEndTag :: String -> Tokenizer Bool
appropriateEndTag testName = do
    prevName <- prevStartTag <$> N.S.get
    return $ prevName == Just (T.pack testName)


-- | Prepare the tokenizer to process the /next/ input 'Char' according to the
-- given set of instructions.
changeState :: CurrentTokenizerState -> Tokenizer ()
changeState newState = do
    state <- N.S.get
    N.S.put $ state
        { currentState = newState
        }


-- | All data with which to initialize the decoder, to resume as if the state
-- machine transition hadn't been interrupted.  If 'Nothing', the associated
-- 'Char' was emitted in the 'init' of several at once; in this case, the
-- decoder can't be re-entered in exactly the same place with the data wrapped
-- in this type, and so the stream must continue to be processed until the
-- first 'Just' value.
type DecoderOutputState = Maybe (Maybe DecoderState, BS.ByteString)


-- | A smart wrapper over 'switch' to provide an end-of-stream recovery.  See
-- 'tokenizers' if multiple objects may be returned at once.
tokenizer
    :: Maybe ([ParseError], out)
        -- ^ The return value to use when the stream is empty.
    -> [SwitchCase TokenizerInput Tokenizer (WrappedOutput out)]
        -- ^ If the return value is paired with 'True', the final state of the
        -- input stream is allowed as a fallback; if 'False' (i.e., something
        -- has been 'push'ed back for reconsumption) the returned state is used
        -- as-is, even if that doesn't result in a valid fixed breakpoint.
    -> Tokenizer (TokenizerOutput out)
tokenizer Nothing cases = next >>= switch' cases
tokenizer (Just (errs, out)) cases = (next >>= switch' cases) <|> do
    eos <- atEndOfStream <$> N.S.get
    if eos
        then end *> return recovery
        else A.empty
  where recovery = TokenizerOutput
            { tokenizedErrs = errs
            , tokenizedOut = out
            , tokenizedState = endState
            }

-- | As 'switch', but with an internal marker to indicate whether the final
-- state of the input stream may be retrieved from the given value, or whether
-- it must only reflect the generated output.  See @switches'@ if multiple
-- objects may be returned at once.
switch'
    :: [SwitchCase TokenizerInput Tokenizer (WrappedOutput out)]
    -> TokenizerInput
    -> Tokenizer (TokenizerOutput out)
switch' cases t' = mapState' <$> switch cases t'
  where mapState' (True, out) = mapState (<|> decodedState t') out
        mapState' (False, out) = out


-- | A smart wrapper over 'switch' to provide an end-of-stream recovery.  See
-- 'tokenizer' if only a single object may be returned.
tokenizers
    :: Maybe [([ParseError], out)]
        -- ^ The return values to use when the stream is empty.
    -> [SwitchCase TokenizerInput Tokenizer (WrappedOutputs out)]
        -- ^ If the return value is paired with 'True', the final state of the
        -- input stream is allowed as a fallback; if 'False' (i.e., something
        -- has been 'push'ed back for reconsumption) the returned state is used
        -- as-is, even if that doesn't result in a valid fixed breakpoint.
    -> Tokenizer [TokenizerOutput out]
tokenizers Nothing cases = next >>= switches' cases
tokenizers (Just recovery) cases = (next >>= switches' cases) <|> do
    eos <- atEndOfStream <$> N.S.get
    if eos
        then end *> return recovery'
        else A.empty
  where recovery' = finalStateList endState . flip map recovery $ \(errs, out) ->
            TokenizerOutput errs out Nothing

-- | As 'switch', but with an internal marker to indicate whether the final
-- state of the input stream may be retrieved from the given value, or whether
-- it must only reflect the generated output.  See @switch'@ if only a single
-- object may be returned.
switches'
    :: [SwitchCase TokenizerInput Tokenizer (WrappedOutputs out)]
    -> TokenizerInput
    -> Tokenizer [TokenizerOutput out]
switches' cases t' = repack <$> switch cases t'
  where repack (_, []) = []
        repack (True, [out']) = [mapState (<|> decodedState t') out']
        repack (False, [out']) = [out']
        repack (useState, t:ts) = t : repack (useState, ts)


-- | Perform some operation on the list of errors associated with a wrapped
-- output value.
mapErrs :: ([ParseError] -> [ParseError]) -> TokenizerOutput out -> TokenizerOutput out
mapErrs f out = out { tokenizedErrs = f $ tokenizedErrs out }

-- | Perform some operation on the stream state associated with a wrapped
-- output value.
mapState
    :: (DecoderOutputState -> DecoderOutputState)
    -> TokenizerOutput out
    -> TokenizerOutput out
mapState f out = out { tokenizedState = f $ tokenizedState out }

-- | Indicate that the given wrapped value should never be used as a breakpoint
-- for future stages of the parser.
continueState :: TokenizerOutput out -> TokenizerOutput out
continueState = mapState $ const Nothing

-- | Mark the list of wrapped values such that any in the 'init' may not be
-- used as a breakpoint for future stages of the parser, and the 'last' has the
-- given 'tokenizedState'.
finalStateList :: DecoderOutputState -> [TokenizerOutput out] -> [TokenizerOutput out]
finalStateList _ [] = []
finalStateList state [t'] = [t' { tokenizedState = state }]
finalStateList state (t':ts') = continueState t' : finalStateList state ts'


-- | Wrap an output value with a placeholder state indicating that the value
-- may not be used as a tokenizer re-entry point.  If the value is a 'Token',
-- use 'emit' instead.
packToken :: ([ParseError], out) -> Tokenizer (TokenizerOutput out)
packToken = flip packState Nothing

-- | Wrap an output value such that parsing will continue with the given binary
-- document remainder if 'Just', or such that it can not serve as a breakpoint
-- with 'Nothing'.
packState :: ([ParseError], out) -> DecoderOutputState -> Tokenizer (TokenizerOutput out)
packState (errs, out) dState = return $ TokenizerOutput
    { tokenizedErrs = errs
    , tokenizedOut = out
    , tokenizedState = dState
    }


-- | Wrap a semantic atom and associated errors into the form the tokenizing
-- parsers expect to output.  See 'packToken' if the 'snd' item in the tuple is
-- not a 'Token', or 'emit'' to easily wrap the output as a singleton list.
emit :: ([ParseError], Token) -> Tokenizer (TokenizerOutput Token)
emit t'@(_, StartTag d) = do
    state <- N.S.get
    N.S.put $ state
        { prevStartTag = Just $ tagName d
        }
    packToken t'
emit t'@(_, EndTag d) = consTokenErrors errs <$> packToken t'
  where errs = Y.catMaybes [attrError, closeError]
        attrError
            | null (tagAttributes d) = Nothing
            | otherwise = Just EndTagWithAttributes
        closeError
            | tagIsSelfClosing d = Just EndTagWithTrailingSolidus
            | otherwise = Nothing
emit t' = packToken t'

-- | Wrap a semantic atom and associated errors into the form the tokenizing
-- parsers expect to output, and pack it as a singleton list.  See 'packToken'
-- if the 'snd' item in the tuple is not a 'Token', or 'emit' if the outer list
-- is unnecessary.
emit' :: ([ParseError], Token) -> Tokenizer [TokenizerOutput Token]
emit' = fmap (: []) . emit

-- | Wrap a semantic atom and associated errors into the form the tokenizing
-- parsers expect to output, and add it at the head of a list of the same
-- generated by a separate parser function.
consEmit
    :: ([ParseError], Token)
    -> Tokenizer [TokenizerOutput Token]
    -> Tokenizer [TokenizerOutput Token]
consEmit tok p = do
    t <- emit tok
    ts <- p
    return $ t : ts


-- | The collection of data returned by the "Web.Mangrove.Parse.Decode"
-- stage, and so comprising the input to the tokenizer.
data TokenizerInput = TokenizerInput
    { decodedErrs :: [ParseError]
        -- ^ Any authoring errors detected during decoding.
    , decodedOut :: Char
        -- ^ The decoded character itself.
    , decodedState :: DecoderOutputState
        -- ^ The data required to resume decoding immediately following the
        -- value, if possible.  See also 'decodedRemainder'.
    }
  deriving ( Eq, Show, Read )

-- | The unparsed portion of the binary stream, /after/ parsing the associated
-- 'Char'.  See also 'decodedState'.
decodedRemainder :: TokenizerInput -> Maybe BS.ByteString
decodedRemainder = fmap snd . decodedState


-- | Store the given binary sequence as unparsable without further input, to be
-- prepended to the beginning of stream on the next call to
-- 'Web.Mangrove.Parse.Tokenize.tokenize'.
setRemainder :: BS.SH.ShortByteString -> TokenizerState -> TokenizerState
setRemainder bs state = state
    { decoderState_ = Right $ Willow.setRemainder bs <$> decoderDefaultState state BS.empty
    }


-- | The standard output of parsers used in the tokenization stage.
-- Specifically, it contains the final state of the decoder stage in addition
-- to the generated value, to enable the recursion loop to detect the end of
-- multi-'Char' outputs and properly update the resume state.  May be easily
-- instantiated through calls to 'packToken' and 'packState'.
data TokenizerOutput out = TokenizerOutput
    { tokenizedErrs :: [ParseError]
        -- ^ Any authoring errors detected during decoding or tokenization.
    , tokenizedOut :: out
        -- ^ The point of data specifically generated by the parser.
    , tokenizedState :: DecoderOutputState
        -- ^ The data required to resume tokenization immediately following the
        -- value, if possible.
    }
  deriving ( Eq, Show, Read )

instance Functor TokenizerOutput where
    fmap f out = out { tokenizedOut = f $ tokenizedOut out }


-- | Type-level abstraction for values output by any tokenize-stage parser,
-- along with the flag required to determine the proper remainder of the input
-- stream to return.
type Wrapped out = (Bool, out)

-- | Type-level abstraction for single values output by a tokenizer, along with
-- the flag required to determine the proper remainder of the input stream to
-- return.
type WrappedOutput out = Wrapped (TokenizerOutput out)

-- | Type-level abstraction for multiple values output by a tokenizer, along
-- with the flag required to determine the proper remainder of the input stream
-- to return.
type WrappedOutputs out = Wrapped [TokenizerOutput out]


-- | Wrap an 'If_' clause returning a single value, with a more user-friendly
-- predicate type than would otherwise be required by a raw 'SwitchCase' in the
-- tokenizer stage.
if_
    :: (Char -> Bool)
    -> Tokenizer (TokenizerOutput out)
    -> SwitchCase TokenizerInput Tokenizer (WrappedOutput out)
if_ f t = If (f . decodedOut) $ \c' ->
    (,) True . mapErrs (decodedErrs c' ++) <$> t

-- | Wrap an 'If_' clause returning a list of semantic atoms, with a more
-- user-friendly predicate type than would otherwise be required by a raw
-- 'SwitchCase' in the tokenizer stage.
ifs_
    :: (Char -> Bool)
    -> Tokenizer [TokenizerOutput Token]
    -> SwitchCase TokenizerInput Tokenizer (WrappedOutputs Token)
ifs_ f t = If (f . decodedOut) $ \c' ->
    (,) True . consTokenErrorsList (decodedErrs c') <$> t


-- | Wrap an 'If' clause returning a single value, with more user-friendly
-- predicate and monad types than would otherwise be required by a raw
-- 'SwitchCase' in the tokenizer stage.
ifChar
    :: (Char -> Bool)
    -> (Char -> Tokenizer (TokenizerOutput out))
    -> SwitchCase TokenizerInput Tokenizer (WrappedOutput out)
ifChar f t = If (f . decodedOut) $ \c' ->
    (,) True . mapErrs (decodedErrs c' ++) <$> t (decodedOut c')

-- | Wrap an 'If' clause returning a list of semantic atoms, with more
-- user-friendly predicate and monad types than would otherwise be required by
-- a raw 'SwitchCase' in the tokenizer stage.
ifsChar
    :: (Char -> Bool)
    -> (Char -> Tokenizer [TokenizerOutput Token])
    -> SwitchCase TokenizerInput Tokenizer (WrappedOutputs Token)
ifsChar f t = If (f . decodedOut) $ \c' ->
    (,) True . consTokenErrorsList (decodedErrs c') <$> t (decodedOut c')


-- | Wrap an 'If_' clause returning a single value, with a more user-friendly
-- predicate type, and return the input character to the stream for
-- reconsumption.
ifPush_
    :: (Char -> Bool)
    -> Tokenizer out
    -> SwitchCase TokenizerInput Tokenizer (Wrapped out)
ifPush_ f t = If (f . decodedOut) $ \c' ->
    push c' *> fmap ((,) False) t

-- | Wrap an 'If' clause returning a single value, with more user-friendly
-- predicate and monad types, and return the input character to the stream for
-- reconsumption.
ifPushChar
    :: (Char -> Bool)
    -> (Char -> Tokenizer out)
    -> SwitchCase TokenizerInput Tokenizer (Wrapped out)
ifPushChar f t = If (f . decodedOut) $ \c' ->
    push c' *> fmap ((,) False) (t $ decodedOut c')


-- | Wrap an 'Else_' clause returning a single value, performing any relevant
-- back-end processing.
else_
    :: Tokenizer (TokenizerOutput out)
    -> SwitchCase TokenizerInput Tokenizer (WrappedOutput out)
else_ t = Else $ \c' ->
    (,) True . mapErrs (decodedErrs c' ++) <$> t

-- | Wrap an 'Else_' clause returning a list of semantic atoms, performing any
-- relevant back-end processing.
elses_
    :: Tokenizer [TokenizerOutput Token]
    -> SwitchCase TokenizerInput Tokenizer (WrappedOutputs Token)
elses_ t = Else $ \c' ->
    (,) True . consTokenErrorsList (decodedErrs c') <$> t


-- | Wrap 'Else' clause returning a single value, with a more user-friendly
-- monad input type than would otherwise be required by a raw 'SwitchCase' in
-- the tokenizer stage.
elseChar
    :: (Char -> Tokenizer (TokenizerOutput out))
    -> SwitchCase TokenizerInput Tokenizer (WrappedOutput out)
elseChar t = Else $ \c' ->
    (,) True . mapErrs (decodedErrs c' ++) <$> t (decodedOut c')

-- | Wrap an 'Else' clause returning a list of semantic atoms, with a more
-- user-friendly monad input type than would otherwise be required by a raw
-- 'SwitchCase' in the tokenizer stage.
elsesChar
    :: (Char -> Tokenizer [TokenizerOutput Token])
    -> SwitchCase TokenizerInput Tokenizer (WrappedOutputs Token)
elsesChar t = Else $ \c' ->
    (,) True . consTokenErrorsList (decodedErrs c') <$> t (decodedOut c')


-- | Wrap an 'Else_' clause returning a single value, performing any relevant
-- back-end processing, and return the input character to the stream for
-- reconsumption.
elsePush_
    :: Tokenizer out
    -> SwitchCase TokenizerInput Tokenizer (Wrapped out)
elsePush_ t = Else $ \c' ->
    push c' *> fmap ((,) False) t

-- | Wrap an 'Else' clause returning a single value, with a more user-friendly
-- monad type, and return the input character to the stream for reconsumption.
elsePushChar
    :: (Char -> Tokenizer out)
    -> SwitchCase TokenizerInput Tokenizer (Wrapped out)
elsePushChar t = Else $ \c' ->
    push c' *> fmap ((,) False) (t $ decodedOut c')


-- | Add the given 'ParseError' to the collection of warnings associated with
-- the wrapped value.
consTokenError :: ParseError -> TokenizerOutput out -> TokenizerOutput out
consTokenError err = mapErrs (err :)

-- | Add the given 'ParseError's to the collection of warnings associated with
-- the wrapped value.
consTokenErrors :: [ParseError] -> TokenizerOutput out -> TokenizerOutput out
consTokenErrors errs = mapErrs (errs ++)

-- | Add the given 'ParseError's to the collection of warnings associated with
-- the first wrapped value in the list, or create a placeholder if it does not
-- yet contain any 'Token's.
consTokenErrorsList :: [ParseError] -> [TokenizerOutput Token] -> [TokenizerOutput Token]
consTokenErrorsList [] [] = []
consTokenErrorsList errs [] = [TokenizerOutput errs EndOfStream Nothing]
consTokenErrorsList errs (t:ts) = consTokenErrors errs t : ts


-- | Push the given value to the head of the wrapped collection.
-- 
-- @
-- consOut x == fmap (x :)
-- @
consOut :: out -> TokenizerOutput [out] -> TokenizerOutput [out]
consOut x = fmap (x :)

-- | Push the given values to the head of the wrapped collection.
-- 
-- @
-- consOut xs == fmap (xs ++)
-- @
consOuts :: [out] -> TokenizerOutput [out] -> TokenizerOutput [out]
consOuts xs = fmap (xs ++)


-- | Lift a call to 'chunk' over the wrapped 'Char's which make up the input to
-- the tokenization stage.  As an additional quality-of-life, perform some
-- function on the input 'String' before comparing it to the desired value; for
-- behaviour according to the base 'chunk' function, use @'chunk'' 'id'@.
chunk' :: (Char -> Char) -> String -> Tokenizer [TokenizerInput]
chunk' f test = nextChunk (fromIntegral $ length test) >>=
    satisfying (\str -> map (f . decodedOut) str == test)


-- | Produce a placeholder value for use in generating a 'TokenizerOutput'
-- value located at the end of the byte stream.
endState :: DecoderOutputState
endState = Just (Nothing, BS.empty)