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 +5/−0
- LICENSE.md +201/−0
- README.md +63/−0
- Setup.hs +3/−0
- src/Text/Ascii.hs +999/−0
- src/Text/Ascii/Char.hs +368/−0
- src/Text/Ascii/Internal.hs +116/−0
- src/Text/Ascii/QQ.hs +150/−0
- text-ascii.cabal +55/−0
+ 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