packages feed

submark-0.3.0: src/Text/CommonMark/Sub.hs

{-# LANGUAGE DisambiguateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
module Text.CommonMark.Sub
    ( HeadingPattern (..)
    , HeadingTitlePattern (..)
    , Level
    , extractSection
    , flattenInlineNodes
    , matchesHeading
    ) where

import Prelude hiding (concat)

import CMark
import Data.Text (Text, concat, pack, strip, toLower)
import Text.Regex.TDFA
import Text.Regex.TDFA.Text

data HeadingPattern = HeadingPattern
    { headingLevel :: Level
    , caseInsensitive :: Bool
    , titlePattern :: HeadingTitlePattern
    } deriving (Eq, Ord, Show)

data HeadingTitlePattern
    = HeadingTitleRegex Text
    | HeadingTitleText Text
    deriving (Eq, Ord, Show)

extractSection :: HeadingPattern -> Node -> Node
extractSection pat@HeadingPattern { headingLevel } (Node pos DOCUMENT nodes) =
    Node pos DOCUMENT $ case headlessNodes of
        (x : xs) -> x : takeWhile isInSection xs
        [] -> []
  where
    isInSection :: Node -> Bool
    isInSection (Node _ (HEADING lv) _) = lv > headingLevel
    isInSection _ = True
    headlessNodes :: [Node]
    headlessNodes =
        dropWhile (not . matchesHeading pat) nodes
extractSection _ (Node pos _ _) = Node pos DOCUMENT []

matchesHeading :: HeadingPattern -> Node -> Bool
matchesHeading HeadingPattern { headingLevel
                              , caseInsensitive
                              , titlePattern
                              } = case titlePattern of
    HeadingTitleText text -> if caseInsensitive
        then \ case
            (Node _ (HEADING lv) nodes) ->
                lv == headingLevel &&
                    toLower (flattenInlineNodes nodes) == toLower text
            _ -> False
        else \ case
            (Node _ (HEADING lv) nodes) ->
                lv == headingLevel && flattenInlineNodes nodes == text
            _ -> False
    HeadingTitleRegex regexPat ->
        let compOption = CompOption
                { caseSensitive = not caseInsensitive
                , multiline = False
                , rightAssoc = True
                , newSyntax = True
                , lastStarGreedy = True
                }
            execOption = ExecOption { captureGroups = False }
            regex' = compile compOption execOption regexPat
        in
            case regex' of
                Left _ -> const False
                Right regex ->
                    \ case
                    (Node _ (HEADING lv) nodes) ->
                        lv == headingLevel &&
                            case execute regex (flattenInlineNodes nodes) of
                                Right (Just _) -> True
                                _ -> False
                    _ -> False

flattenInlineNodes :: [Node] -> Text
flattenInlineNodes =
    strip . concat . map flatten
  where
    flatten :: Node -> Text
    flatten (Node _ (TEXT text) _) = text
    flatten (Node _ LINEBREAK _) = pack "\n"
    flatten (Node _ (HTML_INLINE text) _) = text
    flatten (Node _ (CODE text) _) = text
    flatten (Node _ _ nodes) = flattenInlineNodes nodes