packages feed

text-ascii (empty) → 1.0.0

raw patch · 9 files changed

+1960/−0 lines, 9 filesdep +basedep +bytestringdep +deepseqsetup-changed

Dependencies added: base, bytestring, deepseq, hashable, optics-core, parsec, template-haskell, text

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for text-ascii++## 1.0.0 -- 2021-02-07++* First version. Released on an unsuspecting world.
+ LICENSE.md view
@@ -0,0 +1,201 @@+Apache License+                           Version 2.0, January 2004+                        http://www.apache.org/licenses/++   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION++   1. Definitions.++      "License" shall mean the terms and conditions for use, reproduction,+      and distribution as defined by Sections 1 through 9 of this document.++      "Licensor" shall mean the copyright owner or entity authorized by+      the copyright owner that is granting the License.++      "Legal Entity" shall mean the union of the acting entity and all+      other entities that control, are controlled by, or are under common+      control with that entity. For the purposes of this definition,+      "control" means (i) the power, direct or indirect, to cause the+      direction or management of such entity, whether by contract or+      otherwise, or (ii) ownership of fifty percent (50%) or more of the+      outstanding shares, or (iii) beneficial ownership of such entity.++      "You" (or "Your") shall mean an individual or Legal Entity+      exercising permissions granted by this License.++      "Source" form shall mean the preferred form for making modifications,+      including but not limited to software source code, documentation+      source, and configuration files.++      "Object" form shall mean any form resulting from mechanical+      transformation or translation of a Source form, including but+      not limited to compiled object code, generated documentation,+      and conversions to other media types.++      "Work" shall mean the work of authorship, whether in Source or+      Object form, made available under the License, as indicated by a+      copyright notice that is included in or attached to the work+      (an example is provided in the Appendix below).++      "Derivative Works" shall mean any work, whether in Source or Object+      form, that is based on (or derived from) the Work and for which the+      editorial revisions, annotations, elaborations, or other modifications+      represent, as a whole, an original work of authorship. For the purposes+      of this License, Derivative Works shall not include works that remain+      separable from, or merely link (or bind by name) to the interfaces of,+      the Work and Derivative Works thereof.++      "Contribution" shall mean any work of authorship, including+      the original version of the Work and any modifications or additions+      to that Work or Derivative Works thereof, that is intentionally+      submitted to Licensor for inclusion in the Work by the copyright owner+      or by an individual or Legal Entity authorized to submit on behalf of+      the copyright owner. For the purposes of this definition, "submitted"+      means any form of electronic, verbal, or written communication sent+      to the Licensor or its representatives, including but not limited to+      communication on electronic mailing lists, source code control systems,+      and issue tracking systems that are managed by, or on behalf of, the+      Licensor for the purpose of discussing and improving the Work, but+      excluding communication that is conspicuously marked or otherwise+      designated in writing by the copyright owner as "Not a Contribution."++      "Contributor" shall mean Licensor and any individual or Legal Entity+      on behalf of whom a Contribution has been received by Licensor and+      subsequently incorporated within the Work.++   2. Grant of Copyright License. Subject to the terms and conditions of+      this License, each Contributor hereby grants to You a perpetual,+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable+      copyright license to reproduce, prepare Derivative Works of,+      publicly display, publicly perform, sublicense, and distribute the+      Work and such Derivative Works in Source or Object form.++   3. Grant of Patent License. Subject to the terms and conditions of+      this License, each Contributor hereby grants to You a perpetual,+      worldwide, non-exclusive, no-charge, royalty-free, irreocable+      (except as stated in this section) patent license to make, have made,+      use, offer to sell, sell, import, and otherwise transfer the Work,+      where such license applies only to those patent claims licensable+      by such Contributor that are necessarily infringed by their+      Contribution(s) alone or by combination of their Contribution(s)+      with the Work to which such Contribution(s) was submitted. If You+      institute patent litigation against any entity (including a+      cross-claim or counterclaim in a lawsuit) alleging that the Work+      or a Contribution incorporated within the Work constitutes direct+      or contributory patent infringement, then any patent licenses+      granted to You under this License for that Work shall terminate+      as of the date such litigation is filed.++   4. Redistribution. You may reproduce and distribute copies of the+      Work or Derivative Works thereof in any medium, with or without+      modifications, and in Source or Object form, provided that You+      meet the following conditions:++      (a) You must give any other recipients of the Work or+          Derivative Works a copy of this License; and++      (b) You must cause any modified files to carry prominent notices+          stating that You changed the files; and++      (c) You must retain, in the Source form of any Derivative Works+          that You distribute, all copyright, patent, trademark, and+          attribution notices from the Source form of the Work,+          excluding those notices that do not pertain to any part of+          the Derivative Works; and++      (d) If the Work includes a "NOTICE" text file as part of its+          distribution, then any Derivative Works that You distribute must+          include a readable copy of the attribution notices contained+          within such NOTICE file, excluding those notices that do not+          pertain to any part of the Derivative Works, in at least one+          of the following places: within a NOTICE text file distributed+          as part of the Derivative Works; within the Source form or+          documentation, if provided along with the Derivative Works; or,+          within a display generated by the Derivative Works, if and+          wherever such third-party notices normally appear. The contents+          of the NOTICE file are for informational purposes only and+          do not modify the License. You may add Your own attribution+          notices within Derivative Works that You distribute, alongside+          or as an addendum to the NOTICE text from the Work, provided+          that such additional attribution notices cannot be construed+          as modifying the License.++      You may add Your own copyright statement to Your modifications and+      may provide additional or different license terms and conditions+      for use, reproduction, or distribution of Your modifications, or+      for any such Derivative Works as a whole, provided Your use,+      reproduction, and distribution of the Work otherwise complies with+      the conditions stated in this License.++   5. Submission of Contributions. Unless You explicitly state otherwise,+      any Contribution intentionally submitted for inclusion in the Work+      by You to the Licensor shall be under the terms and conditions of+      this License, without any additional terms or conditions.+      Notwithstanding the above, nothing herein shall supersede or modify+      the terms of any separate license agreement you may have executed+      with Licensor regarding such Contributions.++   6. Trademarks. This License does not grant permission to use the trade+      names, trademarks, service marks, or product names of the Licensor,+      except as required for reasonable and customary use in describing the+      origin of the Work and reproducing the content of the NOTICE file.++   7. Disclaimer of Warranty. Unless required by applicable law or+      agreed to in writing, Licensor provides the Work (and each+      Contributor provides its Contributions) on an "AS IS" BASIS,+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or+      implied, including, without limitation, any warranties or conditions+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A+      PARTICULAR PURPOSE. You are solely responsible for determining the+      appropriateness of using or redistributing the Work and assume any+      risks associated with Your exercise of permissions under this License.++   8. Limitation of Liability. In no event and under no legal theory,+      whether in tort (including negligence), contract, or otherwise,+      unless required by applicable law (such as deliberate and grossly+      negligent acts) or agreed to in writing, shall any Contributor be+      liable to You for damages, including any direct, indirect, special,+      incidental, or consequential damages of any character arising as a+      result of this License or out of the use or inability to use the+      Work (including but not limited to damages for loss of goodwill,+      work stoppage, computer failure or malfunction, or any and all+      other commercial damages or losses), even if such Contributor+      has been advised of the possibility of such damages.++   9. Accepting Warranty or Additional Liability. While redistributing+      the Work or Derivative Works thereof, You may choose to offer,+      and charge a fee for, acceptance of support, warranty, indemnity,+      or other liability obligations and/or rights consistent with this+      License. However, in accepting such obligations, You may act only+      on Your own behalf and on Your sole responsibility, not on behalf+      of any other Contributor, and only if You agree to indemnify,+      defend, and hold each Contributor harmless for any liability+      incurred by, or claims asserted against, such Contributor by reason+      of your accepting any such warranty or additional liability.++   END OF TERMS AND CONDITIONS++   APPENDIX: How to apply the Apache License to your work.++      To apply the Apache License to your work, attach the following+      boilerplate notice, with the fields enclosed by brackets "[]"+      replaced with your own identifying information. (Don't include+      the brackets!)  The text should be enclosed in the appropriate+      comment syntax for the file format. We also recommend that a+      file or class name and description of purpose be included on the+      same "printed page" as the copyright notice for easier+      identification within third-party archives.++   Copyright [yyyy] [name of copyright owner]++   Licensed under the Apache License, Version 2.0 (the "License");+   you may not use this file except in compliance with the License.+   You may obtain a copy of the License at++       http://www.apache.org/licenses/LICENSE-2.0++   Unless required by applicable law or agreed to in writing, software+   distributed under the License is distributed on an "AS IS" BASIS,+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+   See the License for the specific language governing permissions and+   limitations under the License.
+ README.md view
@@ -0,0 +1,63 @@+# `text-ascii`++## What is this thing?++A library for handling ASCII text.++## What are the goals of this project?++### Totality by default++Partial functions (and type classes which provide them) will not be included:+everything is total. When we include anything unsafe, it will be explicitly+firewalled into its own module, behind a newtype.++### Compatibility with the [`text`](http://hackage.haskell.org/package/text) API++We have (without conflicting with totality) a goal to match the API of the+`text` package exactly. If you know how to use `text`, you know how to use this+package too.++### Discoverability, documentation and user-friendliness++In addition to documenting everything with Haddocks, we have over 100 doctests,+which provide _executable_ examples of how the API can be used, and how it will+behave. We aim to clarify _every_ corner case left by the documentation of+`text`, and care strongly about making the API easy to follow, learn and+understand.++### Correctness++We currently use doctests, but plan to add support for more testing. No such+thing as too much!++### Low dependencies++As far as possible, we aim to depend on [GHC boot packages](https://gitlab.haskell.org/ghc/ghc/-/wikis/commentary/libraries/version-history) only. When we+introduce more dependencies, we do it only when we have to. This way, we ensure+this package builds quickly and doesn't 'lag' more than necessary due to GHC+version changes.++## What's with all the cat stuff?++[I am a Haskell catboy.](https://twitter.com/KozRoss)++## What does this run on?++Currently, our CI checks the following versions of GHC:++* 8.6.5+* 8.8.4+* 8.10.3++We check on the following platforms:++* Windows+* Linux+* MacOS++## What can I do with this?++The project is licensed Apache 2.0 (SPDX code+[`Apache-2.0`](https://spdx.org/licenses/Apache-2.0.html)). For more details,+please see the `LICENSE.md` file.
+ Setup.hs view
@@ -0,0 +1,3 @@+import Distribution.Simple++main = defaultMain
+ src/Text/Ascii.hs view
@@ -0,0 +1,999 @@+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE Trustworthy #-}+{-# LANGUAGE NoImplicitPrelude #-}++-- |+-- Module: Text.Ascii+-- Copyright: (C) 2021 Koz Ross+-- License: Apache 2.0+-- Maintainer: Koz Ross <koz.ross@retro-freedom.nz>+-- Stability: stable+-- Portability: GHC only+--+-- An implementation of ASCII strings.+--+-- This module is designed for qualified importing:+--+-- > import qualified Text.Ascii as Ascii+--+-- /See also:/ [Wikipedia entry for ASCII](https://en.wikipedia.org/wiki/ASCII)+module Text.Ascii+  ( -- * Type+    AsciiText,++    -- * Creation+    empty,+    singleton,+    ascii,++    -- * Basic interface+    cons,+    snoc,+    uncons,+    unsnoc,+    length,++    -- * Transformations+    map,+    intercalate,+    intersperse,+    transpose,+    reverse,++    -- * Folds+    foldl,+    foldl',+    foldr,+    foldr',++    -- ** Special folds+    concat,+    concatMap,++    -- * Construction++    -- ** Scans+    scanl,+    scanr,++    -- ** Accumulating maps+    mapAccumL,+    mapAccumR,++    -- ** Generation and unfolding+    unfoldr,+    unfoldrN,++    -- * Substrings++    -- ** Breaking strings+    take,+    drop,+    takeWhile,+    takeWhileEnd,+    dropWhile,+    dropWhileEnd,+    splitAt,+    break,+    span,+    group,+    groupBy,+    inits,+    tails,++    -- ** Breaking into many substrings+    split,++    -- ** Breaking into lines and words++    -- * View patterns+    stripPrefix,+    stripSuffix,++    -- * Searching+    filter,+    find,+    partition,++    -- * Indexing+    findIndex,++    -- * Zipping+    zip,++    -- * Conversions+    fromText,+    fromByteString,+    toText,+    toByteString,++    -- * Optics+    textWise,+    byteStringWise,+  )+where++import Control.Category ((.))+import Data.Bifunctor (first)+import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import Data.Char (isAscii)+import Data.Coerce (coerce)+import Data.Maybe (Maybe (Just, Nothing))+import Data.Text (Text)+import qualified Data.Text as T+import Data.Text.Encoding (decodeUtf8, encodeUtf8)+import Optics.Prism (Prism', prism')+import Text.Ascii.Internal (AsciiChar (AsciiChar), AsciiText (AsciiText))+import Text.Ascii.QQ (ascii)+import Prelude+  ( Bool,+    Int,+    not,+    pure,+    ($),+    (<$>),+    (>),+  )++-- Note on pragmata+--+-- This is cribbed directly from bytestring, as I figure they know what they're+-- doing way better than we do. When we add our own functionality, this probably+-- needs to be considered more carefully. - Koz++-- Creation++-- $setup+-- >>> :set -XNoImplicitPrelude+-- >>> :set -XQuasiQuotes+-- >>> :set -XOverloadedStrings+-- >>> import Text.Ascii+-- >>> import Text.Ascii.Char (char, upcase, AsciiCase (Lower), caseOf)+-- >>> import Prelude ((.), ($), replicate, (<>), (==), (<), (/=))+-- >>> import Data.Maybe (Maybe (Just), fromMaybe)+-- >>> import qualified Data.ByteString as BS++-- | The empty text.+--+-- >>> empty+-- ""+--+-- /Complexity:/ \(\Theta(1)\)+--+-- @since 1.0.0+empty :: AsciiText+empty = coerce BS.empty++-- | A text consisting of a single ASCII character.+--+-- >>> singleton [char| 'w' |]+-- "w"+--+-- /Complexity:/ \(\Theta(1)\)+--+-- @since 1.0.0+{-# INLINE [1] singleton #-}+singleton :: AsciiChar -> AsciiText+singleton = coerce BS.singleton++-- Basic interface++-- | Adds a character to the front of a text. This requires copying, which gives+-- its complexity.+--+-- >>> cons [char| 'n' |] [ascii| "eko" |]+-- "neko"+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE cons #-}+cons :: AsciiChar -> AsciiText -> AsciiText+cons = coerce BS.cons++-- | Adds a character to the back of a text. This requires copying, which gives+-- its complexity.+--+-- >>> snoc [ascii| "nek" |] [char| 'o' |]+-- "neko"+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE snoc #-}+snoc :: AsciiText -> AsciiChar -> AsciiText+snoc = coerce BS.snoc++-- | If the argument is non-empty, gives 'Just' the first character and the+-- rest, and 'Nothing' otherwise.+--+-- >>> uncons empty+-- Nothing+-- >>> uncons . singleton $ [char| 'w' |]+-- Just ('0x77',"")+-- >>> uncons [ascii| "nekomimi" |]+-- Just ('0x6e',"ekomimi")+--+-- /Complexity:/ \(\Theta(1)\)+--+-- @since 1.0.0+{-# INLINE uncons #-}+uncons :: AsciiText -> Maybe (AsciiChar, AsciiText)+uncons = coerce BS.uncons++-- | If the argument is non-empty, gives 'Just' the initial segment and the last+-- character, and 'Nothing' otherwise.+--+-- >>> unsnoc empty+-- Nothing+-- >>> unsnoc . singleton $ [char| 'w' |]+-- Just ("",'0x77')+-- >>> unsnoc [ascii| "catboy" |]+-- Just ("catbo",'0x79')+--+-- /Complexity:/ \(\Theta(1)\)+--+-- @since 1.0.0+{-# INLINE unsnoc #-}+unsnoc :: AsciiText -> Maybe (AsciiText, AsciiChar)+unsnoc = coerce BS.unsnoc++-- | The number of characters (and, since this is ASCII, bytes) in the text.+--+-- >>> length . singleton $ [char| 'w' |]+-- 1+-- >>> length [ascii| "nyan nyan" |]+-- 9+--+-- /Complexity:/ \(\Theta(1)\)+--+-- @since 1.0.0+{-# INLINE length #-}+length :: AsciiText -> Int+length = coerce BS.length++-- Transformations++-- | Copy, and apply the function to each element of, the text.+--+-- >>> map (\c -> fromMaybe c . upcase $ c) [ascii| "nyan!" |]+-- "NYAN!"+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE map #-}+map :: (AsciiChar -> AsciiChar) -> AsciiText -> AsciiText+map = coerce BS.map++-- | Takes a text and a list of texts, and concatenates the list after+-- interspersing the first argument between each element of the list.+--+-- >>> intercalate [ascii| " ~ " |] []+-- ""+-- >>> intercalate [ascii| " ~ " |] [[ascii| "nyan" |]]+-- "nyan"+-- >>> intercalate [ascii| " ~ " |] . replicate 3 $ [ascii| "nyan" |]+-- "nyan ~ nyan ~ nyan"+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE [1] intercalate #-}+intercalate :: AsciiText -> [AsciiText] -> AsciiText+intercalate = coerce BS.intercalate++-- | Takes a character, and places it between the characters of a text.+--+-- >>> intersperse [char| '~' |] empty+-- ""+-- >>> intersperse [char| '~' |] . singleton $ [char| 'w' |]+-- "w"+-- >>> intersperse [char| '~' |] [ascii| "nyan" |]+-- "n~y~a~n"+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+intersperse :: AsciiChar -> AsciiText -> AsciiText+intersperse = coerce BS.intersperse++-- | Transpose the rows and columns of the argument. This uses+-- 'Data.List.transpose' internally, and thus, isn't very efficient.+--+-- >>> transpose []+-- []+-- >>> transpose [[ascii| "w" |]]+-- ["w"]+-- >>> transpose [[ascii| "nyan" |]]+-- ["n","y","a","n"]+-- >>> transpose . replicate 3 $ [ascii| "nyan" |]+-- ["nnn","yyy","aaa","nnn"]+-- >>> transpose [[ascii| "cat" |], [ascii| "boy" |], [ascii| "nyan" |]]+-- ["cbn","aoy","tya","n"]+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+transpose :: [AsciiText] -> [AsciiText]+transpose = coerce BS.transpose++-- | Reverse the text.+--+-- >>> reverse empty+-- ""+-- >>> reverse . singleton $ [char| 'w' |]+-- "w"+-- >>> reverse [ascii| "catboy goes nyan" |]+-- "nayn seog yobtac"+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+reverse :: AsciiText -> AsciiText+reverse = coerce BS.reverse++-- TODO: Replace, justifyLeft, justifyRight, center++-- Folds++-- | Left-associative fold of a text.+--+-- >>> foldl (\acc c -> [ascii| "f(" |] <> acc <> singleton c <> [ascii| ")" |]) [ascii| "a" |] [ascii| "catboy" |]+-- "f(f(f(f(f(f(ac)a)t)b)o)y)"+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE foldl #-}+foldl :: (a -> AsciiChar -> a) -> a -> AsciiText -> a+foldl f x (AsciiText bs) = BS.foldl (coerce f) x bs++-- | Left-associative fold of a text, strict in the accumulator.+--+-- >>> foldl' (\acc c -> [ascii| "f(" |] <> acc <> singleton c <> [ascii| ")" |]) [ascii| "a" |] [ascii| "catboy" |]+-- "f(f(f(f(f(f(ac)a)t)b)o)y)"+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE foldl' #-}+foldl' :: (a -> AsciiChar -> a) -> a -> AsciiText -> a+foldl' f x (AsciiText bs) = BS.foldl' (coerce f) x bs++-- | Right-associative fold of a text.+--+-- >>> foldr (\c acc -> [ascii| "f(" |] <> acc <> singleton c <> [ascii| ")" |]) [ascii| "a" |] [ascii| "catboy" |]+-- "f(f(f(f(f(f(ay)o)b)t)a)c)"+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE foldr #-}+foldr :: (AsciiChar -> a -> a) -> a -> AsciiText -> a+foldr f x (AsciiText bs) = BS.foldr (coerce f) x bs++-- | Right-associative fold of a text, strict in the accumulator.+--+-- >>> foldr' (\c acc -> [ascii| "f(" |] <> acc <> singleton c <> [ascii| ")" |]) [ascii| "a" |] [ascii| "catboy" |]+-- "f(f(f(f(f(f(ay)o)b)t)a)c)"+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE foldr' #-}+foldr' :: (AsciiChar -> a -> a) -> a -> AsciiText -> a+foldr' f x (AsciiText bs) = BS.foldr' (coerce f) x bs++-- Special folds++-- | Concatenate a list of texts.+--+-- >>> concat []+-- ""+-- >>> concat [[ascii| "catboy" |]]+-- "catboy"+-- >>> concat . replicate 4 $ [ascii| "nyan" |]+-- "nyannyannyannyan"+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+concat :: [AsciiText] -> AsciiText+concat = coerce BS.concat++-- | Map a text-producing function over a text, then concatenate the results.+--+-- >>> concatMap singleton empty+-- ""+-- >>> concatMap singleton [ascii| "nyan" |]+-- "nyan"+-- >>> concatMap (\c -> singleton c <> singleton c) [ascii| "nekomimi" |]+-- "nneekkoommiimmii"+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+concatMap :: (AsciiChar -> AsciiText) -> AsciiText -> AsciiText+concatMap = coerce BS.concatMap++-- | 'scanl' is similar to 'foldl', but returns a list of successive values from+-- the left.+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE scanl #-}+scanl ::+  -- | accumulator -> element -> new accumulator+  (AsciiChar -> AsciiChar -> AsciiChar) ->+  -- | Starting accumulator value+  AsciiChar ->+  -- | Input of length \(n\)+  AsciiText ->+  -- | Output of length \(n + 1\)+  AsciiText+scanl = coerce BS.scanl++-- | 'scanr' is similar to 'foldr', but returns a list of successive values from+-- the right.+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE scanr #-}+scanr ::+  -- | element -> accumulator -> new accumulator+  (AsciiChar -> AsciiChar -> AsciiChar) ->+  -- | Starting accumulator value+  AsciiChar ->+  -- | Input of length \(n\)+  AsciiText ->+  -- | Output of length \(n + 1\)+  AsciiText+scanr = coerce BS.scanr++-- Accumulating maps++-- | Like a combination of 'map' and 'foldl''. Applies a function to each+-- element of an 'AsciiText', passing an accumulating parameter from left to+-- right, and returns a final 'AsciiText' along with the accumulating+-- parameter's final value.+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE mapAccumL #-}+mapAccumL :: (a -> AsciiChar -> (a, AsciiChar)) -> a -> AsciiText -> (a, AsciiText)+mapAccumL f x (AsciiText bs) = AsciiText <$> BS.mapAccumL (coerce f) x bs++-- | Like a combination of 'map' and 'foldr'. Applies a function to each element+-- of an 'AsciiText', passing an accumulating parameter from right to left, and+-- returns a final 'AsciiText' along with the accumulating parameter's final+-- value.+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE mapAccumR #-}+mapAccumR :: (a -> AsciiChar -> (a, AsciiChar)) -> a -> AsciiText -> (a, AsciiText)+mapAccumR f x (AsciiText bs) = AsciiText <$> BS.mapAccumL (coerce f) x bs++-- Generation and unfolding++-- TODO: replicate++-- | Similar to 'Data.List.unfoldr'. The function parameter takes a seed value,+-- and produces either 'Nothing' (indicating that we're done) or 'Just' an+-- 'AsciiChar' and a new seed value. 'unfoldr' then, given a starting seed, will+-- repeatedly call the function parameter on successive seed values, returning+-- the resulting 'AsciiText', based on the 'AsciiChar's produced, in the same+-- order.+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE unfoldr #-}+unfoldr :: (a -> Maybe (AsciiChar, a)) -> a -> AsciiText+unfoldr f = AsciiText . BS.unfoldr (coerce f)++-- | Similar to 'unfoldr', but also takes a maximum length parameter. The second+-- element of the result tuple will be 'Nothing' if we finished with the+-- function argument returning 'Nothing', and 'Just' the final seed value if we+-- reached the maximum length before that happened.+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE unfoldrN #-}+unfoldrN :: Int -> (a -> Maybe (AsciiChar, a)) -> a -> (AsciiText, Maybe a)+unfoldrN n f = first AsciiText . BS.unfoldrN n (coerce f)++-- | @take n t@ returns the prefix of @t@ with length+-- \(\min \{ \max \{ 0, {\tt n}\}, {\tt length} \; {\tt t} \}\)+--+-- >>> take (-100) [ascii| "catboy" |]+-- ""+-- >>> take 0 [ascii| "catboy" |]+-- ""+-- >>> take 3 [ascii| "catboy" |]+-- "cat"+-- >>> take 1000 [ascii| "catboy" |]+-- "catboy"+--+-- /Complexity:/ \(\Theta(1)\)+--+-- @since 1.0.0+{-# INLINE take #-}+take :: Int -> AsciiText -> AsciiText+take = coerce BS.take++-- TODO: takeEnd++-- | @drop n t@ returns the suffix of @t@ with length+-- \(\max \{ 0, \min \{ {\tt length} \; {\tt t}, {\tt length} \; {\tt t} - {\tt n} \} \}\)+--+-- >>> drop (-100) [ascii| "catboy" |]+-- "catboy"+-- >>> drop 0 [ascii| "catboy" |]+-- "catboy"+-- >>> drop 3 [ascii| "catboy" |]+-- "boy"+-- >>> drop 1000 [ascii| "catboy" |]+-- ""+--+-- /Complexity:/ \(\Theta(1)\)+--+-- @since 1.0.0+{-# INLINE drop #-}+drop :: Int -> AsciiText -> AsciiText+drop = coerce BS.drop++-- TODO: dropEnd++-- | @takeWhile p t@ returns the longest prefix of @t@ of characters that+-- satisfy @p@.+--+-- >>> takeWhile ((Just Lower ==) . caseOf) empty+-- ""+-- >>> takeWhile ((Just Lower ==) . caseOf) [ascii| "catboy goes nyan" |]+-- "catboy"+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE [1] takeWhile #-}+takeWhile :: (AsciiChar -> Bool) -> AsciiText -> AsciiText+takeWhile f (AsciiText at) = AsciiText . BS.takeWhile (coerce f) $ at++-- | @takeWhileEnd p t@ returns the longest suffix of @t@ of characters that+-- satisfy @p@. Equivalent to @'reverse' . 'takeWhile' p . 'reverse'@.+--+-- >>> takeWhileEnd ((Just Lower ==) . caseOf) empty+-- ""+-- >>> takeWhileEnd ((Just Lower ==) . caseOf) [ascii| "catboy goes nyan" |]+-- "nyan"+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE takeWhileEnd #-}+takeWhileEnd :: (AsciiChar -> Bool) -> AsciiText -> AsciiText+takeWhileEnd f = AsciiText . BS.takeWhileEnd (coerce f) . coerce++-- | @dropWhile p t@ returns the suffix remaining after @'takeWhile' p t@.+--+-- >>> dropWhile ((Just Lower ==) . caseOf) empty+-- ""+-- >>> dropWhile ((Just Lower ==) . caseOf) [ascii| "catboy goes nyan" |]+-- " goes nyan"+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE [1] dropWhile #-}+dropWhile :: (AsciiChar -> Bool) -> AsciiText -> AsciiText+dropWhile f (AsciiText at) = AsciiText . BS.dropWhile (coerce f) $ at++-- | @dropWhileEnd p t@ returns the prefix remaining after @'takeWhileEnd' p t@.+-- Equivalent to @'reverse' . 'dropWhile' p . 'reverse'@.+--+-- >>> dropWhileEnd ((Just Lower ==) . caseOf) empty+-- ""+-- >>> dropWhileEnd ((Just Lower ==) . caseOf) [ascii| "catboy goes nyan" |]+-- "catboy goes "+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE dropWhileEnd #-}+dropWhileEnd :: (AsciiChar -> Bool) -> AsciiText -> AsciiText+dropWhileEnd f = AsciiText . BS.dropWhileEnd (coerce f) . coerce++-- TODO: dropAround, strip, stripStart, stripEnd++-- | @splitAt n t@ is equivalent to @('take' n t, 'drop' n t)@.+--+-- >>> splitAt (-3) [ascii| "catboy" |]+-- ("","catboy")+-- >>> splitAt 0 [ascii| "catboy" |]+-- ("","catboy")+-- >>> splitAt 3 [ascii| "catboy" |]+-- ("cat","boy")+-- >>> splitAt 1000 [ascii| "catboy" |]+-- ("catboy","")+--+-- /Complexity:/ \(\Theta(1)\)+--+-- @since 1.0.0+{-# INLINE splitAt #-}+splitAt :: Int -> AsciiText -> (AsciiText, AsciiText)+splitAt = coerce BS.splitAt++-- TODO: breakOn, breakOnEnd++-- | @break p t@ is equivalent to @('takeWhile' ('not' p) t, 'dropWhile' ('not'+-- p) t)@.+--+-- >>> break ([char| ' ' |] ==) [ascii| "catboy goes nyan" |]+-- ("catboy"," goes nyan")+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+break :: (AsciiChar -> Bool) -> AsciiText -> (AsciiText, AsciiText)+break = coerce BS.break++-- | @span p t@ is equivalent to @('takeWhile' p t, 'dropWhile' p t)@.+--+-- >>> span ([char| 'c' |] ==) [ascii| "catboy goes nyan" |]+-- ("c","atboy goes nyan")+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE [1] span #-}+span :: (AsciiChar -> Bool) -> AsciiText -> (AsciiText, AsciiText)+span = coerce BS.span++-- | Separate a text into a list of texts such that:+--+-- * Their concatenation is equal to the original argument; and+-- * Equal adjacent characters in the original argument are in the same text in+-- the result.+--+-- This is a specialized form of 'groupBy', and is about 40% faster than+-- @'groupBy' '=='@.+--+-- >>> group empty+-- []+-- >>> group . singleton $ [char| 'w' |]+-- ["w"]+-- >>> group [ascii| "nyan" |]+-- ["n","y","a","n"]+-- >>> group [ascii| "nyaaaan" |]+-- ["n","y","aaaa","n"]+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+group :: AsciiText -> [AsciiText]+group = coerce BS.group++-- | Separate a text into a list of texts such that:+--+-- * Their concatenation is equal to the original argument; and+-- * Adjacent characters for which the function argument returns @True@ are in+-- the same text in the result.+--+-- 'group' is a special case for the function argument '=='; it is also about+-- 40% faster.+--+-- >>> groupBy (<) empty+-- []+-- >>> groupBy (<) . singleton $ [char| 'w' |]+-- ["w"]+-- >>> groupBy (<) [ascii| "catboy goes nyan" |]+-- ["c","atboy"," goes"," nyan"]+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+groupBy :: (AsciiChar -> AsciiChar -> Bool) -> AsciiText -> [AsciiText]+groupBy = coerce BS.groupBy++-- | All prefixes of the argument, from shortest to longest.+--+-- >>> inits empty+-- [""]+-- >>> inits . singleton $ [char| 'w' |]+-- ["","w"]+-- >>> inits [ascii| "nyan" |]+-- ["","n","ny","nya","nyan"]+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+inits :: AsciiText -> [AsciiText]+inits = coerce BS.inits++-- | All suffixes of the argument, from shortest to longest.+--+-- >>> tails empty+-- [""]+-- >>> tails . singleton $ [char| 'w' |]+-- ["w",""]+-- >>> tails [ascii| "nyan" |]+-- ["nyan","yan","an","n",""]+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+tails :: AsciiText -> [AsciiText]+tails = coerce BS.tails++-- Breaking into many substrings++-- TODO: splitOn++-- | @split p t@ separates @t@ into components delimited by separators, for+-- which @p@ returns @True@. The results do not contain the separators.+--+-- \(n\) adjacent separators result in \(n - 1\) empty components in the result.+--+-- >>> split ([char| '~' |] ==) empty+-- []+-- >>> split ([char| '~' |] ==) . singleton $ [char| '~' |]+-- ["",""]+-- >>> split ([char| '~' |] ==) [ascii| "nyan" |]+-- ["nyan"]+-- >>> split ([char| '~' |] ==) [ascii| "~nyan" |]+-- ["","nyan"]+-- >>> split ([char| '~' |] ==) [ascii| "nyan~" |]+-- ["nyan",""]+-- >>> split ([char| '~' |] ==) [ascii| "nyan~nyan"|]+-- ["nyan","nyan"]+-- >>> split ([char| '~' |] ==) [ascii| "nyan~~nyan" |]+-- ["nyan","","nyan"]+-- >>> split ([char| '~' |] ==) [ascii| "nyan~~~nyan" |]+-- ["nyan","","","nyan"]+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE split #-}+split :: (AsciiChar -> Bool) -> AsciiText -> [AsciiText]+split = coerce BS.splitWith++-- TODO: chunksOf+-- Breaking into lines and words+-- TODO: lines, words, unlines, unwords++-- View patterns++-- | Return 'Just' the suffix of the second text if it has the first text as+-- a prefix, 'Nothing' otherwise.+--+-- >>> stripPrefix [ascii| "catboy" |] empty+-- Nothing+-- >>> stripPrefix empty [ascii| "catboy" |]+-- Just "catboy"+-- >>> stripPrefix [ascii| "nyan" |] [ascii| "nyan" |]+-- Just ""+-- >>> stripPrefix [ascii| "nyan" |] [ascii| "catboy" |]+-- Nothing+-- >>> stripPrefix [ascii| "catboy" |] [ascii| "catboy goes nyan" |]+-- Just " goes nyan"+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+stripPrefix :: AsciiText -> AsciiText -> Maybe AsciiText+stripPrefix = coerce BS.stripPrefix++-- | Return 'Just' the prefix of the second text if it has the first text as+-- a suffix, 'Nothing' otherwise.+--+-- >>> stripSuffix [ascii| "catboy" |] empty+-- Nothing+-- >>> stripSuffix empty [ascii| "catboy" |]+-- Just "catboy"+-- >>> stripSuffix [ascii| "nyan" |] [ascii| "nyan" |]+-- Just ""+-- >>> stripSuffix [ascii| "nyan" |] [ascii| "catboy" |]+-- Nothing+-- >>> stripSuffix [ascii| "nyan" |] [ascii| "catboy goes nyan" |]+-- Just "catboy goes "+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+stripSuffix :: AsciiText -> AsciiText -> Maybe AsciiText+stripSuffix = coerce BS.stripSuffix++-- TODO: stripInfix, commonPrefixes++-- Searching++-- | Return the text comprised of all the characters that satisfy the function+-- argument (that is, for which it returns 'True'), in the same order as in the+-- original.+--+-- >>> filter ([char| 'n' |] ==) empty+-- ""+-- >>> filter ([char| 'n' |] ==) [ascii| "catboy" |]+-- ""+-- >>> filter ([char| 'n' |] ==) [ascii| "nyan" |]+-- "nn"+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE filter #-}+filter :: (AsciiChar -> Bool) -> AsciiText -> AsciiText+filter = coerce BS.filter++-- TODO: breakOnAll++-- | Returns 'Just' the first character in the text satisfying the predicate,+-- 'Nothing' otherwise.+--+-- >>> find ([char| 'n' |] ==) empty+-- Nothing+-- >>> find ([char| 'n' |] ==) [ascii| "catboy" |]+-- Nothing+-- >>> find ([char| 'n' |] ==) [ascii| "nyan" |]+-- Just '0x6e'+-- >>> find ([char| 'n' |] /=) [ascii| "nyan" |]+-- Just '0x79'+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE find #-}+find :: (AsciiChar -> Bool) -> AsciiText -> Maybe AsciiChar+find = coerce BS.find++-- | @partition p t@ is equivalent to @('filter' p t, 'filter' ('not' p) t)@.+--+-- >>> partition ([char| 'n' |] ==) empty+-- ("","")+-- >>> partition ([char| 'n' |] ==) . singleton $ [char| 'n' |]+-- ("n","")+-- >>> partition ([char| 'n' |] ==) . singleton $ [char| 'w' |]+-- ("","w")+-- >>> partition ([char| 'n' |] ==) [ascii| "nyan!" |]+-- ("nn","ya!")+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+partition :: (AsciiChar -> Bool) -> AsciiText -> (AsciiText, AsciiText)+partition = coerce BS.partition++-- Indexing++-- TODO: index, safe only++-- | Returns 'Just' the first index in the text such that the character at that+-- index satisfies the predicate, 'Nothing' otherwise.+--+-- >>> findIndex ([char| 'n' |] ==) empty+-- Nothing+-- >>> findIndex ([char| 'n' |] ==) . singleton $ [char| 'n' |]+-- Just 0+-- >>> findIndex ([char| 'n' |] ==) . singleton $ [char| 'w' |]+-- Nothing+-- >>> findIndex ([char| 'n' |] ==) [ascii| "nyan" |]+-- Just 0+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+{-# INLINE [1] findIndex #-}+findIndex :: (AsciiChar -> Bool) -> AsciiText -> Maybe Int+findIndex = coerce BS.findIndex++-- TODO: count++-- Zipping++-- | \'Pair off\' characters in both texts at corresponding indices. The result+-- will be limited to the shorter of the two arguments.+--+-- >>> zip empty [ascii| "catboy" |]+-- []+-- >>> zip [ascii| "catboy" |] empty+-- []+-- >>> zip [ascii| "catboy" |] [ascii| "nyan" |]+-- [('0x63','0x6e'),('0x61','0x79'),('0x74','0x61'),('0x62','0x6e')]+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+zip :: AsciiText -> AsciiText -> [(AsciiChar, AsciiChar)]+zip = coerce BS.zip++-- TODO: zipWith++-- Conversions++-- | Try and convert a 'Text' into an 'AsciiText'. Gives 'Nothing' if the 'Text'+-- contains any symbols which lack an ASCII equivalent.+--+-- >>> fromText "catboy"+-- Just "catboy"+-- >>> fromText "😺😺😺😺😺"+-- Nothing+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+fromText :: Text -> Maybe AsciiText+fromText t = case T.find (not . isAscii) t of+  Nothing -> pure . AsciiText . encodeUtf8 $ t+  Just _ -> Nothing++-- | Try and convert a 'ByteString' into an 'AsciiText'. Gives 'Nothing' if the+-- 'ByteString' contains any bytes outside the ASCII range (that is, from 0 to+-- 127 inclusive).+--+-- >>> fromByteString "catboy"+-- Just "catboy"+-- >>> fromByteString . BS.pack $ [128]+-- Nothing+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+fromByteString :: ByteString -> Maybe AsciiText+fromByteString bs = case BS.find (> 127) bs of+  Nothing -> pure . AsciiText $ bs+  Just _ -> Nothing++-- | Convert an 'AsciiText' into a 'Text' (by copying).+--+-- >>> toText empty+-- ""+-- >>> toText . singleton $ [char| 'w' |]+-- "w"+-- >>> toText [ascii| "nyan" |]+-- "nyan"+--+-- /Complexity:/ \(\Theta(n)\)+--+-- @since 1.0.0+toText :: AsciiText -> Text+toText (AsciiText bs) = decodeUtf8 bs++-- | Reinterpret an 'AsciiText' as a 'ByteString' (without copying).+--+-- >>> toByteString empty+-- ""+-- >>> toByteString . singleton $ [char| 'w' |]+-- "w"+-- >>> toByteString [ascii| "nyan" |]+-- "nyan"+--+-- /Complexity:/ \(\Theta(1)\)+--+-- @since 1.0.0+toByteString :: AsciiText -> ByteString+toByteString = coerce++-- Prisms++-- | A convenient demonstration of the relationship between 'toText' and+-- 'fromText'.+--+-- @since 1.0.0+textWise :: Prism' Text AsciiText+textWise = prism' toText fromText++-- | A convenient demonstration of the relationship between 'toByteString' and+-- 'fromByteString'.+--+-- @since 1.0.0+byteStringWise :: Prism' ByteString AsciiText+byteStringWise = prism' toByteString fromByteString
+ src/Text/Ascii/Char.hs view
@@ -0,0 +1,368 @@+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE Trustworthy #-}++-- |+-- Module: Text.Ascii.Char+-- Copyright: (C) 2021 Koz Ross+-- License: Apache 2.0+-- Maintainer: Koz Ross <koz.ross@retro-freedom.nz>+-- Stability: stable+-- Portability: GHC only+--+-- An implementation of ASCII characters, as bytes restricted to the range 0 -+-- 127 inclusive.+--+-- /See also:/ [Wikipedia entry for ASCII](https://en.wikipedia.org/wiki/ASCII)+module Text.Ascii.Char+  ( -- * ASCII characters++    -- ** Type+    AsciiChar (AsByte, AsChar),++    -- ** Construction+    char,+    fromChar,+    fromByte,++    -- ** Transformation+    upcase,+    downcase,++    -- * Categorization+    AsciiType (Control, Printable),+    charType,+    AsciiCategory (Other, Punctuation, Letter, Number, Symbol),+    categorize,+    categorizeGeneral,+    AsciiCase (Upper, Lower),+    caseOf,++    -- * Optics+    charWise,+    byteWise,+  )+where++import Control.DeepSeq (NFData)+import Control.Monad (guard)+import Data.Char (GeneralCategory, chr, generalCategory, isAscii, ord)+import Data.Functor (($>))+import Data.Hashable (Hashable)+import Data.Word (Word8)+import Optics.Prism (Prism', prism')+import Text.Ascii.Internal (AsciiChar (AsciiChar), toByte, pattern AsByte, pattern AsChar)+import Text.Ascii.QQ (char)++-- $setup+-- >>> :set -XQuasiQuotes+-- >>> import Text.Ascii.Char++-- | Try and turn a 'Char' into the equivalent 'AsciiChar'. Will return+-- 'Nothing' if given a 'Char' that has no ASCII equivalent.+--+-- >>> fromChar '0'+-- Just '0x30'+-- >>> fromChar '😺'+-- Nothing+--+-- @since 1.0.0+fromChar :: Char -> Maybe AsciiChar+fromChar c =+  if isAscii c+    then pure . AsciiChar . fromIntegral . ord $ c+    else Nothing++-- | Try to give the 'AsciiChar' corresponding to the given byte. Will return+-- 'Nothing' if given a byte that doesn't correspond to an ASCII character.+--+-- >>> fromByte 50+-- Just '0x32'+-- >>> fromByte 128+-- Nothing+--+-- @since 1.0.0+fromByte :: Word8 -> Maybe AsciiChar+fromByte w8 =+  if isAscii . chr . fromIntegral $ w8+    then pure . AsciiChar $ w8+    else Nothing++-- | Give the 'AsciiChar' corresponding to the uppercase version of the+-- argument. Will give 'Nothing' if given an 'AsciiChar' which has no uppercase+-- version, or is uppercase already.+--+-- >>> upcase [char| 'a' |]+-- Just '0x41'+-- >>> upcase [char| '0' |]+-- Nothing+--+-- @since 1.0.0+upcase :: AsciiChar -> Maybe AsciiChar+upcase c@(AsciiChar w8) =+  caseOf c >>= (\cs -> guard (cs == Lower) $> AsciiChar (w8 - 32))++-- | Give the 'AsciiChar' corresponding to the lowercase version of the+-- argument. Will give 'Nothing' if given an 'AsciiChar' which has no lowercase+-- version, or is lowercase already.+--+-- >>> downcase [char| 'C' |]+-- Just '0x63'+-- >>> downcase [char| '\\' |]+-- Nothing+--+-- @since 1.0.0+downcase :: AsciiChar -> Maybe AsciiChar+downcase c@(AsciiChar w8) =+  caseOf c >>= (\cs -> guard (cs == Upper) $> AsciiChar (w8 + 32))++-- Categorization++-- | A categorization of ASCII characters based on whether they're meant to be+-- displayed ('Printable') or for control ('Control').+--+-- @since 1.0.0+newtype AsciiType = AsciiType Word8+  deriving (Eq, Ord, Hashable, NFData) via Word8++-- | @since 1.0.0+instance Show AsciiType where+  {-# INLINEABLE show #-}+  show = \case+    Control -> "Control"+    Printable -> "Printable"++-- | @since 1.0.0+instance Bounded AsciiType where+  minBound = Control+  maxBound = Printable++-- | A control character is any of the first 32 bytes (0-31), plus @DEL@ (127).+--+-- @since 1.0.0+pattern Control :: AsciiType+pattern Control <-+  AsciiType 0+  where+    Control = AsciiType 0++-- | All ASCII characters whose byte is above 31 (and not 127) are printable+-- characters.+--+-- @since 1.0.0+pattern Printable :: AsciiType+pattern Printable <-+  AsciiType 1+  where+    Printable = AsciiType 1++{-# COMPLETE Control, Printable #-}++-- | Classify an 'AsciiChar' according to whether it's a control character or a+-- printable character.+--+-- >>> charType [char| '\0' |]+-- Control+-- >>> charType [char| 'w' |]+-- Printable+--+-- @since 1.0.0+charType :: AsciiChar -> AsciiType+charType (AsciiChar w8)+  | w8 == 127 = Control+  | w8 < 32 = Control+  | otherwise = Printable++-- | A categorization of ASCII characters based on their usage. Based (loosely)+-- on Unicode categories.+--+-- @since 1.0.0+newtype AsciiCategory = AsciiCategory Word8+  deriving (Eq, Ord, Hashable, NFData) via Word8++-- | @since 1.0.0+instance Show AsciiCategory where+  {-# INLINEABLE show #-}+  show = \case+    Other -> "Other"+    Symbol -> "Symbol"+    Number -> "Number"+    Letter -> "Letter"+    Punctuation -> "Punctuation"++-- | @since 1.0.0+instance Bounded AsciiCategory where+  minBound = Other+  maxBound = Symbol++-- | Something which doesn't fit into any of the other categories.+--+-- @since 1.0.0+pattern Other :: AsciiCategory+pattern Other <-+  AsciiCategory 0+  where+    Other = AsciiCategory 0++-- | A punctuation character.+--+-- @since 1.0.0+pattern Punctuation :: AsciiCategory+pattern Punctuation <-+  AsciiCategory 1+  where+    Punctuation = AsciiCategory 1++-- | A letter, either uppercase or lowercase.+--+-- @since 1.0.0+pattern Letter :: AsciiCategory+pattern Letter <-+  AsciiCategory 2+  where+    Letter = AsciiCategory 2++-- | A numerical digit.+--+-- @since 1.0.0+pattern Number :: AsciiCategory+pattern Number <-+  AsciiCategory 3+  where+    Number = AsciiCategory 3++-- | A symbol whose role isn't (normally) punctuation.+--+-- @since 1.0.0+pattern Symbol :: AsciiCategory+pattern Symbol <-+  AsciiCategory 4+  where+    Symbol = AsciiCategory 4++{-# COMPLETE Other, Punctuation, Letter, Number, Symbol #-}++-- | Classify an 'AsciiChar' based on its category.+--+-- >>> categorize [char| ',' |]+-- Punctuation+-- >>> categorize [char| '~' |]+-- Symbol+-- >>> categorize [char| 'w' |]+-- Letter+-- >>> categorize [char| '2' |]+-- Number+-- >>> categorize [char| '\0' |]+-- Other+--+-- @since 1.0.0+categorize :: AsciiChar -> AsciiCategory+categorize c@(AsciiChar w8)+  | charType c == Control = Other+  | w8 == 0x20 = Punctuation+  | w8 >= 0x21 && w8 <= 0x23 = Punctuation+  | w8 == 0x24 = Symbol+  | w8 >= 0x25 && w8 <= 0x2a = Punctuation+  | w8 == 0x2b = Symbol+  | w8 >= 0x2c && w8 <= 0x2f = Punctuation+  | w8 >= 0x30 && w8 <= 0x39 = Number+  | w8 >= 0x3a && w8 <= 0x3b = Punctuation+  | w8 >= 0x3c && w8 <= 0x3e = Symbol+  | w8 >= 0x3f && w8 <= 0x40 = Punctuation+  | w8 >= 0x41 && w8 <= 0x5a = Letter+  | w8 >= 0x5b && w8 <= 0x5d = Punctuation+  | w8 == 0x5e = Symbol+  | w8 == 0x5f = Punctuation+  | w8 == 0x60 = Symbol+  | w8 >= 0x61 && w8 <= 0x7a = Letter+  | w8 == 0x7b = Punctuation+  | w8 == 0x7c = Symbol+  | w8 == 0x7d = Punctuation+  | otherwise = Symbol -- This only leaves ~. - Koz++-- | Compatibility method for the 'GeneralCategory' provided by 'Data.Char'.+--+-- >>> categorizeGeneral [char| ',' |]+-- OtherPunctuation+-- >>> categorizeGeneral [char| '~' |]+-- MathSymbol+-- >>> categorizeGeneral [char| 'w' |]+-- LowercaseLetter+-- >>> categorizeGeneral [char| '2' |]+-- DecimalNumber+-- >>> categorizeGeneral [char| '\0' |]+-- Control+--+-- @since 1.0.0+categorizeGeneral :: AsciiChar -> GeneralCategory+categorizeGeneral (AsciiChar w8) = generalCategory . chr . fromIntegral $ w8++-- | The case of an ASCII character (if it has one).+--+-- @since 1.0.0+newtype AsciiCase = AsciiCase Word8+  deriving (Eq, Ord, Hashable, NFData) via Word8++-- | @since 1.0.0+instance Show AsciiCase where+  {-# INLINEABLE show #-}+  show = \case+    Upper -> "Upper"+    Lower -> "Lower"++-- | @since 1.0.0+instance Bounded AsciiCase where+  minBound = Upper+  maxBound = Lower++-- | Indicator of an uppercase character.+--+-- @since 1.0.0+pattern Upper :: AsciiCase+pattern Upper <-+  AsciiCase 0+  where+    Upper = AsciiCase 0++-- | Indicator of a lowercase character.+--+-- @since 1.0.0+pattern Lower :: AsciiCase+pattern Lower <-+  AsciiCase 1+  where+    Lower = AsciiCase 1++{-# COMPLETE Upper, Lower #-}++-- | Determine the case of an 'AsciiChar'. Returns 'Nothing' if the character+-- doesn't have a case.+--+-- >>> caseOf [char| 'w' |]+-- Just Lower+-- >>> caseOf [char| 'W' |]+-- Just Upper+-- >>> caseOf [char| '~' |]+-- Nothing+--+-- @since 1.0.0+caseOf :: AsciiChar -> Maybe AsciiCase+caseOf c@(AsciiChar w8)+  | categorize c /= Letter = Nothing+  | w8 <= 0x5a = Just Upper+  | otherwise = Just Lower++-- Optics++-- | A representation of the relationship between 'Char' and 'AsciiChar'.+--+-- @since 1.0.0+charWise :: Prism' Char AsciiChar+charWise = prism' (chr . fromIntegral . toByte) fromChar++-- | A representation of the relationship between ASCII characters and bytes.+--+-- @since 1.0.0+byteWise :: Prism' Word8 AsciiChar+byteWise = prism' toByte fromByte
+ src/Text/Ascii/Internal.hs view
@@ -0,0 +1,116 @@+{-# LANGUAGE DerivingVia #-}+{-# LANGUAGE PatternSynonyms #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE Trustworthy #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ViewPatterns #-}++module Text.Ascii.Internal where++import Control.DeepSeq (NFData)+import Data.ByteString (ByteString)+import Data.Char (chr, isAscii)+import Data.Coerce (coerce)+import Data.Hashable (Hashable)+import Data.Word (Word8)+import GHC.Exts (IsList (Item, fromList, fromListN, toList))+import Numeric (showHex)+import Type.Reflection (Typeable)++-- | Represents valid ASCII characters, which are bytes from @0x00@ to @0x7f@.+--+-- @since 1.0.0+newtype AsciiChar = AsciiChar {toByte :: Word8}+  deriving+    ( -- | @since 1.0.0+      Eq,+      -- | @since 1.0.0+      Ord,+      -- | @since 1.0.0+      Hashable,+      -- | @since 1.0.0+      NFData+    )+    via Word8+  deriving stock+    ( -- | @since 1.0.0+      Typeable+    )++-- | @since 1.0.0+instance Show AsciiChar where+  {-# INLINEABLE show #-}+  show (AsciiChar w8) = "'0x" <> showHex w8 "'"++-- | @since 1.0.0+instance Bounded AsciiChar where+  minBound = AsciiChar 0+  maxBound = AsciiChar 127++-- | View an 'AsciiChar' as its underlying byte. You can pattern match on this,+-- but since there are more bytes than valid ASCII characters, you cannot use+-- this to construct.+--+-- @since 1.0.0+pattern AsByte :: Word8 -> AsciiChar+pattern AsByte w8 <- AsciiChar w8++-- | View an 'AsciiChar' as a 'Char'. You can pattern match on this, but since+-- there are more 'Char's than valid ASCII characters, you cannot use this to+-- construct.+--+-- @since 1.0.0+pattern AsChar :: Char -> AsciiChar+pattern AsChar c <- AsciiChar (isJustAscii -> Just c)++{-# COMPLETE AsByte #-}++{-# COMPLETE AsChar #-}++-- | A string of ASCII characters, represented as a packed byte array.+--+-- @since 1.0.0+newtype AsciiText = AsciiText ByteString+  deriving+    ( -- | @since 1.0.0+      Eq,+      -- | @since 1.0.0+      Ord,+      -- | @since 1.0.0+      NFData,+      -- | @since 1.0.0+      Semigroup,+      -- | @since 1.0.0+      Monoid,+      -- | @since 1.0.0+      Show+    )+    via ByteString++-- | @since 1.0.0+instance IsList AsciiText where+  type Item AsciiText = AsciiChar+  {-# INLINEABLE fromList #-}+  fromList =+    coerce @ByteString @AsciiText+      . fromList+      . coerce @[AsciiChar] @[Word8]+  {-# INLINEABLE fromListN #-}+  fromListN n =+    coerce @ByteString @AsciiText+      . fromListN n+      . coerce @[AsciiChar] @[Word8]+  {-# INLINEABLE toList #-}+  toList = coerce . toList . coerce @AsciiText @ByteString++-- Helpers++isJustAscii :: Word8 -> Maybe Char+isJustAscii w8 =+  if isAscii asChar+    then pure asChar+    else Nothing+  where+    asChar :: Char+    asChar = chr . fromIntegral $ w8
+ src/Text/Ascii/QQ.hs view
@@ -0,0 +1,150 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE Trustworthy #-}++module Text.Ascii.QQ where++import Data.ByteString (ByteString)+import qualified Data.ByteString as BS+import Data.Char+  ( isAlphaNum,+    isAscii,+    isPunctuation,+    isSymbol,+    ord,+  )+import Data.Functor (void)+import GHC.Exts (IsList (fromList))+import Language.Haskell.TH.Quote (QuasiQuoter (QuasiQuoter))+import Language.Haskell.TH.Syntax+  ( Dec,+    Exp (AppE, ConE, ListE, LitE, VarE),+    Lit (IntegerL),+    Pat,+    Q,+    Type,+  )+import Text.Ascii.Internal (AsciiChar (AsciiChar), AsciiText (AsciiText))+import Text.Parsec+  ( Parsec,+    between,+    eof,+    lookAhead,+    manyTill,+    oneOf,+    parse,+    satisfy,+    spaces,+    try,+  )+import qualified Text.Parsec as Parsec++-- $setup+-- >>> :set -XQuasiQuotes+-- >>> import Text.Ascii.QQ++-- | Allows constructing ASCII characters from literals, whose correctness is+-- checked by the compiler.+--+-- Currently, accepts literal syntax similar to the Haskell parser, with escape+-- sequences preceded by \'\\\'. In particular, this includes the single quote+-- (see the example below).+--+-- >>> [char| '\'' |]+-- '0x27'+--+-- @since 1.0.0+char :: QuasiQuoter+char = QuasiQuoter charQQ (errPat "char") (errType "char") (errDec "char")++-- | Allows constructing ASCII strings from literals, whose correctness is+-- checked by the compiler.+--+-- Currently accepts literal syntax similar to the Haskell parser, with escape+-- sequences preceded by \'\\\'. In particular, this includes the double quote+-- (see the example below).+--+-- >>> [ascii| "\"Nyan!\", said the catboy." |]+-- "\"Nyan!\", said the catboy."+--+-- @since 1.0.0+ascii :: QuasiQuoter+ascii = QuasiQuoter asciiQQ (errPat "ascii") (errType "ascii") (errDec "ascii")++-- Helpers++asciiQQ :: String -> Q Exp+asciiQQ input = case parse (between open close go) "" input of+  Left err -> fail . show $ err+  Right result ->+    pure+      . AppE (ConE 'AsciiText)+      . AppE (VarE 'fromList)+      . ListE+      . fmap (LitE . IntegerL . fromIntegral)+      . BS.unpack+      $ result+  where+    open :: Parsec String () ()+    open = spaces *> (void . Parsec.char $ '"')+    close :: Parsec String () ()+    close = Parsec.char '"' *> spaces *> eof+    go :: Parsec String () ByteString+    go = BS.pack <$> manyTill asciiByte (lookAhead . try . Parsec.char $ '"')+    asciiByte = do+      c <- satisfy isAscii+      case c of+        '\\' -> do+          c' <- oneOf "0abfnrtv\\\""+          pure . fromIntegral . ord $ case c' of+            '0' -> '\0'+            'a' -> '\a'+            'b' -> '\b'+            'f' -> '\f'+            'n' -> '\n'+            'r' -> '\r'+            't' -> '\t'+            'v' -> '\v'+            '\\' -> '\\'+            _ -> '"'+        _ -> pure . fromIntegral . ord $ c++charQQ :: String -> Q Exp+charQQ input = case parse (between open close go) "" input of+  Left err -> fail . show $ err+  Right result ->+    pure . AppE (ConE 'AsciiChar) . LitE . IntegerL . fromIntegral $ result+  where+    open :: Parsec String () ()+    open = spaces *> (void . Parsec.char $ '\'')+    close :: Parsec String () ()+    close = Parsec.char '\'' *> spaces *> eof+    go :: Parsec String () Int+    go = do+      c1 <- satisfy isValidLead+      case c1 of+        '\\' -> do+          c2 <- oneOf "0abfnrtv\\\'"+          pure . ord $ case c2 of+            '0' -> '\0'+            'a' -> '\a'+            'b' -> '\b'+            'f' -> '\f'+            'n' -> '\n'+            'r' -> '\r'+            't' -> '\t'+            'v' -> '\v'+            '\\' -> '\\'+            _ -> '\''+        _ -> pure . ord $ c1++isValidLead :: Char -> Bool+isValidLead c = isAscii c && (isAlphaNum c || c == ' ' || isSymbol c || isPunctuation c)++errPat :: String -> String -> Q Pat+errPat name _ = fail $ "'" <> name <> "' should not be used in a pattern context."++errType :: String -> String -> Q Type+errType name _ = fail $ "'" <> name <> "' should not be used in a type context."++errDec :: String -> String -> Q [Dec]+errDec name _ = fail $ "'" <> name <> "' should not be used in a declaration context."
+ text-ascii.cabal view
@@ -0,0 +1,55 @@+cabal-version:      3.0+name:               text-ascii+version:            1.0.0+synopsis:           ASCII string and character processing.+description:+  A total-by-default, tested and documented library for+  working with ASCII text. Low on dependencies , high on usability.++homepage:           https://github.org/kozross/text-ascii+license:            Apache-2.0+license-file:       LICENSE.md+author:             Koz Ross+maintainer:         koz.ross@retro-freedom.nz+bug-reports:        https://github.org/kozross/text-ascii/issues+copyright:          (C) Koz Ross 2021+category:           Text+tested-with:        GHC ==8.6.5 || ==8.8.4 || ==8.10.3+build-type:         Simple+extra-source-files:+  CHANGELOG.md+  README.md++library+  exposed-modules:+    Text.Ascii+    Text.Ascii.Char++  other-modules:+    Text.Ascii.Internal+    Text.Ascii.QQ++  build-depends:+    , base              >=4.12     && <5+    , bytestring        ^>=0.11.0.0+    , deepseq           ^>=1.4.0.0+    , hashable          ^>=1.3.0.0+    , optics-core       ^>=0.3.0.0+    , parsec            ^>=3.1.14.0+    , template-haskell  >=2.14.0.0 && <3.0.0.0+    , text              ^>=1.2.4.1++  ghc-options:+    -Wall -Wcompat -Wincomplete-record-updates+    -Wincomplete-uni-patterns -Wredundant-constraints++  if flag(dev)+    ghc-options: -Werror++  hs-source-dirs:   src+  default-language: Haskell2010++flag dev+  description: Always use this during development!+  manual:      True+  default:     False