packages feed

core-text (empty) → 0.2.0.0

raw patch · 7 files changed

+1279/−0 lines, 7 filesdep +basedep +bytestringdep +deepseq

Dependencies added: base, bytestring, deepseq, fingertree, hashable, prettyprinter, prettyprinter-ansi-terminal, template-haskell, text, text-short

Files

+ LICENCE view
@@ -0,0 +1,32 @@+Opinionated Haskell Interoperability++Copyright © 2018-2019 Operational Dynamics Consulting, Pty Ltd and Others+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions+are met:++    1. Redistributions of source code must retain the above copyright+       notice, this list of conditions and the following disclaimer.++    2. Redistributions in binary form must reproduce the above+       copyright notice, this list of conditions and the following+       disclaimer in the documentation and/or other materials provided+       with the distribution.+      +    3. Neither the name of the project nor the names of its contributors+       may be used to endorse or promote products derived from this +       software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ core-text.cabal view
@@ -0,0 +1,54 @@+cabal-version: 1.12+name: core-text+version: 0.2.0.0+license: BSD3+license-file: LICENCE+copyright: © 2018-2019 Operational Dynamics Consulting Pty Ltd, and Others+maintainer: Andrew Cowie <andrew@operationaldynamics.com>+author: Andrew Cowie <andrew@operationaldynamics.com>+stability: experimental+tested-with: ghc ==8.6.5+homepage: https://github.com/oprdyn/unbeliever#readme+bug-reports: https://github.com/oprdyn/unbeliever/issues+synopsis: A text type based on a finger tree over UTF-8 fragments+description:+    A data type for text, built as a finger tree over UTF-8 text fragments.+    .+    The main type and its usage are described at "Core.Text.Rope" in this+    package.+    .+    This is part of a library intended to ease interoperability and assist in+    building command-line programs, both tools and longer-running daemons.+    A list of features and some background to the library's design is contained+    in the+    <https://github.com/oprdyn/unbeliever/blob/master/README.markdown README>+    on GitHub.+category: System+build-type: Simple++source-repository head+    type: git+    location: https://github.com/oprdyn/unbeliever++library+    exposed-modules:+        Core.Text+        Core.Text.Bytes+        Core.Text.Rope+        Core.Text.Utilities+    hs-source-dirs: lib+    other-modules:+        Core.Text.Breaking+    default-language: Haskell2010+    ghc-options: -Wall -Wwarn -fwarn-tabs+    build-depends:+        base >=4.11 && <5,+        bytestring >=0.10.8.2 && <0.11,+        deepseq >=1.4.4.0 && <1.5,+        fingertree >=0.1.4.2 && <0.2,+        hashable >=1.2.7.0 && <1.3,+        prettyprinter >=1.2.1 && <1.3,+        prettyprinter-ansi-terminal >=1.1.1.2 && <1.2,+        template-haskell >=2.14.0.0 && <2.15,+        text >=1.2.3.1 && <1.3,+        text-short >=0.1.2 && <0.2
+ lib/Core/Text.hs view
@@ -0,0 +1,36 @@+{-# OPTIONS_HADDOCK not-home #-}++{-|+A unified Text type providing interoperability between various text+back-ends present in the Haskell ecosystem.++This is intended to be used directly:++@+import "Core.Text"+@++as this module re-exports all of the various components making up this+library's text handling subsystem.+-}+module Core.Text+    (+        {-* Internal representation -}+{-|+Exposes 'Bytes', a wrapper around different types of binary data, and 'Rope',+a finger-tree over buffers containing text.+-}+        module Core.Text.Bytes+      , module Core.Text.Rope++        {-* Useful utilities -}+{-|+Useful functions for common use cases.+-}+      , module Core.Text.Utilities+    ) where++import Core.Text.Bytes+import Core.Text.Rope+import Core.Text.Utilities+
+ lib/Core/Text/Breaking.hs view
@@ -0,0 +1,147 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_HADDOCK hide #-}++-- This is an Internal module, hidden from Haddock+module Core.Text.Breaking+    ( breakWords+    , breakLines+    , breakPieces+    , intoPieces+    , intoChunks+    )+where++import Data.Char (isSpace)+import Data.Foldable (foldr)+import Data.List (uncons)+import qualified Data.Text.Short as S (ShortText, null, break, uncons,empty)++import Core.Text.Rope++{-|+Split a passage of text into a list of words. A line is broken wherever+there is one or more whitespace characters, as defined by "Data.Char"'s+'Data.Char.isSpace'.++Examples:++@+λ> __breakWords \"This is a test\"__+[\"This\",\"is\",\"a\",\"test\"]+λ> __breakWords (\"St\" <> \"op and \" <> \"go left\")__+[\"Stop\",\"and\",\"go\",\"left\"]+λ> __breakWords emptyRope__+[]+@+-}+breakWords :: Rope -> [Rope]+breakWords = filter (not . nullRope) . breakPieces isSpace++{-|+Split a paragraph of text into a list of its individual lines. The+paragraph will be broken wherever there is a @'\n'@ character.++Blank lines will be preserved. Note that as a special case you do /not/ get+a blank entry at the end of the a list of newline terminated strings.++@+λ> __breakLines \"Hello\\n\\nWorld\\n\"__+[\"Hello\",\"\",\"World\"]+@+-}+breakLines :: Rope -> [Rope]+breakLines text =+  let+    result = breakPieces isNewline text+    n = length result - 1+    (fore,aft) = splitAt n result+  in case result of+    [] -> []+    [p] -> [p]+    _ -> if aft == [""]+        then fore+        else result++isNewline :: Char -> Bool+isNewline c = c == '\n'++{-|+Break a Rope into pieces whereever the given predicate function returns+@True@. If found, that character will not be included on either side. Empty+runs, however, *will* be preserved.+-}+breakPieces :: (Char -> Bool) -> Rope -> [Rope]+breakPieces predicate text =+  let+    x = unRope text+    (final,result) = foldr (intoPieces predicate) (Nothing,[]) x+  in+    case final of+       Nothing -> result+       Just piece -> intoRope piece : result++{-+Was the previous piece a match, or are we in the middle of a run of+characters? If we were, then join the previous run to the current piece+before processing into chunks.+-}+-- now for right fold+intoPieces :: (Char -> Bool) -> S.ShortText -> (Maybe S.ShortText,[Rope]) -> (Maybe S.ShortText,[Rope])+intoPieces predicate piece (stream,list) =+  let+    piece' = case stream of+        Nothing -> piece+        Just previous -> piece <> previous       -- more rope, less text?++    pieces = intoChunks predicate piece'+  in+    case uncons pieces of+        Nothing -> (Nothing,list)+        Just (text,remainder) -> (Just (fromRope text),remainder ++ list)++--+-- λ> S.break isSpace "a d"+-- ("a"," d")+--+-- λ> S.break isSpace " and"+-- (""," and")+--+-- λ> S.break isSpace "and "+-- ("and"," ")+--+-- λ> S.break isSpace ""+-- ("","")+--+-- λ> S.break isSpace " "+-- (""," ")+--++{-+This was more easily expressed as ++  let+    remainder' = S.drop 1 remainder+  in+    if remainder == " "++for the case when we were breaking on spaces. But generalized to a predicate+we have to strip off the leading character and test that its the only character;+this is cheaper than S.length etc.+-}+intoChunks :: (Char -> Bool) -> S.ShortText -> [Rope]+intoChunks _ piece | S.null piece = []+intoChunks predicate piece =+  let+    (chunk,remainder) = S.break predicate piece++    -- Handle the special case that a trailing " " (generalized to predicate)+    -- is the only character left.+    (trailing,remainder') = case S.uncons remainder of+        Nothing -> (False,S.empty)+        Just (c,remaining) -> if S.null remaining+            then (predicate c,S.empty)+            else (False,remaining)+  in+    if trailing+        then intoRope chunk : emptyRope : []+        else intoRope chunk : intoChunks predicate remainder'
+ lib/Core/Text/Bytes.hs view
@@ -0,0 +1,150 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE StrictData #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TypeFamilies #-}+{-# OPTIONS_GHC -fno-warn-unused-imports #-}        -- FIXME+{-# OPTIONS_GHC -fno-warn-incomplete-patterns #-}   -- FIXME+{-# OPTIONS_HADDOCK prune #-}++{-|+Binary (as opposed to textual) data is encountered in weird corners of the+Haskell ecosystem. We tend to forget (for example) that the content+recieved from a web server is /not/ text until we convert it from UTF-8 (if+that's what it is); and of course that glosses over the fact that something+of content-type @image/jpeg@ is not text in any way, shape, or form.++Bytes also show up when working with crypto algorithms, taking hashes, and+when doing serialization to external binary formats. Although we frequently+display these in terminals (and in URLs!) as text, but we take for granted+that we have actually deserialized the data or rendered the it in+hexidecimal or base64 or...++This module presents a simple wrapper around various representations of+binary data to make it easier to interoperate with libraries supplying+or consuming bytes.+-}+module Core.Text.Bytes+    ( Bytes+    , Binary(fromBytes, intoBytes)+    , hOutput+    , hInput+      {-* Internals -}+    , unBytes+    ) where++import qualified Data.ByteString as B (ByteString, foldl', splitAt+    , pack, unpack, length, hPut, hGetContents)+import qualified Data.ByteString.Char8 as C (pack, unpack)+import qualified Data.ByteString.Builder as B (Builder, toLazyByteString, byteString)+import qualified Data.ByteString.Lazy as L (ByteString, fromStrict, toStrict)+import Data.Hashable (Hashable)+import Data.Word (Word8)+import GHC.Generics (Generic)+import Data.Text.Prettyprint.Doc+    ( Doc, emptyDoc, pretty, annotate, (<+>), hsep, vcat+    , space, punctuate, hcat, group, flatAlt, sep, fillSep+    , line, line', softline, softline', hardline+    )+import Data.Text.Prettyprint.Doc.Render.Terminal (+    color, colorDull, bold, Color(..))+import System.IO (Handle)++{-|+A block of data in binary form.+-}+data Bytes+    = StrictBytes B.ByteString+    deriving (Show, Eq, Ord, Generic)++{-|+Access the strict 'ByteString' underlying the @Bytes@ type.+-}+unBytes :: Bytes -> B.ByteString+unBytes (StrictBytes b') = b'+{-# INLINE unBytes #-}++instance Hashable Bytes++{-|+Conversion to and from various types containing binary data into our+convenience Bytes type.++As often as not these conversions are /expensive/; these methods are+here just to wrap calling the relevant functions in a uniform interface.+-}+class Binary α where+    fromBytes :: Bytes -> α+    intoBytes :: α -> Bytes++instance Binary Bytes where+    fromBytes = id+    intoBytes = id++{-| from "Data.ByteString" Strict -}+instance Binary B.ByteString where+    fromBytes (StrictBytes b') = b'+    intoBytes b' = StrictBytes b'++{-| from "Data.ByteString.Lazy" -}+instance Binary L.ByteString where+    fromBytes (StrictBytes b') = L.fromStrict b'+    intoBytes b' = StrictBytes (L.toStrict b')      -- expensive++instance Binary B.Builder where+    fromBytes (StrictBytes b') = B.byteString b'+    intoBytes b' = StrictBytes (L.toStrict (B.toLazyByteString b'))++{-| from "Data.Word" -}+instance Binary [Word8] where+    fromBytes (StrictBytes b') = B.unpack b'+    intoBytes = StrictBytes . B.pack++{-|+Output the content of the 'Bytes' to the specified 'Handle'.++@+    hOutput h b+@++'Core.Program.Execute.output' provides a convenient way to write a @Bytes@+to a file or socket handle from within the 'Core.Program.Execute.Program'+monad.++Don't use this function to write to @stdout@ if you are using any of the+other output or logging facililities of this libarary as you will corrupt+the ordering of output on the user's terminal. Instead do:++@+    'Core.Program.Execute.write' ('intoRope' b)+@++on the assumption that the bytes in question are UTF-8 (or plain ASCII)+encoded.+-}+hOutput :: Handle -> Bytes -> IO ()+hOutput handle (StrictBytes b') = B.hPut handle b'++{-|+Read the (entire) contents of a handle into a Bytes object.++If you want to read the entire contents of a file, you can do:++@+    contents <- 'Core.System.Base.withFile' name 'Core.System.Base.ReadMode' 'hInput'+@++At any kind of scale, Streaming I/O is almost always for better, but for+small files you need to pick apart this is fine.+-}+hInput :: Handle -> IO Bytes+hInput handle = do+   contents <- B.hGetContents handle+   return (StrictBytes contents)++{-+instance Show Bytes where+    show x = case x of+        StrictBytes b' -> +-}
+ lib/Core/Text/Rope.hs view
@@ -0,0 +1,493 @@+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE StrictData #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE InstanceSigs #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++{-|+If you're accustomed to working with text in almost any other programming+language, you'd be aware that a \"string\" typically refers to an in-memory+/array/ of characters. Traditionally this was a single ASCII byte per+character; more recently UTF-8 variable byte encodings which dramatically+complicates finding offsets but which gives efficient support for the+entire Unicode character space. In Haskell, the original text type,+'String', is implemented as a list of 'Char' which, because a Haskell list+is implemented as a /linked-list of boxed values/, is wildly inefficient at+any kind of scale.++In modern Haskell there are two primary ways to represent text.++First is via the [rather poorly named] @ByteString@ from the __bytestring__+package (which is an array of bytes in pinned memory). The+"Data.ByteString.Char8" submodule gives you ways to manipulate those arrays+as if they were ASCII characters. Confusingly there are both strict+(@Data.ByteString@) and lazy (@Data.ByteString.Lazy@) variants which are+often hard to tell the difference between when reading function signatures+or haddock documentation. The performance problem an immutable array backed+data type runs into is that appending a character (that is, ASCII byte) or+concatonating a string (that is, another array of ASCII bytes) is very+expensive and requires allocating a new larger array and copying the whole+thing into it. This led to the development of \"builders\" which amortize+this reallocation cost over time, but it can be cumbersome to switch+between @Builder@, the lazy @ByteString@ that results, and then having to+inevitably convert to a strict @ByteString@ because that's what the next+function in your sequence requires.++The second way is through the opaque @Text@ type of "Data.Text" from the+__text__ package, which is well tuned and high-performing but suffers from+the same design; it is likewise backed by arrays. Rather surprisingly, the+storage backing Text objects are encoded in UTF-16, meaning every time you+want to work with unicode characters that came in from /anywhere/ else and+which inevitably are UTF-8 encoded you have to convert to UTF-16 and copy+into a new array, wasting time and memory.++In this package we introduce 'Rope', a text type backed by the 2-3+'Data.FingerTree.FingerTree' data structure from the __fingertree__+package. This is not an uncommon solution in many languages as finger trees+support exceptionally efficient appending to either end and good+performance inserting anywhere else (you often find them as the backing+data type underneath text editors for this reason). Rather than 'Char' the+pieces of the rope are 'Data.Text.Short.ShortText' from the __text-short__+package, which are UTF-8 encoded and in normal memory managed by the+Haskell runtime. Conversion from other Haskell text types is not /O(1)/+(UTF-8 validity must be checked, or UTF-16 decoded, or...), but in our+benchmarking the performance has been comparable to the established types+and you may find the resultant interface for combining chunks is comparable+to using a Builder, without being forced to use a Builder.++'Rope' is used as the text type throughout this library. If you use the+functions within this package (rather than converting to other text types)+operations are quite efficient. When you do need to convert to another type+you can use 'fromRope' or 'intoRope' from the 'Textual' typeclass.++Note that we haven't tried to cover the entire gamut of operations or+customary convenience functions you would find in the other libraries; so+far 'Rope' is concentrated on aiding interoperation, being good at+appending (lots of) small pieces, and then efficiently taking the resultant+text object out to a file handle, be that the terminal console, a file, or+a network socket.++-}+module Core.Text.Rope+    ( {-* Rope type -}+      Rope+    , emptyRope+    , widthRope+    , splitRope+    , insertRope+    , containsCharacter+      {-* Interoperation and Output -}+    , Textual(fromRope, intoRope, appendRope)+    , hWrite+      {-* Internals -}+    , unRope+    , nullRope+    , unsafeIntoRope+    , Width(..)+    ) where++import Control.DeepSeq (NFData(..))+import qualified Data.ByteString as B (ByteString)+import qualified Data.ByteString.Builder as B (toLazyByteString+    , hPutBuilder)+import qualified Data.ByteString.Lazy as L (ByteString, toStrict+    , foldrChunks)+import qualified Data.FingerTree as F (FingerTree, Measured(..), empty+    , singleton, (><), (<|), (|>), search, SearchResult(..), null+    , viewl, ViewL(..))+import Data.Foldable (foldr, foldr', foldMap, toList, any)+import Data.Hashable (Hashable, hashWithSalt)+import Data.String (IsString(..))+import qualified Data.Text as T (Text)+import qualified Data.Text.Lazy as U (Text, fromChunks, foldrChunks+    , toStrict)+import qualified Data.Text.Lazy.Builder as U (Builder, toLazyText+    , fromText)+import Data.Text.Prettyprint.Doc (Pretty(..), emptyDoc)+import qualified Data.Text.Short as S (ShortText, length, any, null+    , fromText, toText, fromByteString, pack, unpack+    , append, empty, toBuilder, splitAt)+import qualified Data.Text.Short.Unsafe as S (fromByteStringUnsafe)+import GHC.Generics (Generic)+import System.IO (Handle)++import Core.Text.Bytes++{-|+A type for textual data. A rope is text backed by a tree data structure,+rather than a single large continguous array, as is the case for strings.++There are three use cases:++/Referencing externally sourced data/++Often we interpret large blocks of data sourced from external systems as+text. Ideally we would hold onto this without copying the memory, but (as+in the case of @ByteString@ which is the most common source of data) before+we can treat it as text we have to validate the UTF-8 content. Safety+first. We also copy it out of pinned memory, allowing the Haskell runtime+to manage the storage.++/Interoperating with other libraries/++The only constant of the Haskell universe is that you won't have the right+combination of {strict, lazy} × {@Text@, @ByteString@, @String@, @[Word8]@,+etc} you need for the next function call. The 'Textual' typeclass provides+for moving between different text representations. To convert between+@Rope@ and something else use 'fromRope'; to construct a @Rope@ from+textual content in another type use 'intoRope'.++You can get at the underlying finger tree with the 'unRope' function.++/Assembling text to go out/++This involves considerable appending of data, very very occaisionally+inserting it. Often the pieces are tiny. To add text to a @Rope@ use the+'appendRope' method as below or the ('Data.Semigroup.<>') operator from+"Data.Monoid" (like you would have with a @Builder@).++Output to a @Handle@ can be done efficiently with 'hWrite'.+-}+data Rope+    = Rope (F.FingerTree Width S.ShortText)+    deriving Generic++instance NFData Rope where+    rnf (Rope x) = foldMap (\piece -> rnf piece) x++instance Show Rope where+    show text = "\"" ++ fromRope text ++ "\""++instance Eq Rope where+    (==) (Rope x1) (Rope x2) = (==) (stream x1) (stream x2)+      where+        stream x = foldMap S.unpack x++instance Ord Rope where+    compare (Rope x1) (Rope x2) = compare x1 x2++instance Pretty Rope where+    pretty (Rope x) = foldr ((<>) . pretty . S.toText) emptyDoc x ++{-|+Access the finger tree underlying the @Rope@. You'll want the following+imports:++@+import qualified "Data.FingerTree" as F  -- from the __fingertree__ package+import qualified "Data.Text.Short" as S  -- from the __text-short__ package+@+-}+unRope :: Rope -> F.FingerTree Width S.ShortText+unRope (Rope x) = x+{-# INLINE unRope #-}+++{-|+The length of the @Rope@, in characters. This is the monoid used to+structure the finger tree underlying the @Rope@.+-}+newtype Width = Width Int+    deriving (Eq, Ord, Show, Num, Generic)++instance F.Measured Width S.ShortText where+    measure :: S.ShortText -> Width+    measure piece = Width (S.length piece)++instance Semigroup Width where+    (<>) (Width w1) (Width w2) = Width (w1 + w2)++instance Monoid Width where+    mempty = Width 0+    mappend = (<>)++-- here Maybe we just need type Strand = ShortText and then Rope is+-- FingerTree Strand or Builder (Strand)++instance IsString Rope where+    fromString "" = emptyRope+    fromString xs = Rope . F.singleton . S.pack $ xs++instance Semigroup Rope where+    (<>) text1@(Rope x1) text2@(Rope x2) =+        if F.null x2+            then text1+            else if F.null x1+                then text2+                else Rope ((F.><) x1 x2) -- god I hate these operators++instance Monoid Rope where+    mempty = emptyRope+    mappend = (<>)++{-|+An zero-length 'Rope'. You can also use @\"\"@ presuming the+__@OverloadedStrings@__ language extension is turned on in your source+file.+-}+emptyRope :: Rope+emptyRope = Rope F.empty+{-# INLINABLE emptyRope #-}++{-|+Get the length of this text, in characters.+-}+widthRope :: Rope -> Int+widthRope = foldr' f 0 . unRope+  where+    f piece count = S.length piece + count++nullRope :: Rope -> Bool+nullRope (Rope x) = case F.viewl x of+    F.EmptyL        -> True+    (F.:<) piece _  -> S.null piece++{-|+Break the text into two pieces at the specified offset.++Examples:++@+λ> __splitRope 0 \"abcdef\"__+(\"\", \"abcdef\")+λ> __splitRope 3 \"abcdef\"__+(\"abc\", \"def\")+λ> __splitRope 6 \"abcdef\"__+(\"abcdef\",\"\")+@++Going off either end behaves sensibly:++@+λ> __splitRope 7 \"abcdef\"__+(\"abcdef\",\"\")+λ> __splitRope (-1) \"abcdef\"__+(\"\", \"abcdef\")+@+-}+splitRope :: Int -> Rope -> (Rope,Rope)+splitRope i text@(Rope x) =+  let+    pos = Width i+    result = F.search (\w1 _ -> w1 >= pos) x+  in+    case result of+        F.Position before piece after ->+          let+            (Width w) = F.measure before+            (one,two) = S.splitAt (i - w) piece+          in+            (Rope ((F.|>) before one),Rope ((F.<|) two after))+        F.OnLeft -> (Rope F.empty, text)+        F.OnRight -> (text, Rope F.empty)+        F.Nowhere -> error "Position not found in split. Probable cause: predicate function given not monotonic. This is supposed to be unreachable"++{-|+Insert a new piece of text into an existing @Rope@ at the specified offset.++Examples:++@+λ> __insertRope 3 \"Con\" \"Def 1\"__+"DefCon 1"+λ> __insertRope 0 \"United \" \"Nations\"__+"United Nations"+@+-}+insertRope :: Int -> Rope -> Rope -> Rope+insertRope 0 (Rope new) (Rope x) = Rope ((F.><) new x)+insertRope i (Rope new) text =+  let+    (Rope before,Rope after) = splitRope i text+  in+    Rope (mconcat [before, new, after])++--+-- Manual instance to get around the fact that FingerTree doesn't have a+-- Hashable instance. If this were ever to become a hotspot we could+-- potentially use the Hashed caching type in the finger tree as+--+-- FingerTree Width (Hashed S.ShortText)+--+-- at the cost of endless unwrapping.+--+instance Hashable Rope where+    hashWithSalt salt (Rope x) = foldr f salt x+      where+        f :: S.ShortText -> Int -> Int+        f piece num = hashWithSalt num piece++{-|+Machinery to interpret a type as containing valid Unicode that can be+represented as a @Rope@ object.++/Implementation notes/++Given that @Rope@ is backed by a finger tree, 'append' is relatively+inexpensive, plus whatever the cost of conversion is. There is a subtle+trap, however: if adding small fragments of that were obtained by slicing+(for example) a large ByteString we would end up holding on to a reference+to the entire underlying block of memory. This module is optimized to+reduce heap fragmentation by letting the Haskell runtime and garbage+collector manage the memory, so instances are expected to /copy/ these+substrings out of pinned memory.++The @ByteString@ instance requires that its content be valid UTF-8. If not+an empty @Rope@ will be returned.++Several of the 'fromRope' implementations are expensive and involve a lot+of intermediate allocation and copying. If you're ultimately writing to a+handle prefer 'hWrite' which will write directly to the output buffer.+-}+class Textual α where+    {-|+Convert a @Rope@ into another text-like type.+    -}+    fromRope :: Rope -> α+    {-|+Take another text-like type and convert it to a @Rope@.+    -}+    intoRope :: α -> Rope+    {-|+Append some text to this @Rope@. The default implementation is basically a+convenience wrapper around calling 'intoRope' and 'mappend'ing it to your+text (which will work just fine, but for some types more efficient+implementations are possible).+    -}+    appendRope :: α -> Rope -> Rope+    appendRope thing text = text <> intoRope thing++instance Textual (F.FingerTree Width S.ShortText) where+    fromRope = unRope+    intoRope = Rope++instance Textual Rope where+    fromRope = id+    intoRope = id+    appendRope (Rope x2) (Rope x1) = Rope ((F.><) x1 x2)++{-| from "Data.Text.Short" -}+instance Textual S.ShortText where+    fromRope = foldr S.append S.empty . unRope+    intoRope = Rope . F.singleton+    appendRope piece (Rope x) = Rope ((F.|>) x piece)++{-| from "Data.Text" Strict -}+instance Textual T.Text where+    fromRope = U.toStrict . U.toLazyText . foldr f mempty . unRope+      where+        f :: S.ShortText -> U.Builder -> U.Builder+        f piece built = (<>) (U.fromText (S.toText piece)) built++    intoRope t = Rope (F.singleton (S.fromText t))+    appendRope chunk (Rope x) = Rope ((F.|>) x (S.fromText chunk))++{-| from "Data.Text.Lazy" -}+instance Textual U.Text where+    fromRope (Rope x) = U.fromChunks . fmap S.toText . toList $ x+    intoRope t = Rope (U.foldrChunks ((F.<|) . S.fromText) F.empty t)++{-| from "Data.ByteString" Strict -}+instance Textual B.ByteString where+    fromRope = L.toStrict . B.toLazyByteString . foldr g mempty . unRope+      where+        g piece built = (<>) (S.toBuilder piece) built++    -- If the input ByteString does not contain valid UTF-8 then an empty+    -- Rope will be returned. That's not ideal.+    intoRope b' = case S.fromByteString b' of+        Just piece -> Rope (F.singleton piece)+        Nothing -> Rope F.empty         -- bad++    -- ditto+    appendRope b' (Rope x) = case S.fromByteString b' of+        Just piece -> Rope ((F.|>) x piece)+        Nothing -> (Rope x)             -- bad++{-| from "Data.ByteString.Lazy" -}+instance Textual L.ByteString where+    fromRope = B.toLazyByteString . foldr g mempty . unRope+      where+        g piece built = (<>) (S.toBuilder piece) built++    intoRope b' = Rope (L.foldrChunks ((F.<|) . check) F.empty b')+      where+        check chunk = case S.fromByteString chunk of+            Just piece -> piece+            Nothing -> S.empty          -- very bad++instance Textual Bytes where+    fromRope = intoBytes . (fromRope :: Rope -> B.ByteString)+    intoRope = intoRope . unBytes++instance Binary Rope where+    fromBytes = intoRope . unBytes+    intoBytes = intoBytes . (fromRope :: Rope -> B.ByteString)+++{-|+If you /know/ the input bytes are valid UTF-8 encoded characters, then+you can use this function to convert to a piece of @Rope@.+-}+unsafeIntoRope :: B.ByteString -> Rope+unsafeIntoRope = Rope . F.singleton . S.fromByteStringUnsafe++{-| from "Data.String" -}+instance Textual [Char] where+    fromRope (Rope x) = foldr h [] x+      where+        h piece string = (S.unpack piece) ++ string -- ugh+    intoRope = Rope . F.singleton . S.pack++{-|+Write the 'Rope' to the given 'Handle'.++@+import "Core.Text"+import "Core.System" -- re-exports stdout++main :: IO ()+main =+  let+    text :: 'Rope'+    text = "Hello World"+  in+    'hWrite' 'System.IO.stdout' text+@+because it's tradition.++Uses 'Data.ByteString.Builder.hPutBuilder' internally which saves all kinds+of intermediate allocation and copying because we can go from the+'Data.Text.Short.ShortText's in the finger tree to+'Data.ByteString.Short.ShortByteString' to+'Data.ByteString.Builder.Builder' to the 'System.IO.Handle''s output buffer+in one go.++If you're working in the 'Core.Program.Execute.Program' monad, then+'Core.Program.Execute.write' provides an efficient way to write a @Rope@ to+@stdout@.+-}+hWrite :: Handle -> Rope -> IO ()+hWrite handle (Rope x) = B.hPutBuilder handle (foldr j mempty x)+  where+    j piece built = (<>) (S.toBuilder piece) built++{-|+Does the text contain this character?++We've used it to ask whether there are newlines present in a @Rope@, for+example:++@+    if 'containsCharacter' \'\\n\' text+        then handleComplexCase+        else keepItSimple+@+-}+containsCharacter :: Char -> Rope -> Bool+containsCharacter q (Rope x) = any j x+  where+    j piece = S.any (\c -> c == q) piece
+ lib/Core/Text/Utilities.hs view
@@ -0,0 +1,367 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE BangPatterns #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# OPTIONS_HADDOCK prune #-}++{-|+Useful tools for working with 'Rope's. Support for pretty printing,+multi-line strings, and...+-}+module Core.Text.Utilities (+      {-* Pretty printing -}+      Render(..)+    , render+      {-* Helpers -}+    , indefinite+    , breakWords+    , breakLines+    , breakPieces+    , wrap+    , underline+    , leftPadWith+    , rightPadWith+      {-* Multi-line strings -}+    , quote++    -- for testing+    , intoPieces+    , intoChunks++    , byteChunk+) where++import Data.Bits (Bits (..))+import Data.Char (intToDigit)+import qualified Data.ByteString as B (ByteString, splitAt, length, unpack)+import qualified Data.FingerTree as F ((<|), ViewL(..), viewl)+import qualified Data.List as List (foldl', dropWhileEnd, splitAt)+import Data.Monoid ((<>))+import qualified Data.Text as T+import qualified Data.Text.Short as S (ShortText, uncons, toText, replicate+    , singleton)+import Data.Text.Prettyprint.Doc (Doc, layoutPretty , annotate, reAnnotateS+    , Pretty(..), pretty, emptyDoc+    , LayoutOptions(LayoutOptions)+    , PageWidth(AvailablePerLine)+    , hsep, vcat, group, flatAlt+    , softline'+    )++import Data.Text.Prettyprint.Doc.Render.Terminal (renderLazy, AnsiStyle+    , color, Color(..))++import Data.Word (Word8)+import Language.Haskell.TH (litE, stringL)+import Language.Haskell.TH.Quote (QuasiQuoter(QuasiQuoter))++import Core.Text.Bytes+import Core.Text.Breaking+import Core.Text.Rope++-- change AnsiStyle to a custom token type, perhaps Ansi, which+-- has the escape codes already converted to Rope.++{-|+Types which can be rendered "prettily", that is, formatted by a pretty+printer and embossed with beautiful ANSI colours when printed to the+terminal.++Use 'render' to build text object for later use or "Core.Program.Execute"'s+'Core.Program.Execute.writeR' if you're writing directly to console now.+-}++class Render α where+    {-|+Which type are the annotations of your Doc going to be expressed in?+    -}+    type Token α :: *+    {-|+Convert semantic tokens to specific ANSI escape tokens+    -}+    colourize :: Token α -> AnsiStyle+    {-|+Arrange your type as a 'Doc' @ann@, annotated with your semantic+tokens.+    -}+    intoDocA :: α -> Doc (Token α)++instance Render Rope where+    type Token Rope = ()+    colourize = const mempty+    intoDocA = foldr f emptyDoc . unRope+      where+        f :: S.ShortText -> Doc () -> Doc ()+        f piece built = (<>) (pretty (S.toText piece)) built++instance Render Char where+    type Token Char = ()+    colourize = const mempty+    intoDocA c = pretty c++instance (Render a) => Render [a] where+    type Token [a] = Token a+    colourize = const mempty+    intoDocA = mconcat . fmap intoDocA++instance Render T.Text where+    type Token T.Text = ()+    colourize = const mempty+    intoDocA t = pretty t+++-- (), aka Unit, aka **1**, aka something with only one inhabitant++instance Render Bytes where+    type Token Bytes = ()+    colourize = const (color Green)+    intoDocA = prettyBytes++prettyBytes :: Bytes -> Doc ()+prettyBytes = annotate () . vcat . twoWords+    . fmap wordToHex . byteChunk . unBytes++twoWords :: [Doc ann] -> [Doc ann]+twoWords ds = go ds+  where+    go [] = []+    go [x] = [softline' <> x]+    go xs =+      let+        (one:two:[], remainder) = List.splitAt 2 xs+      in+        group (one <> spacer <> two) : go remainder++    spacer = flatAlt softline' "  "++byteChunk :: B.ByteString -> [B.ByteString]+byteChunk = reverse . go []+  where+    go acc blob =+      let+        (eight, remainder) = B.splitAt 8 blob+      in+        if B.length remainder == 0+            then eight : acc+            else go (eight : acc) remainder++-- Take an [up to] 8 byte (64 bit) word+wordToHex :: B.ByteString -> Doc ann+wordToHex eight =+  let+    ws = B.unpack eight+    ds = fmap byteToHex ws+  in+    hsep ds++byteToHex :: Word8 -> Doc ann+byteToHex c = pretty hi <> pretty low+  where+    !low      = byteToDigit $ c .&. 0xf+    !hi       = byteToDigit $ (c .&. 0xf0) `shiftR` 4++    byteToDigit :: Word8 -> Char+    byteToDigit = intToDigit . fromIntegral++{-|+Given an object of a type with a 'Render' instance, transform it into a+Rope saturated with ANSI escape codes representing syntax highlighting or+similar colouring, wrapping at the specified @width@.++The obvious expectation is that the next thing you're going to do is send+the Rope to console with:++@+    'Core.Program.Execute.write' ('render' 80 thing)+@++However, the /better/ thing to do is to instead use:++@+    'Core.Program.Execute.writeR' thing+@++which is able to pretty print the document text respecting the available+width of the terminal.+-}+-- the annotation (_ :: α) of the parameter is to bring type a into scope+-- at term level so that it can be used by TypedApplications. Which then+-- needed AllowAmbiguousTypes, but with all that finally it works:+-- colourize no longer needs a in its type signature.+render :: Render α => Int -> α -> Rope+render columns (thing :: α) =+  let+    options = LayoutOptions (AvailablePerLine (columns - 1) 1.0)+  in+    intoRope . renderLazy . reAnnotateS (colourize @α)+                . layoutPretty options . intoDocA $ thing++--+-- | Render "a" or "an" in front of a word depending on English's idea of+-- whether it's a vowel or not.+--+indefinite :: Rope -> Rope+indefinite text =+  let+    x = unRope text+  in+    case F.viewl x of+        F.EmptyL -> text+        piece F.:< _ -> case S.uncons piece of+            Nothing -> text+            Just (c,_)  -> if c `elem` ['A','E','I','O','U','a','e','i','o','u']+                then intoRope ("an " F.<| x)+                else intoRope ("a " F.<| x)++{-|+Often the input text represents a paragraph, but does not have any internal+newlines (representing word wrapping). This function takes a line of text+and inserts newlines to simulate such folding, keeping the line under+the supplied maximum width.++A single word that is excessively long will be included as-is on its own+line (that line will exceed the desired maxium width).++Any trailing newlines will be removed.+-}+wrap :: Int -> Rope -> Rope+wrap margin text =+  let+    built = wrapHelper margin (breakWords text)+  in+    built++wrapHelper :: Int -> [Rope] -> Rope+wrapHelper _ [] = ""+wrapHelper _ [x]  = x+wrapHelper margin (x:xs) =+    snd $ List.foldl' (wrapLine margin) (widthRope x, x) xs++wrapLine :: Int -> (Int, Rope) -> Rope -> (Int, Rope)+wrapLine margin (pos,builder) word =+  let+    wide = widthRope word+    wide' = pos + wide + 1+  in+    if wide' > margin+        then (wide , builder <> "\n" <> word)+        else (wide', builder <> " "  <> word)+++underline :: Char -> Rope -> Rope+underline level text =+  let+    title = fromRope text+    line = T.map (\_ -> level) title+  in+    intoRope line++{-|+Pad a pieve of text on the left with a specified character to the desired+width. This function is named in homage to the famous result from Computer+Science known as @leftPad@ which has a glorious place in the history of the+world-wide web.+-}+leftPadWith :: Char -> Int -> Rope -> Rope+leftPadWith c digits text =+    intoRope pad <> text+  where+    pad = S.replicate len (S.singleton c)+    len = digits - widthRope text+++{-|+Right pad a text with the specified character.+-}+rightPadWith :: Char -> Int -> Rope -> Rope+rightPadWith c digits text =+    text <> intoRope pad+  where+    pad = S.replicate len (S.singleton c)+    len = digits - widthRope text+++{-|+Multi-line string literals.++To use these you need to enable the @QuasiQuotes@ language extension+in your source file:++@+\{\-\# LANGUAGE OverloadedStrings \#\-\}+\{\-\# LANGUAGE QuasiQuotes \#\-\}+@++you are then able to easily write a string stretching over several lines.++How best to formatting multi-line string literal within your source code is+an aesthetic judgement. Sometimes you don't care about the whitespace+leading a passage (8 spaces in this example):++@+    let message = ['quote'|+        This is a test of the Emergency Broadcast System. Do not be+        alarmed. If this were a real emergency, someone would have tweeted+        about it by now.+    |]+@++because you are feeding it into a 'Data.Text.Prettyprint.Doc.Doc' for+pretty printing and know the renderer will convert the whole text into a+single line and then re-flow it. Other times you will want to have the+string as is, literally:++@+    let poem = ['quote'|+If the sun+    rises+        in the+    west+you     drank+    too much+                last week.+    |]+@++Leading whitespace from the first line and trailing whitespace from the+last line will be trimmed, so this:++@+    let value = ['quote'|+Hello+    |]+@++is translated to:++@+    let value = 'Data.String.fromString' \"Hello\\n\"+@++without the leading newline or trailing four spaces. Note that as string+literals they are presented to your code with 'Data.String.fromString' @::+String -> α@ so any type with an 'Data.String.IsString' instance (as 'Rope'+has) can be constructed from a multi-line @['quote'| ... |]@ literal.++-}+-- I thought this was going to be more complicated.+quote :: QuasiQuoter+quote = QuasiQuoter+    (litE . stringL . trim)        -- in an expression+    (error "Cannot use [quote| ... |] in a pattern")+    (error "Cannot use [quote| ... |] as a type")+    (error "Cannot use [quote| ... |] for a declaration")+  where+    trim :: String -> String+    trim = bot . top++    top [] = []+    top ('\n':cs) = cs+    top str = str++    bot = List.dropWhileEnd (== ' ')+