packages feed

fluent-syntax-1.1.0: src/Language/Fluent/Parser/Location.hs

module Language.Fluent.Parser.Location where

import Data.Attoparsec.Internal.Types (Pos (Pos))
import Data.List qualified as List
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Text.Unsafe (dropWord8, lengthWord8, takeWord8)
import Language.Fluent.AST
import Prelude

-- | Replaces the byte offsets of the errors of junk with character offsets, lines and columns.
locate :: Pos -> Text -> [Entry] -> [Entry]
locate (Pos start) whole = snd . List.mapAccumL step (Cursor 0 0 1 1 whole)
  where
    step :: Cursor -> Entry -> (Cursor, Entry)
    step cursor (JunkEntry text ParseError{offset, reason}) =
        let moved@Cursor{characters, lineNumber, columnNumber} = advanceTo (offset - start) cursor
         in ( moved
            , JunkEntry
                text
                ParseError
                    { offset = characters
                    , line = lineNumber
                    , column = columnNumber
                    , reason
                    }
            )
    step cursor other = (cursor, other)

-- | A position in the text in bytes, characters, lines and columns, and the text after it.
data Cursor = Cursor
    { bytes :: !Int
    , characters :: !Int
    , lineNumber :: !Int
    , columnNumber :: !Int
    , remaining :: Text
    }

-- | Moves the cursor forward to the given byte offset.
advanceTo :: Int -> Cursor -> Cursor
advanceTo target Cursor{..} =
    Cursor
        { bytes = target
        , characters = characters + Text.length passed
        , lineNumber = lineNumber + breaks
        , columnNumber = newColumn
        , remaining = dropWord8 skipped remaining
        }
  where
    skipped = target - bytes
    passed = takeWord8 skipped remaining
    breaks = Text.count "\n" passed
    newColumn
        | breaks == 0 = columnNumber + Text.length passed
        | otherwise = 1 + Text.length (Text.takeWhileEnd (/= '\n') passed)

-- | Moves a failure that follows a line break back to the end of the last non-blank character,
-- where what is missing belongs. Both offsets are in bytes of the text.
settle :: Text -> Int -> Int
settle text failure
    | Text.any (== '\n') $ Text.takeWhileEnd isBlank before = lengthWord8 content
    | otherwise = failure
  where
    before = takeWord8 failure text
    content = Text.dropWhileEnd isBlank before
    isBlank = (`elem` (" \r\n" :: String))