packages feed

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
    }