1 {-# LANGUAGE OverloadedStrings #-}
2 {-# LANGUAGE UndecidableInstances #-}
3 module Symantic.Document.Plain where
6 import Data.Char (Char)
7 import Data.Eq (Eq(..))
8 import Data.Foldable (foldr)
9 import Data.Function (($), (.), id)
10 import Data.Functor ((<$>))
11 import Data.Maybe (Maybe(..))
12 import Data.Monoid (Monoid(..))
13 import Data.Ord (Ord(..), Ordering(..))
14 import Data.Semigroup (Semigroup(..))
15 import Data.String (String, IsString(..))
16 import Data.Text (Text)
17 import GHC.Natural (minusNatural,quotRemNatural)
18 import Numeric.Natural (Natural)
19 import Prelude (fromIntegral, Num(..))
20 import System.Console.ANSI
21 import Text.Show (Show(..), showString, showParen)
22 import qualified Data.List as List
23 import qualified Data.Text.Lazy as TL
25 import Symantic.Document.API
28 -- | Church encoded for performance concerns.
29 -- Kind like 'ParsecT' in @megaparsec@ but a little bit different
30 -- due to the use of 'PlainFit' for implementing 'breakingSpace' correctly
31 -- when in the left hand side of ('<>').
32 -- Prepending is done using continuation, like in a difference list.
33 newtype Plain d = Plain
36 {-curr-}PlainState d ->
37 {-ok-}( ({-prepend-}(d->d), {-new-}PlainState d) -> PlainFit d) ->
39 -- NOTE: equivalent to:
40 -- ReaderT PlainInh (StateT (PlainState d) (Cont (PlainFit d))) (d->d)
43 runPlain :: Monoid d => Plain d -> d
48 {-k-}(\(px,_sx) fits _overflow ->
49 -- NOTE: if px fits, then appending mempty fits
54 -- ** Type 'PlainState'
55 data PlainState d = PlainState
56 { plainState_buffer :: ![PlainChunk d]
57 , plainState_bufferStart :: !Column
58 , plainState_bufferWidth :: !Width
59 , plainState_removableIndent :: !Indent
60 -- ^ The amount of 'Indent' added by 'breakspace'
61 -- that can be removed by breaking the 'space' into a 'newline'.
64 defPlainState :: PlainState d
65 defPlainState = PlainState
66 { plainState_buffer = mempty
67 , plainState_bufferStart = 0
68 , plainState_bufferWidth = 0
69 , plainState_removableIndent = 0
73 data PlainInh = PlainInh
74 { plainInh_width :: !(Maybe Column)
75 , plainInh_justify :: !Bool
76 , plainInh_indent :: !Width
79 defPlainInh :: PlainInh
80 defPlainInh = PlainInh
81 { plainInh_width = Nothing
82 , plainInh_justify = False
87 -- | Double continuation to qualify the returned document
88 -- as fitting or overflowing the given 'plainInh_width'.
89 -- It's like @('Bool',d)@ in a normal style
90 -- (a non continuation-passing-style).
91 type PlainFit d = {-fits-}(d -> d) ->
92 {-overflow-}(d -> d) ->
95 -- ** Type 'PlainChunk'
97 = PlainChunk_Ignored d
98 -- ^ Ignored by the justification but kept in place.
99 -- Used for instance to put ANSI sequences.
100 | PlainChunk_Word (Word d)
101 | PlainChunk_Spaces Width
102 -- ^ 'spaces' preserved to be interleaved
103 -- correctly with 'PlainChunk_Ignored'.
104 instance Show d => Show (PlainChunk d) where
108 PlainChunk_Ignored d ->
111 PlainChunk_Word (Word d) ->
114 PlainChunk_Spaces s ->
117 instance Lengthable d => Lengthable (PlainChunk d) where
119 PlainChunk_Ignored{} -> 0
120 PlainChunk_Word d -> length d
121 PlainChunk_Spaces s -> s
123 PlainChunk_Ignored{} -> True
124 PlainChunk_Word d -> nullLength d
125 PlainChunk_Spaces s -> s == 0
126 instance From [SGR] d => From [SGR] (PlainChunk d) where
127 from sgr = PlainChunk_Ignored (from sgr)
129 runPlainChunk :: Spaceable d => PlainChunk d -> d
130 runPlainChunk = \case
131 PlainChunk_Ignored d -> d
132 PlainChunk_Word (Word d) -> d
133 PlainChunk_Spaces s -> spaces s
135 instance Semigroup d => Semigroup (Plain d) where
136 Plain x <> Plain y = Plain $ \inh st k ->
137 x inh st $ \(px,sx) ->
138 y inh sx $ \(py,sy) ->
140 instance Monoid d => Monoid (Plain d) where
141 mempty = Plain $ \_inh st k -> k (id,st)
143 instance (Spaceable d) => Spaceable (Plain d) where
144 newline = Plain $ \inh st k ->
146 (if plainInh_justify inh then joinLine inh st else mempty) <>
147 newline<>spaces (plainInh_indent inh)<>next
149 { plainState_bufferStart = plainInh_indent inh
150 , plainState_bufferWidth = 0
151 , plainState_buffer = mempty
155 spaces n = Plain $ \inh st@PlainState{..} k fits overflow ->
156 let newWidth = plainState_bufferStart + plainState_bufferWidth + n in
157 if plainInh_justify inh
160 case plainState_buffer of
161 PlainChunk_Spaces s:bs -> st
162 { plainState_buffer = PlainChunk_Spaces (s+n):bs
165 { plainState_buffer = PlainChunk_Spaces n:plainState_buffer
166 , plainState_bufferWidth = plainState_bufferWidth + 1
169 case plainInh_width inh of
170 Just width | width < newWidth ->
171 overflow $ k (id{-(d<>)-}, newState) fits overflow
172 _ -> k (id{-(d<>)-}, newState) fits overflow
175 { plainState_bufferWidth = plainState_bufferWidth + n
177 case plainInh_width inh of
178 Just width | width < newWidth ->
179 overflow $ k ((spaces n <>), newState) fits fits
180 _ -> k ((spaces n <>), newState) fits overflow
181 instance (From (Word s) d, Semigroup d, Lengthable s) => From (Word s) (Plain d) where
182 from s = Plain $ \inh st@PlainState{..} k fits overflow ->
183 let wordLen = length s in
185 then k (id,st) fits overflow
187 let newBufferWidth = plainState_bufferWidth + wordLen in
188 let newWidth = plainState_bufferStart + newBufferWidth in
189 if plainInh_justify inh
192 { plainState_buffer =
193 PlainChunk_Word (Word (from s)) :
195 , plainState_bufferWidth = newBufferWidth
197 case plainInh_width inh of
198 Just width | width < newWidth ->
199 overflow $ k (id, newState) fits overflow
200 _ -> k (id, newState) fits overflow
203 { plainState_bufferWidth = newBufferWidth
205 case plainInh_width inh of
206 Just width | width < newWidth ->
207 overflow $ k ((from s <>), newState) fits fits
208 _ -> k ((from s <>), newState) fits overflow
209 instance (From (Word s) d, Lengthable s, Spaceable d, Splitable s) =>
210 From (Line s) (Plain d) where
213 List.intersperse breakspace .
217 instance Spaceable d => Indentable (Plain d) where
218 align p = Plain $ \inh st ->
219 let currInd = plainState_bufferStart st + plainState_bufferWidth st in
220 unPlain p inh{plainInh_indent=currInd} st
221 incrIndent i p = Plain $ \inh ->
222 unPlain p inh{plainInh_indent = plainInh_indent inh + i}
223 setIndent i p = Plain $ \inh ->
224 unPlain p inh{plainInh_indent=i}
225 fill m p = Plain $ \inh0 st0 ->
226 let col0 = plainState_bufferStart st0 + plainState_bufferWidth st0 in
227 let p1 = Plain $ \inh1 st1 ->
228 let col1 = plainState_bufferStart st1 + plainState_bufferWidth st1 in
229 let w | col0 <= col1 = col1`minusNatural`col0
230 | otherwise = col0`minusNatural`col1 in
233 then spaces (m`minusNatural`w)
237 unPlain (p <> p1) inh0 st0
238 breakfill m p = Plain $ \inh0 st0 ->
239 let col0 = plainState_bufferStart st0 + plainState_bufferWidth st0 in
240 let p1 = Plain $ \inh1 st1 ->
241 let col1 = plainState_bufferStart st1 + plainState_bufferWidth st1 in
242 let w | col0 <= col1 = col1`minusNatural`col0
243 | otherwise = col0`minusNatural`col1 in
246 LT -> spaces (m`minusNatural`w)
248 GT -> setIndent (col0 + m) newline)
251 unPlain (p <> p1) inh0 st0
252 instance Spaceable d => Justifiable (Plain d) where
253 justify p = (<> flushLastLine) $ Plain $ \inh ->
254 unPlain p inh{plainInh_justify=True}
256 flushLastLine :: Plain d
257 flushLastLine = Plain $ \_inh st@PlainState{..} ok ->
259 ( (joinPlainLine (collapseSpaces <$> List.reverse plainState_buffer) <>)
261 { plainState_bufferStart = plainState_bufferStart + plainState_bufferWidth
262 , plainState_bufferWidth = 0
263 , plainState_buffer = mempty
266 collapseSpaces = \case
267 PlainChunk_Spaces s -> PlainChunk_Spaces (if s > 0 then 1 else 0)
269 instance (Spaceable d) => Wrappable (Plain d) where
270 setWidth w p = Plain $ \inh ->
271 unPlain p inh{plainInh_width=w}
272 breakpoint = Plain $ \inh st k fits overflow ->
273 let newlineInd = plainInh_indent inh in
277 { plainState_removableIndent = newlineInd
282 unPlain newline inh st k
285 if plainState_removableIndent st < newlineInd
290 breakspace = Plain $ \inh st k fits overflow ->
291 let newlineInd = plainInh_indent inh in
293 ( if plainInh_justify inh then id else (space <>)
295 { plainState_buffer =
296 case plainState_buffer st of
297 PlainChunk_Spaces s:bs -> PlainChunk_Spaces (s+1):bs
298 bs -> PlainChunk_Spaces 1:bs
299 , plainState_bufferWidth = plainState_bufferWidth st + 1
300 , plainState_removableIndent = newlineInd
305 unPlain newline inh st k
308 if plainState_removableIndent st < newlineInd
313 breakalt x y = Plain $ \inh st k fits overflow ->
314 unPlain x inh st k fits
316 unPlain y inh st k fits overflow
319 instance (From (Word String) d, Spaceable d) =>
320 From String (Plain d) where
323 List.intersperse newline .
326 instance (From (Word String) d, Spaceable d) =>
327 IsString (Plain d) where
330 instance (From (Word Text) d, Spaceable d) =>
331 From Text (Plain d) where
334 List.intersperse newline .
337 instance (From (Word TL.Text) d, Spaceable d) =>
338 From TL.Text (Plain d) where
341 List.intersperse newline .
345 instance (From (Word Char) d, Spaceable d) =>
346 From Char (Plain d) where
347 from ' ' = breakspace
349 from c = from (Word c)
352 instance (From [SGR] d, Semigroup d) => From [SGR] (Plain d) where
353 from sgr = Plain $ \inh st k ->
354 if plainInh_justify inh
355 then k (id, st {plainState_buffer = PlainChunk_Ignored (from sgr) : plainState_buffer st})
356 else k ((from sgr <>), st)
360 PlainInh -> PlainState d -> d
361 joinLine PlainInh{..} PlainState{..} =
362 case plainInh_width of
363 Nothing -> joinPlainLine $ List.reverse plainState_buffer
365 if width < plainState_bufferStart
366 || width < plainInh_indent
367 then joinPlainLine $ List.reverse plainState_buffer
370 foldr (\c acc -> acc + case c of
371 PlainChunk_Word{} -> 1
372 _ -> 0) 0 plainState_buffer in
374 -- NOTE: compress all separated spaces into a single one.
378 let spaceCount = foldr
381 PlainChunk_Ignored{} -> 0
382 PlainChunk_Word{} -> 0
383 PlainChunk_Spaces s -> s)
384 0 plainState_buffer in
385 (plainState_bufferWidth`minusNatural`spaceCount) +
386 (wordCount`minusNatural`1) in
390 (width`minusNatural`plainState_bufferStart)
391 (width`minusNatural`plainInh_indent) in
392 unLine $ padPlainLineInits adjustedWidth
393 (bufferWidth,wordCount,List.reverse plainState_buffer)
395 -- | @('justifyPadding' a b)@ returns the padding lengths
396 -- to reach @(a)@ in @(b)@ pads,
397 -- using the formula: @(a '==' m'*'(q '+' q'+'1) '+' ('r'-'m)'*'(q'+'1) '+' (b'-'r'-'m)'*'q)@
398 -- where @(q+1)@ and @(q)@ are the two padding lengths used and @(m = min (b-r) r)@.
400 -- A simple implementation of 'justifyPadding' could be:
402 -- 'justifyPadding' a b =
403 -- 'join' ('List.replicate' m [q,q'+'1])
404 -- <> ('List.replicate' (r'-'m) (q'+'1)
405 -- <> ('List.replicate' ((b'-'r)'-'m) q
407 -- (q,r) = a`divMod`b
410 justifyPadding :: Natural -> Natural -> [Natural]
411 justifyPadding a b = go r (b-r) -- NOTE: r >= 0 && b-r >= 0 due to 'divMod'
413 (q,r) = a`quotRemNatural`b
415 go 0 bmr = List.replicate (fromIntegral bmr) q -- when min (b-r) r == b-r
416 go rr 0 = List.replicate (fromIntegral rr) (q+1) -- when min (b-r) r == r
417 go rr bmr = q:(q+1) : go (rr`minusNatural`1) (bmr`minusNatural`1)
421 Width -> (Natural, Natural, [PlainChunk d]) -> Line d
422 padPlainLineInits width (lineLen,wordCount,line) = Line $
424 -- The gathered line reached or overreached the width,
425 -- hence no padding id needed.
427 -- The case width <= lineLen && wordCount == 1
428 -- can happen if first word's length is < width
429 -- but second word's len is >= width.
430 then joinPlainLine line
432 -- Share the missing spaces as evenly as possible
433 -- between the words of the line.
434 padPlainLine line $ justifyPadding (width-lineLen) (wordCount-1)
436 joinPlainLine :: Monoid d => Spaceable d => [PlainChunk d] -> d
437 joinPlainLine = mconcat . (runPlainChunk <$>)
439 padPlainLine :: Spaceable d => [PlainChunk d] -> [Width] -> d
442 go (w:ws) lls@(l:ls) =
444 PlainChunk_Spaces _s -> spaces (fromIntegral (l+1)) <> go ws ls
445 _ -> runPlainChunk w <> go ws lls
446 go (w:ws) [] = runPlainChunk w <> go ws []