mangrove-0.1.0.0: src/Web/Mangrove/Parse/Tree.hs
{-# HLINT ignore "Redundant flip" #-}
{-|
Description: Fold a linear, semantic stream into a tree structure.
Copyright: (c) 2020-2021 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 "Tree Construction"
section of the
__[HTML](https://html.spec.whatwg.org/multipage/parsing.html#tree-construction)__
document parsing specification, operating over the output of the
"Web.Mangrove.Parse.Tokenize" stage to produce a DOM tree representation of a
web page. As this library is still in the early stages of development, the
representation produced here is not actually a proper DOM implementation, but
instead only stores basic parameters in an equivalent (but less-featured)
structure. Nonetheless, it is still enough for basic evaluation and unstyled
rendering.
-}
module Web.Mangrove.Parse.Tree
( -- * Types
-- ** Final
Tree ( .. )
, Node ( .. )
, QuirksMode ( .. )
-- ** Intermediate
, Patch
, TreeState
, Encoding ( .. )
, NodeIndex
, ElementParams ( .. )
, emptyElementParams
-- * Initialization
, defaultTreeState
, treeEncoding
, treeFragment
, treeInIFrame
-- * Transformations
, tree
, treeStep
, finalizeTree
) where
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.List as L
import qualified Data.Maybe as Y
import qualified Data.Text as T
import Web.Willow.DOM
import Web.Mangrove.Parse.Common.Error
import Web.Mangrove.Parse.Tokenize
import Web.Mangrove.Parse.Tokenize.Common
import Web.Mangrove.Parse.Tree.Common
import Web.Mangrove.Parse.Tree.Dispatcher
import Web.Mangrove.Parse.Tree.Patch
import Web.Mangrove.Parse.Tree.Patch.Fold
import Web.Willow.Common.Encoding hiding ( setRemainder )
import Web.Willow.Common.Encoding.Sniffer
import Web.Willow.Common.Parser
-- | __HTML:__
-- @[tree construction]
-- (https://html.spec.whatwg.org/multipage/parsing.html#tree-construction)@
--
-- Given a starting environment, transform a binary document stream into a
-- hierarchical markup tree. If the parse fails, returns an empty tree (a
-- 'Document' node with no children).
tree :: TreeState -> BS.ByteString -> ([Patch], TreeState)
tree state stream = L.foldl' treeFold ([], state) $ L.unfoldr treeUnfold (state, stream)
where treeUnfold = unfoldLoop treeStep tokenizerState
treeFold (ps, _) (ps', state', _) = (ps ++ ps', state')
-- | Wrap a parser in a signature appropriate to pass to 'L.unfoldr'.
unfoldLoop
:: Eq state
=> (state -> BS.ByteString -> ([out], state, BS.ByteString))
-- ^ The single-step parser function.
-> (state -> TokenizerState)
-- ^ Repack the state used by the step parser into a standardized form.
-> (state, BS.ByteString)
-- ^ The initial inputs to the parser.
-> Maybe (([out], state, BS.ByteString), (state, BS.ByteString))
unfoldLoop step toTokState (state, stream)
| BS.null stream = Nothing
| otherwise = case step state stream of
out@(_, state', _) | state /= state' || hasRemainder state' -> continueUnfold out
([], _, stream') | BS.null stream' -> Nothing
out -> continueUnfold out
where continueUnfold (ps, state', stream') = Just ((ps, state', stream'), (state', stream'))
hasRemainder = maybe False (not . BS.SH.null . decoderRemainder) . decoderState . toTokState
-- | Parse a minimal number of tokens from a binary document stream, into a
-- state-independent sequence of folding instructions. Returns all data
-- required to seamlessly resume parsing.
treeStep :: TreeState -> BS.ByteString -> ([Patch], TreeState, BS.ByteString)
treeStep state stream = treeStep' stream' stateRemainder state
where stateRemainder state' = state'
{ tokenizerState = setRemainder (BS.SH.toShort stream) $ tokenizerState state'
}
stream' = L.unfoldr tokenUnfold (tokenizerState state, stream)
tokenUnfold = unfoldLoop tokenizeStep id
-- | Parse a minimal number of tokens from a binary document stream, performing
-- any backend processing required to correctly generate the document tree.
treeStep'
:: [([([ParseError], Token)], TokenizerState, BS.ByteString)]
-> (TreeState -> TreeState)
-> TreeState
-> ([Patch], TreeState, BS.ByteString)
treeStep' input fallback state =
case runParserT (N.S.runStateT recurse $ treeParserState state) stream of
Just (((ps, tokState, stream'), parserState), _) -> (ps', state', stream')
where state' = TreeState
{ tokenizerState = tokState
, treeParserState = parserState
}
ps' = map redirectPatches ps
Nothing -> ([], fallback state, BS.empty)
where stream = L.foldr repackStream [] input
redirectPatches p = case fragmentContext $ treeParserState state of
-- A fragment with an @\<html\>@ context won't create that node in
-- the folded tree, and so any patches sent there won't find an
-- anchor.
Just ctx | nodeIsElement (T.pack "html") $ fst ctx -> case p of
InsertComment errs InDocument _ -> ErrorList errs
InsertComment errs InHtmlElement txt -> InsertComment errs InDocument txt
AddAttribute InDocument _ -> ErrorList []
AddAttribute InHtmlElement attr -> AddAttribute InDocument attr
_ -> p
-- The fragment parsing algorithm returns the children of the
-- context node, and so any patches destined for locations above
-- that shouldn't show up in the resulting tree.
Just _ -> case p of
InsertComment errs InDocument _ -> ErrorList errs
InsertComment errs InHtmlElement _ -> ErrorList errs
AddAttribute InDocument _ -> ErrorList []
AddAttribute InHtmlElement _ -> ErrorList []
_ -> p
_ -> p
-- | Explicitly indicate that the input stream will not contain any further
-- bytes, and perform any finalization processing based on that.
finalizeTree :: [Patch] -> TreeState -> Tree
finalizeTree ps state = buildTree $ ps ++ ps'
where (ps', _, _) = treeStep' [(ts, setRemainder BS.SH.empty tokState, BS.empty)] id state
tokState = tokenizerState state
ts = finalizeTokenizer tokState ++ [([], EndOfStream)]
-- | Specify the encoding scheme a given parse environment should use to read
-- from the binary document stream. Note that this will always use the initial
-- state for the respective decoder; intermediate states as returned by
-- 'decodeStep' are not supported.
treeEncoding :: Either SnifferEnvironment (Maybe Encoding) -> TreeState -> TreeState
treeEncoding enc state = state
{ tokenizerState = tokenizerEncoding enc $ tokenizerState state
}
-- | __HTML:__
-- @[fragment parsing algorithm]
-- (https://html.spec.whatwg.org/multipage/parsing.html#html-fragment-parsing-algorithm)@
--
-- Transform a given parse environment by adding context for an embedded but
-- separate document fragment. Calling this with an intermediate state
-- returned by 'treeStep' (as opposed to an initial state from
-- 'defaultTreeState') may result in an unexpected tree structure.
treeFragment
:: ElementParams
-- ^ __HTML:__
-- @[context element]
-- (https://html.spec.whatwg.org/multipage/parsing.html#concept-frag-parse-context)@
--
-- The node wrapping -- in one way or another -- the embedded document fragment.
-> [(NodeIndex, ElementParams)]
-- ^ The ancestors of the context element, most immediate first.
-> Maybe QuirksMode
-- ^ The degree of backwards compatibility used in the node document of
-- the context element, if it can be determined.
-> Maybe Bool
-- ^ Whether the node document of the context element has been parsed
-- in a way which would require scripting to be enabled (@'Just'
-- 'True'@) or disabled (@'Just' 'False'@).
-> TreeState
-> TreeState
treeFragment ctxNode ctxTree ctxQuirks ctxScript state = state
{ tokenizerState =
let mode = if elementNamespace ctxNode == Just htmlNamespace
then case T.unpack $ elementName ctxNode of
"title" -> RCDataState
"textarea" -> RCDataState
"style" -> RawTextState
"xmp" -> RawTextState
"iframe" -> RawTextState
"noembed" -> RawTextState
"noframes" -> RawTextState
"script" -> ScriptDataState
"noscript" -> if ctxScript == Just True
then RawTextState
else DataState
"plaintext" -> PlainTextState
_ -> DataState
else DataState
in tokenizerStartTag (Just htmlNamespace) (T.pack "html") .
tokenizerMode mode $ tokenizerState state
, treeParserState = resetInsertionMode' $ (treeParserState state)
{ openElements = [(0, htmlElement)]
, elementIndex = ctxIndex + 1
, quirksMode = Y.fromMaybe NoQuirks ctxQuirks
, templateInsertionModes = if nodeIsElement (T.pack "template") ctxNode
then [InTemplate]
else []
, fragmentContext = Just (ctxNode, ctxTree)
, scriptingEnabled = Y.fromMaybe False ctxScript
, formElementPointer = fmap fst . L.find (nodeIsElement (T.pack "form") . snd) $
(ctxIndex, ctxNode) : ctxTree
}
}
where ctxIndex = foldr (max . fst) 0 ctxTree + 1
htmlElement = emptyElementParams
{ elementName = T.pack "html"
, elementNamespace = Just htmlNamespace
}
-- | Specify whether the given parse environment should be treated as if the
-- document were contained within the @srcdoc@ attribute of an @\<iframe\>@
-- element ('False' by default).
treeInIFrame :: Bool -> TreeState -> TreeState
treeInIFrame b state = state
{ treeParserState = (treeParserState state)
{ isInIFrameSrcDoc = b
}
}
-- | Given the output of 'tokenizeStep', rewrap the token list and single state
-- into a single uniform stream. The final returned tuple will have a 'Just'
-- value containing the single state, while every other element has 'Nothing'
-- (indicating a point where reentry is impossible).
repackStream
:: ([([ParseError], Token)], TokenizerState, BS.ByteString)
-> [TreeInput]
-> [TreeInput]
repackStream ([], _, _) is = is
repackStream (ts, state, stream) is = case reverse ts of
-- Be sure any 'EndOfStream' tokens encountered mid-stream don't break parsing.
((errs, EndOfStream):ts') -> case is of
[] -> repackStream' ts
(i:is') -> repackStream' (reverse ts') ++ consErrors errs i : is'
-- Any other token ending the segment, however, can simply be repacked.
_ -> repackStream' ts ++ is
where repackStream' = flip foldr [] $ \(errs, t) ts' -> TreeInput
{ tokenErrs = errs
, tokenOut = t
, tokenState = if null ts'
then Just (state, stream)
else Nothing
} : ts'
consErrors errs i = i
{ tokenErrs = errs ++ tokenErrs i
}
-- | Loop the tree dispatcher until it returns a patchset which happens to
-- coincide with a tokenizer 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 :: TreeBuilder ([Patch], TokenizerState, BS.ByteString)
recurse = do
out <- dispatcher
case treeState out of
-- The tokenizer can't provide a breakpoint at the current token.
Nothing -> do
(out', tokState', stream') <- recurse
return (treePatches out ++ out', tokState', stream')
-- We have a re-entrant state to seamlessly resume the tokenizer.
Just (tokState, stream) ->
return (treePatches out, tokState, stream)