{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ViewPatterns #-} {-# LANGUAGE CPP #-} -- | Module in charge of finding the various segment -- in an ASCII text and the various anchors. module Text.AsciiDiagram.Parser( ParsingState( .. ) , parseText , parseTextLines , extractTextZones , detectTagFromTextZone ) where #if !MIN_VERSION_base(4,8,0) import Data.Monoid( mempty ) import Control.Applicative( (<$>) ) #endif import Control.Monad( foldM, when ) import Control.Monad.State.Strict( State , execState , modify ) import qualified Data.Foldable as F import qualified Data.Map as M import qualified Data.Set as S import qualified Data.Text as T import qualified Data.Traversable as TT import qualified Data.Vector.Unboxed as VU import Linear( V2( .. ) ) import Text.AsciiDiagram.Geometry isAnchor :: Char -> Bool isAnchor c = c `VU.elem` anchors where anchors = VU.fromList "<>^vV+/\\*" anchorOfChar :: Char -> Anchor anchorOfChar '+' = AnchorMulti anchorOfChar '/' = AnchorFirstDiag anchorOfChar '\\' = AnchorSecondDiag anchorOfChar '>' = AnchorArrowRight anchorOfChar '<' = AnchorArrowLeft anchorOfChar '^' = AnchorArrowUp anchorOfChar 'V' = AnchorArrowDown anchorOfChar 'v' = AnchorArrowDown anchorOfChar '*' = AnchorBullet anchorOfChar _ = AnchorMulti isHorizontalLine :: Char -> Bool isHorizontalLine c = c `VU.elem` horizontalLineElements where horizontalLineElements = VU.fromList "-=" isVerticalLine :: Char -> Bool isVerticalLine c = c `VU.elem` verticalLineElements where verticalLineElements = VU.fromList ":|" isDashed :: Char -> Bool isDashed c = case c of ':' -> True '=' -> True _ -> False data ParsingState = ParsingState { anchorMap :: !(M.Map Point Anchor) , segmentSet :: !(S.Set Segment) , currentSegment :: !(Maybe Segment) , styleLine :: [(Int, T.Text)] } deriving Show emptyParsingState :: ParsingState emptyParsingState = ParsingState { anchorMap = mempty , segmentSet = mempty , currentSegment = Nothing , styleLine = mempty } type Parsing = State ParsingState type LineNumber = Int addAnchor :: Point -> Char -> Parsing () addAnchor p c = modify $ \s -> s { anchorMap = M.insert p (anchorOfChar c) $ anchorMap s } addSegment :: Segment -> Parsing () addSegment seg = modify $ \s -> s { segmentSet = S.insert seg $ segmentSet s } addStyleLine :: (Int, T.Text) -> Parsing () addStyleLine l = modify $ \s -> s { styleLine = l : styleLine s } continueHorizontalSegment :: Point -> Parsing () continueHorizontalSegment p = modify $ \s -> s { currentSegment = Just . update $ currentSegment s } where update Nothing = mempty { _segStart = p , _segEnd = p , _segKind = SegmentHorizontal } update (Just seg) = seg { _segEnd = p , _segKind = SegmentHorizontal } setHorizontaDashing :: Parsing () setHorizontaDashing = modify $ \s -> s { currentSegment = setDashed <$> currentSegment s } where setDashed seg = seg { _segDraw = SegmentDashed } stopHorizontalSegment :: Parsing () stopHorizontalSegment = modify $ \s -> s { segmentSet = inserter (currentSegment s) $ segmentSet s , currentSegment = Nothing } where inserter Nothing s = s inserter (Just seg) s = S.insert seg s continueVerticalSegment :: Maybe Segment -> Point -> Parsing (Maybe Segment) continueVerticalSegment Nothing p = return $ Just seg where seg = mempty { _segStart = p , _segEnd = p , _segKind = SegmentVertical } continueVerticalSegment (Just seg) p = return $ Just seg { _segEnd = p, _segKind = SegmentVertical } stopVerticalSegment :: Maybe Segment -> Parsing (Maybe a) stopVerticalSegment Nothing = return Nothing stopVerticalSegment (Just seg) = do addSegment seg return Nothing parseLine :: [Maybe Segment] -> (LineNumber, T.Text) -> Parsing [Maybe Segment] parseLine prevSegments (n, T.stripPrefix ":::" -> Just txt) = do addStyleLine (n, txt) return prevSegments parseLine prevSegments (lineNumber, txt) = do ret <- TT.mapM go $ zip3 [0 ..] prevSegments stringLine stopHorizontalSegment return ret where stringLine = T.unpack txt ++ repeat ' ' go (columnNumber, vertical, c) | isHorizontalLine c = do let point = V2 columnNumber lineNumber continueHorizontalSegment point when (isDashed c) $ setHorizontaDashing stopVerticalSegment vertical go (columnNumber, vertical, c) | isVerticalLine c = do let point = V2 columnNumber lineNumber dashingSet seg | isDashed c = seg { _segDraw = SegmentDashed } | otherwise = seg stopHorizontalSegment fmap dashingSet <$> continueVerticalSegment vertical point go (columnNumber, vertical, c) | isAnchor c = do let point = V2 columnNumber lineNumber addAnchor point c stopHorizontalSegment stopVerticalSegment vertical go (_, vertical, _) = do stopHorizontalSegment stopVerticalSegment vertical maximumLineLength :: [T.Text] -> Int maximumLineLength [] = 0 maximumLineLength lst = maximum $ T.length <$> lst parseTextLines :: [T.Text] -> ParsingState parseTextLines lst = flip execState emptyParsingState $ do let initialLine = replicate (maximumLineLength lst) Nothing lastVerticalLine <- foldM parseLine initialLine $ zip [0 ..] lst mapM_ stopVerticalSegment lastVerticalLine -- | Extract the segment information of a given text. parseText :: T.Text -> ParsingState parseText = parseTextLines . T.lines zoneFromLine :: (Int, T.Text) -> [TextZone] zoneFromLine (lineIndex, line) = eatSpaces 0 $ T.split (== ' ') line where eatSpaces ix lst = case lst of [] -> [] ("":rest) -> eatSpaces (ix + 1) rest _ -> createZoneFrom ix lst createZoneFrom ix = go ix where go endIdx [] | ix == endIdx = [] go _ [] = [TextZone (V2 ix lineIndex) $ T.drop ix line] go endIdx ("":rest) = zone : eatSpaces (endIdx + 1) rest where origin = V2 ix lineIndex zone = TextZone origin . T.drop ix $ T.take endIdx line go endIdx (x:xs) = go (endIdx + T.length x + 1) xs extractTextZones :: [T.Text] -> [TextZone] extractTextZones = F.concatMap zoneFromLine . zip [0 ..] detectTagFromTextZone :: [TextZone] -> ([TextZone], [TextZone]) detectTagFromTextZone zones = (concat foundTags, concat normalZones) where (foundTags, normalZones) = unzip $ fmap findTag zones findTag zone@(TextZone (V2 x y) txt) = case splitTags y x $ T.split (== ' ') txt of ([], _) -> ([], [zone]) tagsAndText -> tagsAndText splitTags _ _ [] = ([], []) splitTags y ix (thisTxt : rest) | tlength >= 3 && T.head thisTxt == '{' && T.last thisTxt == '}' = (TextZone (V2 ix y) tagText: afterTags, normalTexts) | otherwise = (afterTags, TextZone (V2 ix y) thisTxt : normalTexts) where tlength = T.length thisTxt tagText = T.init $ T.drop 1 thisTxt (afterTags, normalTexts) = splitTags y (ix + tlength + 1) rest