aboutsummaryrefslogtreecommitdiff
path: root/spec/src/Thermoprint/Printout.hs
diff options
context:
space:
mode:
authorGregor Kleen <gkleen@yggdrasil.li>2016-01-09 02:19:56 +0000
committerGregor Kleen <gkleen@yggdrasil.li>2016-01-09 02:19:56 +0000
commit8307d7e80a88f1425eb0a93bbde4adff388b7cdc (patch)
tree308a9f53c0873f2442342e829caecf7b90264f6b /spec/src/Thermoprint/Printout.hs
parenta2093f0a59dd1f557e8d5b885325d07a8aa55073 (diff)
downloadthermoprint-8307d7e80a88f1425eb0a93bbde4adff388b7cdc.tar
thermoprint-8307d7e80a88f1425eb0a93bbde4adff388b7cdc.tar.gz
thermoprint-8307d7e80a88f1425eb0a93bbde4adff388b7cdc.tar.bz2
thermoprint-8307d7e80a88f1425eb0a93bbde4adff388b7cdc.tar.xz
thermoprint-8307d7e80a88f1425eb0a93bbde4adff388b7cdc.zip
spec/src/Thermoprint/Printout.hs & test-framework
Implemented Printout, Paragraph, Chunk, Block, Line, and text Test coverage for text
Diffstat (limited to 'spec/src/Thermoprint/Printout.hs')
-rw-r--r--spec/src/Thermoprint/Printout.hs172
1 files changed, 172 insertions, 0 deletions
diff --git a/spec/src/Thermoprint/Printout.hs b/spec/src/Thermoprint/Printout.hs
new file mode 100644
index 0000000..d13d02e
--- /dev/null
+++ b/spec/src/Thermoprint/Printout.hs
@@ -0,0 +1,172 @@
1{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-}
2{-# LANGUAGE OverloadedStrings #-}
3{-# OPTIONS_HADDOCK show-extensions #-}
4
5-- | This module contains the definition of the structure -- 'Printout' -- used to represent the content of a job
6module Thermoprint.Printout
7 ( Printout(..)
8 , Paragraph(..)
9 , Chunk(..)
10 , Block(..)
11 , Line( HSpace
12 , SpaceSep
13 )
14 , text, cotext
15 , prop_text
16 ) where
17
18import Data.Sequence (Seq, (|>), (<|))
19
20import Data.Text.Lazy (Text)
21
22import Data.ByteString.Lazy (ByteString)
23
24import GHC.Generics (Generic)
25import Control.DeepSeq (NFData)
26
27import Test.QuickCheck.Arbitrary (Arbitrary(..), CoArbitrary, genericShrink)
28import Test.QuickCheck.Modifiers (NonNegative(..))
29import Test.QuickCheck.Gen (oneof, suchThat)
30import Test.QuickCheck.Instances
31import Test.QuickCheck (forAll, Property)
32
33
34import qualified Data.Text.Lazy as TL (lines, split, null, pack, filter, intercalate, map)
35import Data.Char (isSpace)
36
37import Data.Monoid (Monoid(..), (<>))
38
39import Data.List (dropWhile, dropWhileEnd, groupBy, genericLength, genericReplicate)
40
41import Data.Sequence as Seq (fromList, null, singleton)
42
43import Data.Function (on)
44
45import Data.Foldable (toList)
46
47
48-- | A 'Printout' is a sequence of visually seperated 'Paragraph's
49type Printout = Seq Paragraph
50
51-- | A 'Paragraph' is a non-seperated sequence of 'Chunk's
52type Paragraph = Seq Chunk
53
54-- | We introduce both 'Chunk' and 'Paragraph' mainly to allow 'Raw'.
55--
56-- Were we to disallow 'Raw', 'Block' would be identical to 'Paragraph'
57data Chunk = Cooked Block -- ^ text semantically structured to be rendered in accordance with the display format of printer
58 | Raw ByteString -- ^ direct instructions to the printer
59 deriving (Generic, NFData, Show, CoArbitrary)
60
61-- | 'Block' is the entry point for our structured document format
62data Block = Line Line -- ^ a single 'Line' of text
63 | VSpace Integer -- ^ vertical space of height equivalent to 'Integer' lines
64 | NewlSep (Seq Block) -- ^ A sequence of 'Block's seperated by newlines
65 deriving (Generic, NFData, Show, CoArbitrary)
66
67{- | A 'Line' is one of:
68
69 * a single word
70 * horizontal space equivalent to the width of 'Integer' `em`.
71 * a sequence of words seperated by spaces
72
73We don't export all constructors and instead encourage the use of 'text'.
74-}
75data Line = Word Text
76 | HSpace Integer
77 | SpaceSep (Seq Line)
78 deriving (Generic, NFData, Show, CoArbitrary)
79
80instance Monoid Block where
81 mempty = NewlSep mempty
82 x@(NewlSep xs) `mappend` y@(NewlSep ys)
83 | Seq.null xs = y
84 | Seq.null ys = x
85 | otherwise = NewlSep (xs <> ys)
86 (NewlSep xs) `mappend` y = NewlSep (xs |> y)
87 x `mappend` (NewlSep ys) = NewlSep (x <| ys)
88 x `mappend` y = NewlSep $ Seq.fromList [x, y]
89
90instance Monoid Line where
91 mempty = SpaceSep mempty
92 x@(SpaceSep xs) `mappend` y@(SpaceSep ys)
93 | Seq.null xs = y
94 | Seq.null ys = x
95 | otherwise = SpaceSep (xs <> ys)
96 (SpaceSep xs) `mappend` y = SpaceSep (xs |> y)
97 x `mappend` (SpaceSep ys) = SpaceSep (x <| ys)
98 x `mappend` y = SpaceSep $ Seq.fromList [x, y]
99
100
101text :: Text -> Either Block Line
102-- ^ Smart constructor for 'Line'/'Block' which maps word and line boundaries (as determined by 'isSpace' and 'TL.lines' respectively) to the structure of 'Block' and 'Line'.
103--
104-- Since we are unwilling to duplicate the list of chars from 'isSpace' we cannot reasonably determine a width for the various whitespace 'Char's.
105-- Thus they are all weighted equally as having width 1 `em`.
106text t = case splitLines t of
107 [] -> Right mempty
108 [Line x] -> Right x
109 xs -> Left $ mconcat xs
110 where
111 splitLines :: Text -> [Block]
112 splitLines t = map toBlock
113 . groupBy ((==) `on` TL.null)
114 $ TL.lines t
115 splitWords :: Text -> [Line]
116 splitWords t = map toLine
117 . groupBy ((==) `on` TL.null)
118 $ TL.split isSpace t
119 toBlock [] = mempty
120 toBlock xs@(x:_)
121 | TL.null x = VSpace $ genericLength xs
122 | otherwise = mconcat . map (Line . mconcat . splitWords) $ xs
123 toLine [] = mempty
124 toLine xs@(x:_)
125 | TL.null x = HSpace $ genericLength xs
126 | otherwise = mconcat . map Word $ xs
127 list :: b -> (a -> [a] -> b) -> [a] -> b
128 list c _ [] = c
129 list _ f (x:xs) = f x xs
130
131cotext :: Block -> Text
132-- ^ inverse of
133-- @
134-- either id Line . `text`
135-- @
136cotext (VSpace n) = TL.pack . genericReplicate (n - 1) $ '\n'
137cotext (NewlSep xs) = TL.intercalate "\n" . map cotext . toList $ xs
138cotext (Line x) = cotext' x
139 where
140 cotext' (Word x) = x
141 cotext' (HSpace n) = TL.pack . genericReplicate (n - 1) $ ' '
142 cotext' (SpaceSep xs) = TL.intercalate " " . map cotext' . toList $ xs
143
144prop_text :: Property
145-- ^ prop> cotext . either id Line . `text` $ x = x
146--
147-- Where 'x' is restricted to those `TL.Text` which do not contain whitespace besides ' ' and '\n'.
148prop_text = forAll (TL.map normSpace <$> arbitrary) $ \x -> (cotext . either id Line . text $ x) == x
149 where
150 normSpace c
151 | c == '\n' = '\n'
152 | isSpace c = ' ' -- We have to do this because all whitespace gets interpreted as width 1
153 | otherwise = c
154
155-- | We don't test 'Raw' 'Chunk's
156instance Arbitrary Chunk where
157 shrink = genericShrink
158 arbitrary = Cooked <$> arbitrary
159
160instance Arbitrary Block where
161 shrink = genericShrink
162 arbitrary = oneof [ Line <$> arbitrary
163 , VSpace . getNonNegative <$> arbitrary
164 , NewlSep <$> arbitrary
165 ]
166
167instance Arbitrary Line where
168 shrink = genericShrink
169 arbitrary = oneof [ Word . TL.filter (not . isSpace) <$> arbitrary -- 'isSpace '\n' == True'
170 , HSpace . getNonNegative <$> arbitrary
171 , SpaceSep <$> arbitrary
172 ]