seonbi-0.1.0: src/Text/Seonbi/PairedTransformer.hs
{-# LANGUAGE ScopedTypeVariables #-}
module Text.Seonbi.PairedTransformer
( PairedTransformer (..)
, transformPairs
) where
import Data.Text hiding (break, reverse)
import Text.Seonbi.Html
-- | Settings for 'transformPairs'.
data PairedTransformer match = PairedTransformer
{ ignoresTagStack :: HtmlTagStack -> Bool
, matchStart :: [match] -> Text -> Maybe (match, Text, Text, Text)
, matchEnd :: Text -> Maybe (match, Text, Text, Text)
, areMatchesPaired :: match -> match -> Bool
, transformPair :: match -> match -> [HtmlEntity] -> [HtmlEntity]
}
-- | Some transformations should be done only if a start and an end are paired
-- like parentheses. These even usually can be nested. Even if there is
-- a start and an end they should not be paired unless they are sibling in
-- an HTML tree.
--
-- These kinds of scanning are easily turned highly stateful and imperative,
-- hence hard to debug. This base class provides the common logic between
-- these kinds of paired transformations so that an implementation class fill
-- several abstract methods triggered by the state machine.
transformPairs :: forall m . PairedTransformer m -> [HtmlEntity] -> [HtmlEntity]
transformPairs (PairedTransformer ignores start end arePaired transform) =
iter [] . normalizeText
where
iter :: [Unclosed m] -> [HtmlEntity] -> [HtmlEntity]
iter [] [] = []
iter stack [] = unstack stack
iter stack (x@HtmlText { tagStack = ts, rawText = txt } : xs) =
case (startMatch, endMatch) of
(Just captured, Nothing) ->
roll stack captured ts xs
(Nothing, Just captured@(m, _, _, _))
| Prelude.any ((`arePaired` m) . match) stack ->
unroll stack captured ts xs
(Just captured@(_, pre, _, _), Just captured'@(m', pre', _, _)) ->
if Data.Text.length pre >= Data.Text.length pre' &&
Prelude.any ((`arePaired` m') . match) stack
then unroll stack captured' ts xs
else roll stack captured ts xs
(Nothing, _) ->
case stack of
[] -> x : iter stack xs
s : ss -> iter (s { buffer = x : buffer s } : ss) xs
where
startMatch :: Maybe (m, Text, Text, Text)
startMatch = start (reverse $ fmap match stack) txt
endMatch :: Maybe (m, Text, Text, Text)
endMatch = end txt
iter (s@Unclosed {} : ss) (x : xs) =
iter (s { buffer = x : buffer s } : ss) xs
iter [] (x : xs) = x : iter [] xs
roll :: [Unclosed m]
-> (m, Text, Text, Text)
-> HtmlTagStack
-> [HtmlEntity]
-> [HtmlEntity]
roll [] (startMatch, pre, t, post) tagStack_ entities =
prependText tagStack_ pre $ iter
[Unclosed startMatch [HtmlText tagStack_ t]]
(normalizeText (prependText tagStack_ post entities))
roll (s : ss) (startMatch, pre, t, post) tagStack_ entities = iter
( Unclosed startMatch [HtmlText tagStack_ t]
: s { buffer = prependText tagStack_ pre $ buffer s }
: ss
)
(normalizeText (prependText tagStack_ post entities))
unroll :: [Unclosed m]
-> (m, Text, Text, Text)
-> HtmlTagStack
-> [HtmlEntity]
-> [HtmlEntity]
unroll stack (endMatch, pre, t, post) tagStack_ es =
case remainStack of
[] -> unrolled ++ iter [] remainEntities
s : ss -> iter
(s { buffer = reverse unrolled ++ buffer s } : ss)
remainEntities
where
prependText' :: Text -> [HtmlEntity] -> [HtmlEntity]
prependText' = prependText tagStack_
unrolled :: [HtmlEntity]
remainStack :: [Unclosed m]
(unrolled, remainStack) = case findPair endMatch stack of
(_, []) ->
([HtmlText tagStack_ (pre `append` t)], [])
(stack', s@Unclosed { match = startMatch } : ss) ->
let
buf = prependText' pre (unstack' stack' ++ buffer s)
buf' = prependText' t buf
buf'' = reverse buf'
transformed = if Prelude.any (ignores . tagStack) buf''
then buf''
else transform startMatch endMatch buf''
in
(transformed, ss)
remainEntities :: [HtmlEntity]
remainEntities = prependText' post es
findPair :: m -> [Unclosed m] -> ([Unclosed m], [Unclosed m])
findPair m = break (arePaired m . match)
unstack :: [Unclosed m] -> [HtmlEntity]
unstack = reverse . unstack'
unstack' :: [Unclosed m] -> [HtmlEntity]
unstack' [] = []
unstack' (Unclosed { buffer = b } : ss) = b ++ unstack' ss
prependText :: HtmlTagStack -> Text -> [HtmlEntity] -> [HtmlEntity]
prependText tagStack_ txt
| Data.Text.null txt = id
| otherwise = (HtmlText tagStack_ txt :)
data Unclosed match = Unclosed
{ match :: match
, buffer :: [HtmlEntity] -- in reverse order
}