1 {-# LANGUAGE OverloadedStrings #-}
2 {-# LANGUAGE NoMonomorphismRestriction #-}
3 {-# LANGUAGE UndecidableInstances #-}
4 module Symantic.Document.Plain where
6 import Control.Monad (Monad(..))
8 import Data.Char (Char)
9 import Data.Eq (Eq(..))
10 import Data.Function (($), (.), id)
11 import Data.Functor ((<$>))
12 import Data.Maybe (Maybe(..))
13 import Data.Monoid (Monoid(..))
14 import Data.Ord (Ord(..), Ordering(..))
15 import Data.Semigroup (Semigroup(..))
16 import Data.String (String, IsString(..))
17 import Data.Text (Text)
18 import Data.Tuple (snd)
19 import GHC.Natural (minusNatural,minusNaturalMaybe,quotRemNatural)
20 import Numeric.Natural (Natural)
21 import Prelude (fromIntegral, Num(..), pred)
22 import System.Console.ANSI
23 import Text.Show (Show(..), showString, showParen)
24 import qualified Data.Foldable as Fold
25 import qualified Data.List as List
26 import qualified Data.Text.Lazy as TL
28 import Symantic.Document.API
31 -- | Church encoded for performance concerns.
32 -- Kind like 'ParsecT' in @megaparsec@ but a little bit different
33 -- due to the use of 'PlainFit' for implementing 'breakingSpace' correctly
34 -- when in the left hand side of ('<>').
35 -- Prepending is done using continuation, like in a difference list.
36 newtype Plain d = Plain
39 {-curr-}PlainState d ->
40 {-ok-}( ({-prepend-}(d->d), {-new-}PlainState d) -> PlainFit d) ->
42 -- NOTE: equivalent to:
43 -- ReaderT PlainInh (StateT (PlainState d) (Cont (PlainFit d))) (d->d)
45 instance (Show d, Spaceable d) => Show (Plain d) where
46 show = show . runPlain
48 runPlain :: Spaceable d => Plain d -> d
53 {-k-}(\(px,_sx) fits _overflow ->
54 -- NOTE: if px fits, then appending mempty fits
59 -- ** Type 'PlainState'
60 data PlainState d = PlainState
61 { plainState_buffer :: ![PlainChunk d]
62 , plainState_bufferStart :: !Column
63 -- ^ The 'Column' from which the 'plainState_buffer'
65 , plainState_bufferWidth :: !Width
66 -- ^ The 'Width' of the 'plainState_buffer' so far.
67 , plainState_breakIndent :: !Indent
68 -- ^ The amount of 'Indent' added by 'breakspace'
69 -- that can be reached by breaking the 'space'
70 -- into a 'newlineJustifyingPlain'.
73 defPlainState :: PlainState d
74 defPlainState = PlainState
75 { plainState_buffer = mempty
76 , plainState_bufferStart = 0
77 , plainState_bufferWidth = 0
78 , plainState_breakIndent = 0
82 data PlainInh d = PlainInh
83 { plainInh_width :: !(Maybe Column)
84 , plainInh_justify :: !Bool
85 , plainInh_indent :: !Indent
86 , plainInh_indenting :: !(Plain d)
89 defPlainInh :: Spaceable d => PlainInh d
90 defPlainInh = PlainInh
91 { plainInh_width = Nothing
92 , plainInh_justify = False
94 , plainInh_indenting = mempty
98 -- | Double continuation to qualify the returned document
99 -- as fitting or overflowing the given 'plainInh_width'.
100 -- It's like @('Bool',d)@ in a normal style
101 -- (a non continuation-passing-style).
102 type PlainFit d = {-fits-}(d -> d) ->
103 {-overflow-}(d -> d) ->
106 -- ** Type 'PlainChunk'
108 = PlainChunk_Ignored !d
109 -- ^ Ignored by the justification but kept in place.
110 -- Used for instance to put ANSI sequences.
111 | PlainChunk_Word !(Word d)
112 | PlainChunk_Spaces !Width
113 -- ^ 'spaces' preserved to be interleaved
114 -- correctly with 'PlainChunk_Ignored'.
115 instance Show d => Show (PlainChunk d) where
119 PlainChunk_Ignored d ->
122 PlainChunk_Word (Word d) ->
125 PlainChunk_Spaces s ->
128 instance Lengthable d => Lengthable (PlainChunk d) where
130 PlainChunk_Ignored{} -> 0
131 PlainChunk_Word d -> width d
132 PlainChunk_Spaces s -> s
134 PlainChunk_Ignored{} -> True
135 PlainChunk_Word d -> nullWidth d
136 PlainChunk_Spaces s -> s == 0
137 instance From [SGR] d => From [SGR] (PlainChunk d) where
138 from sgr = PlainChunk_Ignored (from sgr)
140 runPlainChunk :: Spaceable d => PlainChunk d -> d
141 runPlainChunk = \case
142 PlainChunk_Ignored d -> d
143 PlainChunk_Word (Word d) -> d
144 PlainChunk_Spaces s -> spaces s
146 instance Semigroup d => Semigroup (Plain d) where
147 Plain x <> Plain y = Plain $ \inh st k ->
148 x inh st $ \(px,sx) ->
149 y inh sx $ \(py,sy) ->
151 instance Monoid d => Monoid (Plain d) where
152 mempty = Plain $ \_inh st k -> k (id,st)
154 instance Spaceable d => Spaceable (Plain d) where
155 -- | The default 'newline' does not justify 'plainState_buffer',
156 -- for that use 'newlineJustifyingPlain'.
157 newline = Plain $ \inh st ->
161 <> propagatePlain (plainState_breakIndent st)
165 indentPlain = Plain $ \inh ->
167 (plainInh_indenting inh)
168 inh{plainInh_justify=False}
169 newlinePlain = Plain $ \inh st k ->
171 (if plainInh_justify inh
172 then joinLinePlainChunk $ List.reverse $ plainState_buffer st
176 { plainState_bufferStart = 0
177 , plainState_bufferWidth = 0
178 , plainState_buffer = mempty
180 propagatePlain breakIndent = Plain $ \inh st1 k fits overflow ->
184 -- NOTE: the text after this newline overflows,
185 -- so propagate the overflow before this 'newline',
186 -- if and only if there is a 'breakspace' before this 'newline'
187 -- whose replacement by a 'newline' indents to a lower indent
188 -- than this 'newline''s indent.
189 -- Otherwise there is no point in propagating the overflow.
190 if breakIndent < plainInh_indent inh
195 spaces n = Plain $ \inh st@PlainState{..} k fits overflow ->
196 let newWidth = plainState_bufferStart + plainState_bufferWidth + n in
197 if plainInh_justify inh
200 { plainState_buffer =
201 case plainState_buffer of
202 PlainChunk_Spaces s:buf -> PlainChunk_Spaces (s+n):buf
203 buf -> PlainChunk_Spaces n:buf
204 , plainState_bufferWidth = plainState_bufferWidth + n
206 case plainInh_width inh of
207 Just maxWidth | maxWidth < newWidth ->
208 overflow $ k (id{-(d<>)-}, newState) fits overflow
209 _ -> k (id{-(d<>)-}, newState) fits overflow
212 { plainState_bufferWidth = plainState_bufferWidth + n
214 case plainInh_width inh of
215 Just maxWidth | maxWidth < newWidth ->
216 overflow $ k ((spaces n <>), newState) fits fits
217 _ -> k ((spaces n <>), newState) fits overflow
218 instance (From (Word s) d, Semigroup d, Lengthable s) =>
219 From (Word s) (Plain d) where
220 from s = Plain $ \inh st@PlainState{..} k fits overflow ->
221 let wordWidth = width s in
223 then k (id,st) fits overflow
225 let newBufferWidth = plainState_bufferWidth + wordWidth in
226 let newWidth = plainState_bufferStart + newBufferWidth in
227 if plainInh_justify inh
230 { plainState_buffer =
231 PlainChunk_Word (Word (from s)) :
233 , plainState_bufferWidth = newBufferWidth
235 case plainInh_width inh of
236 Just maxWidth | maxWidth < newWidth ->
237 overflow $ k (id, newState) fits overflow
238 _ -> k (id, newState) fits overflow
241 { plainState_bufferWidth = newBufferWidth
243 case plainInh_width inh of
244 Just maxWidth | maxWidth < newWidth ->
245 overflow $ k ((from s <>), newState) fits fits
246 _ -> k ((from s <>), newState) fits overflow
247 instance (From (Word s) d, Lengthable s, Spaceable d, Splitable s) =>
248 From (Line s) (Plain d) where
251 List.intersperse breakspace .
255 instance Spaceable d => Indentable (Plain d) where
256 align p = (flushlinePlain <>) $ Plain $ \inh st ->
257 let col = plainState_bufferStart st + plainState_bufferWidth st in
259 { plainInh_indent = col
260 , plainInh_indenting =
261 if plainInh_indent inh <= col
263 plainInh_indenting inh <>
264 spaces (col`minusNatural`plainInh_indent inh)
267 setIndent d i p = Plain $ \inh ->
269 { plainInh_indent = i
270 , plainInh_indenting = d
272 incrIndent d i p = Plain $ \inh ->
274 { plainInh_indent = plainInh_indent inh + i
275 , plainInh_indenting = plainInh_indenting inh <> d
278 fill m p = Plain $ \inh0 st0 ->
279 let maxCol = plainState_bufferStart st0 + plainState_bufferWidth st0 + m in
280 let p1 = Plain $ \inh1 st1 ->
281 let col = plainState_bufferStart st1 + plainState_bufferWidth st1 in
284 then spaces (maxCol`minusNatural`col)
288 unPlain (p <> p1) inh0 st0
289 fillOrBreak m p = Plain $ \inh0 st0 ->
290 let maxCol = plainState_bufferStart st0 + plainState_bufferWidth st0 + m in
291 let p1 = Plain $ \inh1 st1 ->
292 let col = plainState_bufferStart st1 + plainState_bufferWidth st1 in
294 (case col`compare`maxCol of
295 LT -> spaces (maxCol`minusNatural`col)
297 GT -> incrIndent (spaces m) m newline
300 unPlain (p <> p1) inh0 st0
301 instance (Spaceable d, From (Word Char) d, From (Word String) d) => Listable (Plain d) where
305 from (Word '-')<>space<>flushlinePlain<>align d<>flushlinePlain
310 (pred i, (from i<>from (Word '.')<>space<>flushlinePlain<>align d<>flushlinePlain) : acc)
311 ) (Fold.length ds, []) ds
312 instance Spaceable d => Justifiable (Plain d) where
313 justify p = (\x -> flushlinePlain <> x <> flushlinePlain) $ Plain $ \inh ->
314 unPlain p inh{plainInh_justify=True}
316 -- | Commit 'plainState_buffer' upto there, so that it won't be justified.
317 flushlinePlain :: Spaceable d => Plain d
318 flushlinePlain = Plain $ \_inh st k ->
319 k( (joinLinePlainChunk (collapsePlainChunkSpaces <$> List.reverse (plainState_buffer st)) <>)
321 { plainState_bufferStart = plainState_bufferStart st + plainState_bufferWidth st
322 , plainState_bufferWidth = 0
323 , plainState_buffer = mempty
327 collapsePlainChunkSpaces :: PlainChunk d -> PlainChunk d
328 collapsePlainChunkSpaces = \case
329 PlainChunk_Spaces s -> PlainChunk_Spaces (if s > 0 then 1 else 0)
332 instance Spaceable d => Wrappable (Plain d) where
333 setWidth w p = Plain $ \inh ->
334 unPlain p inh{plainInh_width=w}
335 breakpoint = Plain $ \inh st k fits overflow ->
336 k(id, st {plainState_breakIndent = plainInh_indent inh})
338 {-overflow-}(\_r -> unPlain newlineJustifyingPlain inh st k fits overflow)
339 breakspace = Plain $ \inh st k fits overflow ->
340 k( if plainInh_justify inh then id else (space <>)
342 { plainState_buffer =
343 if plainInh_justify inh
344 then case plainState_buffer st of
345 PlainChunk_Spaces s:bs -> PlainChunk_Spaces (s+1):bs
346 bs -> PlainChunk_Spaces 1:bs
347 else plainState_buffer st
348 , plainState_bufferWidth = plainState_bufferWidth st + 1
349 , plainState_breakIndent = plainInh_indent inh
353 {-overflow-}(\_r -> unPlain newlineJustifyingPlain inh st k fits overflow)
354 breakalt x y = Plain $ \inh st k fits overflow ->
355 -- NOTE: breakalt must be y if and only if x does not fit,
356 -- hence the use of dummyK to limit the test
357 -- to overflows raised within x, and drop those raised after x.
358 unPlain x inh st dummyK
359 {-fits-} (\_r -> unPlain x inh st k fits overflow)
360 {-overflow-}(\_r -> unPlain y inh st k fits overflow)
362 dummyK (px,_sx) fits _overflow =
363 -- NOTE: if px fits, then appending mempty fits
365 endline = Plain $ \inh st k fits _overflow ->
366 let col = plainState_bufferStart st + plainState_bufferWidth st in
367 case plainInh_width inh >>= (`minusNaturalMaybe` col) of
368 Nothing -> k (id, st) fits fits
371 { plainState_bufferWidth = plainState_bufferWidth st + w
373 k (id,newState) fits fits
375 -- | Like 'newline', but justify 'plainState_buffer' before.
376 newlineJustifyingPlain :: Spaceable d => Plain d
377 newlineJustifyingPlain = Plain $ \inh st ->
381 <> propagatePlain (plainState_breakIndent st)
385 indentPlain = Plain $ \inh ->
387 (plainInh_indenting inh)
388 inh{plainInh_justify=False}
389 newlinePlain = Plain $ \inh st k ->
391 (if plainInh_justify inh
392 then justifyLinePlain inh st
396 { plainState_bufferStart = 0
397 , plainState_bufferWidth = 0
398 , plainState_buffer = mempty
400 propagatePlain breakIndent = Plain $ \inh st1 k fits overflow ->
404 -- NOTE: the text after this newline overflows,
405 -- so propagate the overflow before this 'newline',
406 -- if and only if there is a 'breakspace' before this 'newline'
407 -- whose replacement by a 'newline' indents to a lower indent
408 -- than this 'newline''s indent.
409 -- Otherwise there is no point in propagating the overflow.
410 if breakIndent < plainInh_indent inh
416 instance (From (Word String) d, Spaceable d) =>
417 From String (Plain d) where
420 List.intersperse newline .
423 instance (From (Word String) d, Spaceable d) =>
424 IsString (Plain d) where
427 instance (From (Word Text) d, Spaceable d) =>
428 From Text (Plain d) where
431 List.intersperse newline .
434 instance (From (Word TL.Text) d, Spaceable d) =>
435 From TL.Text (Plain d) where
438 List.intersperse newline .
442 instance (From (Word Char) d, Spaceable d) =>
443 From Char (Plain d) where
444 from ' ' = breakspace
446 from c = from (Word c)
448 instance (From [SGR] d, Semigroup d) => From [SGR] (Plain d) where
449 from sgr = Plain $ \inh st k ->
450 if plainInh_justify inh
451 then k (id, st {plainState_buffer = PlainChunk_Ignored (from sgr) : plainState_buffer st})
452 else k ((from sgr <>), st)
456 PlainInh d -> PlainState d -> d
457 justifyLinePlain inh PlainState{..} =
458 case plainInh_width inh of
459 Nothing -> joinLinePlainChunk $ List.reverse plainState_buffer
461 if maxWidth < plainState_bufferStart
462 || maxWidth < plainInh_indent inh
463 then joinLinePlainChunk $ List.reverse plainState_buffer
465 let superfluousSpaces = Fold.foldr
468 PlainChunk_Ignored{} -> 0
469 PlainChunk_Word{} -> 0
470 PlainChunk_Spaces s -> s`minusNatural`(min 1 s))
471 0 plainState_buffer in
473 -- NOTE: cap the spaces at 1,
474 -- to let justifyWidth decide where to add spaces.
475 plainState_bufferWidth`minusNatural`superfluousSpaces in
477 -- NOTE: when minBufferWidth is not breakable,
478 -- the width of justification can be wider than
479 -- what remains to reach maxWidth.
481 maxWidth`minusNatural`plainState_bufferStart
483 let wordCount = countWordsPlain plainState_buffer in
484 unLine $ padLinePlainChunkInits justifyWidth $
485 (minBufferWidth,wordCount,List.reverse plainState_buffer)
487 -- | @('countWordsPlain' ps)@ returns the number of words in @(ps)@
488 -- clearly separated by spaces.
489 countWordsPlain :: [PlainChunk d] -> Natural
490 countWordsPlain = go False 0
492 go inWord acc = \case
494 PlainChunk_Word{}:xs ->
496 then go inWord acc xs
497 else go True (acc+1) xs
498 PlainChunk_Spaces s:xs
499 | s == 0 -> go inWord acc xs
500 | otherwise -> go False acc xs
501 PlainChunk_Ignored{}:xs -> go inWord acc xs
503 -- | @('justifyPadding' a b)@ returns the padding lengths
504 -- to reach @(a)@ in @(b)@ pads,
505 -- using the formula: @(a '==' m'*'(q '+' q'+'1) '+' ('r'-'m)'*'(q'+'1) '+' (b'-'r'-'m)'*'q)@
506 -- where @(q+1)@ and @(q)@ are the two padding lengths used and @(m = min (b-r) r)@.
508 -- A simple implementation of 'justifyPadding' could be:
510 -- 'justifyPadding' a b =
511 -- 'join' ('List.replicate' m [q,q'+'1])
512 -- <> ('List.replicate' (r'-'m) (q'+'1)
513 -- <> ('List.replicate' ((b'-'r)'-'m) q
515 -- (q,r) = a`divMod`b
518 justifyPadding :: Natural -> Natural -> [Natural]
519 justifyPadding a b = go r (b-r) -- NOTE: r >= 0 && b-r >= 0 due to 'divMod'
521 (q,r) = a`quotRemNatural`b
523 go 0 bmr = List.replicate (fromIntegral bmr) q -- when min (b-r) r == b-r
524 go rr 0 = List.replicate (fromIntegral rr) (q+1) -- when min (b-r) r == r
525 go rr bmr = q:(q+1) : go (rr`minusNatural`1) (bmr`minusNatural`1)
527 padLinePlainChunkInits ::
529 Width -> (Natural, Natural, [PlainChunk d]) -> Line d
530 padLinePlainChunkInits maxWidth (lineWidth,wordCount,line) = Line $
531 if maxWidth <= lineWidth
532 -- The gathered line reached or overreached the maxWidth,
533 -- hence no padding id needed.
535 -- The case maxWidth <= lineWidth && wordCount == 1
536 -- can happen if first word's length is < maxWidth
537 -- but second word's len is >= maxWidth.
538 then joinLinePlainChunk line
540 -- Share the missing spaces as evenly as possible
541 -- between the words of the line.
542 padLinePlainChunk line $ justifyPadding (maxWidth-lineWidth) (wordCount-1)
544 -- | Just concat 'PlainChunk's with no justification.
545 joinLinePlainChunk :: Monoid d => Spaceable d => [PlainChunk d] -> d
546 joinLinePlainChunk = mconcat . (runPlainChunk <$>)
548 -- | Interleave 'PlainChunk's with 'Width's from 'justifyPadding'.
549 padLinePlainChunk :: Spaceable d => [PlainChunk d] -> [Width] -> d
550 padLinePlainChunk = go
552 go (w:ws) lls@(l:ls) =
554 PlainChunk_Spaces _s -> spaces (fromIntegral (l+1)) <> go ws ls
555 _ -> runPlainChunk w <> go ws lls
556 go (w:ws) [] = runPlainChunk w <> go ws []