fluent-syntax-1.1.0: src/Language/Fluent/Parser/Whitespace.hs
module Language.Fluent.Parser.Whitespace where
import Control.Monad (void)
import Control.Monad.Extra (ifM, whenM, whenMaybeM, (&&^))
import Data.Attoparsec.Extra
import Data.Attoparsec.Text (Parser, atEnd, peekChar, takeWhile)
import Data.Text (Text)
import Data.Text qualified as Text
import Language.Fluent.AST
import Prelude hiding (takeWhile)
-- | Consumes a line end: a line break or the end of the input.
expectLineEnd :: Parser ()
expectLineEnd =
peekChar >>= \case
Nothing -> pure ()
Just '\n' -> advance
Just '\r' -> ifM (secondIs '\n') (advance *> advance) notLineEnd
_ -> notLineEnd
where
notLineEnd :: Parser ()
notLineEnd = parseError "Expected token: end of line"
-- | Whether a line end is next.
atLineEnd :: Parser Bool
atLineEnd =
peekChar >>= \case
Nothing -> pure True
Just '\n' -> pure True
Just '\r' -> secondIs '\n'
_ -> pure False
-- | Consumes a line break, and tells whether there was one. The end of the input is not one.
consumeNewline :: Parser Bool
consumeNewline =
peekChar >>= \case
Just '\n' -> True <$ advance
Just '\r' -> secondIs '\n' &&^ (True <$ (advance *> advance))
_ -> pure False
-- | Skips the spaces before the next character.
skipBlankInline :: Parser ()
skipBlankInline = void $ takeWhile (== ' ')
-- | Skips spaces and line breaks.
skipBlank :: Parser ()
skipBlank = skipBlankInline *> whenM consumeNewline skipBlank
-- | Skips lines of only spaces, and tells how many.
skipBlankBlock :: Parser Int
skipBlankBlock = length <$> whileJust (attemptMaybe blankLine)
where
blankLine = skipBlankInline *> whenMaybeM consumeNewline (pure ())
-- | Like 'skipBlankBlock', and skips spaces that end the input.
skipBlankBlockToEnd :: Parser Int
skipBlankBlockToEnd =
skipBlankBlock <* attemptMaybe (skipBlankInline *> whenMaybeM atEnd (pure ()))
-- | Applies <https://projectfluent.org/fluent/guide/multiline multi-line rules>
-- to the raw pattern elements.
dedent :: [PatternElement] -> [PatternElement]
dedent elements =
onLast (mapText $ Text.dropWhileEnd (`elem` blanks))
. onFirst dropLeadingBreaks
$ strip <$> elements
where
newlines = "\r\n" :: String
blanks = " \r\n" :: String
commonIndent :: Int
commonIndent =
minimum $
maxBound
: [indentWidth t | BlockText t <- elements]
<> [indent | Placeable (BlockPlaceable indent _) <- elements]
indentWidth :: Text -> Int
indentWidth = Text.length . Text.takeWhile (== ' ') . Text.dropWhile (`elem` newlines)
strip :: PatternElement -> PatternElement
strip (BlockText t) =
let (ns, rest) = Text.span (`elem` newlines) t
in BlockText (ns <> Text.drop commonIndent rest)
strip e = e
dropLeadingBreaks :: PatternElement -> PatternElement
dropLeadingBreaks (BlockText t) = BlockText $ Text.dropWhile (`elem` newlines) t
dropLeadingBreaks e = e
mapText :: (Text -> Text) -> PatternElement -> PatternElement
mapText f (InlineText t) = InlineText $ f t
mapText f (BlockText t) = BlockText $ f t
mapText _ e = e
onFirst, onLast :: (a -> a) -> [a] -> [a]
onFirst f (x : xs) = f x : xs
onFirst _ [] = []
onLast f = reverse . onFirst f . reverse