packages feed

megaparsec-9.8.2: Text/Megaparsec/Unicode.hs

{-# LANGUAGE Safe #-}

-- |
-- Module      :  Text.Megaparsec.Unicode
-- Copyright   :  © 2024–present Megaparsec contributors
-- License     :  FreeBSD
--
-- Maintainer  :  Mark Karpov <markkarpov92@gmail.com>
-- Stability   :  experimental
-- Portability :  portable
--
-- Utility functions for working with Unicode.
--
-- @since 9.7.0
module Text.Megaparsec.Unicode
  ( stringLength,
    charLength,
    isWideChar,
    isZeroWidthChar,
  )
where

import Data.Array (Array, bounds, (!))
import Data.Char (ord)
import Text.Megaparsec.Unicode.Tables

-- | Calculate the length of a string, taking into account the fact that
-- certain 'Char's may span more than 1 column.
--
-- @since 9.7.0
stringLength :: (Traversable t) => t Char -> Int
stringLength = sum . fmap charLength

-- | Return the length of an individual 'Char'.
--
-- @since 9.7.0
charLength :: Char -> Int
charLength ch
  | n < simpleCharLimit = if isSimpleZeroWidth n then 0 else 1
  -- The two tables are disjoint, so the order of the lookups only affects
  -- speed. Wide characters are looked up first because scripts that use
  -- them use them for nearly every character, while combining marks are
  -- interspersed with characters that are in neither table.
  | inRanges wideCharRanges n = 2
  | inRanges zeroWidthCharRanges n = 0
  | otherwise = 1
  where
    n = ord ch

-- | Determine whether the given 'Char' is “wide”, that is, whether it spans
-- 2 columns instead of one.
--
-- @since 9.7.0
isWideChar :: Char -> Bool
isWideChar ch = n >= simpleCharLimit && inRanges wideCharRanges n
  where
    n = ord ch

-- | Determine whether the given 'Char' is “zero-width”, that is, whether it
-- has no visible representation and does not advance the cursor position.
-- This includes control characters and certain Unicode zero-width
-- characters.
--
-- @since 9.8.0
isZeroWidthChar :: Char -> Bool
isZeroWidthChar ch
  | n < simpleCharLimit = isSimpleZeroWidth n
  | otherwise = inRanges zeroWidthCharRanges n
  where
    n = ord ch

-- | Decide whether a code point below 'simpleCharLimit' is zero-width. Only
-- the control characters and the soft hyphen are; the generator checks that
-- this agrees with the data it produces.
isSimpleZeroWidth :: Int -> Bool
isSimpleZeroWidth n =
  n < 0x20 -- C0 control chars
    || (n >= 0x7f && n <= 0x9f) -- DEL and C1 control chars
    || n == 0xad -- soft hyphen
{-# INLINE isSimpleZeroWidth #-}

-- | Look up a code point in a sorted collection of ranges that neither
-- overlap nor touch.
inRanges :: Array Int (Int, Int) -> Int -> Bool
inRanges ranges n = go (bounds ranges)
  where
    go (lo, hi)
      | hi < lo = False
      | a <= n && n <= b = True
      | n < a = go (lo, pred mid)
      | otherwise = go (succ mid, hi)
      where
        mid = (lo + hi) `div` 2
        (a, b) = ranges ! mid