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 hiding (SGR)
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.Class
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)
87 , plainInh_sgr :: ![SGR]
90 defPlainInh :: Spaceable d => PlainInh d
91 defPlainInh = PlainInh
92 { plainInh_width = Nothing
93 , plainInh_justify = False
95 , plainInh_indenting = mempty
100 -- | Double continuation to qualify the returned document
101 -- as fitting or overflowing the given 'plainInh_width'.
102 -- It's like @('Bool',d)@ in a normal style
103 -- (a non continuation-passing-style).
106 {-overflow-}(d -> d) ->
109 -- ** Type 'PlainChunk'
111 = PlainChunk_Ignored !d
112 -- ^ Ignored by the justification but kept in place.
113 -- Used for instance to put ANSI sequences.
114 | PlainChunk_Word !(Word d)
115 | PlainChunk_Spaces !Width
116 -- ^ 'spaces' preserved to be interleaved
117 -- correctly with 'PlainChunk_Ignored'.
118 instance Show d => Show (PlainChunk d) where
122 PlainChunk_Ignored d ->
125 PlainChunk_Word (Word d) ->
128 PlainChunk_Spaces s ->
131 instance Lengthable d => Lengthable (PlainChunk d) where
133 PlainChunk_Ignored{} -> 0
134 PlainChunk_Word d -> width d
135 PlainChunk_Spaces s -> s
137 PlainChunk_Ignored{} -> True
138 PlainChunk_Word d -> nullWidth d
139 PlainChunk_Spaces s -> s == 0
140 instance From [SGR] d => From [SGR] (PlainChunk d) where
141 from sgr = PlainChunk_Ignored (from sgr)
143 runPlainChunk :: Spaceable d => PlainChunk d -> d
144 runPlainChunk = \case
145 PlainChunk_Ignored d -> d
146 PlainChunk_Word (Word d) -> d
147 PlainChunk_Spaces s -> spaces s
149 instance Semigroup d => Semigroup (Plain d) where
150 Plain x <> Plain y = Plain $ \inh st k ->
151 x inh st $ \(px,sx) ->
152 y inh sx $ \(py,sy) ->
154 instance Monoid d => Monoid (Plain d) where
155 mempty = Plain $ \_inh st k -> k (id,st)
157 instance Spaceable d => Spaceable (Plain d) where
158 -- | The default 'newline' does not justify 'plainState_buffer',
159 -- for that use 'newlineJustifyingPlain'.
160 newline = Plain $ \inh st ->
164 <> propagatePlain (plainState_breakIndent st)
168 indentPlain = Plain $ \inh ->
170 (plainInh_indenting inh)
171 inh{plainInh_justify=False}
172 newlinePlain = Plain $ \inh st k ->
174 (if plainInh_justify inh
175 then joinLinePlainChunk $ List.reverse $ plainState_buffer st
179 { plainState_bufferStart = 0
180 , plainState_bufferWidth = 0
181 , plainState_buffer = mempty
183 propagatePlain breakIndent = Plain $ \inh st1 k fits overflow ->
187 -- NOTE: the text after this newline overflows,
188 -- so propagate the overflow before this 'newline',
189 -- if and only if there is a 'breakspace' before this 'newline'
190 -- whose replacement by a 'newline' indents to a lower indent
191 -- than this 'newline''s indent.
192 -- Otherwise there is no point in propagating the overflow.
193 if breakIndent < plainInh_indent inh
198 spaces n = Plain $ \inh st@PlainState{..} k fits overflow ->
199 let newWidth = plainState_bufferStart + plainState_bufferWidth + n in
200 if plainInh_justify inh
203 { plainState_buffer =
204 case plainState_buffer of
205 PlainChunk_Spaces s:buf -> PlainChunk_Spaces (s+n):buf
206 buf -> PlainChunk_Spaces n:buf
207 , plainState_bufferWidth = plainState_bufferWidth + n
209 case plainInh_width inh of
210 Just maxWidth | maxWidth < newWidth ->
211 overflow $ k (id{-(d<>)-}, newState) fits overflow
212 _ -> k (id{-(d<>)-}, newState) fits overflow
215 { plainState_bufferWidth = plainState_bufferWidth + n
217 case plainInh_width inh of
218 Just maxWidth | maxWidth < newWidth ->
219 overflow $ k ((spaces n <>), newState) fits fits
220 _ -> k ((spaces n <>), newState) fits overflow
221 instance (From (Word s) d, Semigroup d, Lengthable s) =>
222 From (Word s) (Plain d) where
223 from s = Plain $ \inh st@PlainState{..} k fits overflow ->
224 let wordWidth = width s in
226 then k (id,st) fits overflow
228 let newBufferWidth = plainState_bufferWidth + wordWidth in
229 let newWidth = plainState_bufferStart + newBufferWidth in
230 if plainInh_justify inh
233 { plainState_buffer =
234 PlainChunk_Word (Word (from s)) :
236 , plainState_bufferWidth = newBufferWidth
238 case plainInh_width inh of
239 Just maxWidth | maxWidth < newWidth ->
240 overflow $ k (id, newState) fits overflow
241 _ -> k (id, newState) fits overflow
244 { plainState_bufferWidth = newBufferWidth
246 case plainInh_width inh of
247 Just maxWidth | maxWidth < newWidth ->
248 overflow $ k ((from s <>), newState) fits fits
249 _ -> k ((from s <>), newState) fits overflow
250 instance (From (Word s) d, Lengthable s, Spaceable d, Splitable s) =>
251 From (Line s) (Plain d) where
254 List.intersperse breakspace .
258 instance Spaceable d => Indentable (Plain d) where
259 align p = (flushlinePlain <>) $ Plain $ \inh st ->
260 let col = plainState_bufferStart st + plainState_bufferWidth st in
262 { plainInh_indent = col
263 , plainInh_indenting =
264 if plainInh_indent inh <= col
266 plainInh_indenting inh <>
267 spaces (col`minusNatural`plainInh_indent inh)
270 setIndent d i p = Plain $ \inh ->
272 { plainInh_indent = i
273 , plainInh_indenting = d
275 incrIndent d i p = Plain $ \inh ->
277 { plainInh_indent = plainInh_indent inh + i
278 , plainInh_indenting = plainInh_indenting inh <> d
281 fill m p = Plain $ \inh0 st0 ->
282 let maxCol = plainState_bufferStart st0 + plainState_bufferWidth st0 + m in
283 let p1 = Plain $ \inh1 st1 ->
284 let col = plainState_bufferStart st1 + plainState_bufferWidth st1 in
287 then spaces (maxCol`minusNatural`col)
291 unPlain (p <> p1) inh0 st0
292 fillOrBreak m p = Plain $ \inh0 st0 ->
293 let maxCol = plainState_bufferStart st0 + plainState_bufferWidth st0 + m in
294 let p1 = Plain $ \inh1 st1 ->
295 let col = plainState_bufferStart st1 + plainState_bufferWidth st1 in
297 (case col`compare`maxCol of
298 LT -> spaces (maxCol`minusNatural`col)
300 GT -> incrIndent (spaces m) m newline
303 unPlain (p <> p1) inh0 st0
304 instance (Spaceable d, From (Word Char) d, From (Word String) d) => Listable (Plain d) where
308 from (Word '-')<>space<>flushlinePlain<>align d{-<>flushlinePlain-}
313 (pred i, (from i<>from (Word '.')<>space<>flushlinePlain<>align d{-<>flushlinePlain-}) : acc)
314 ) (Fold.length ds, []) ds
315 instance Spaceable d => Justifiable (Plain d) where
316 justify p = (\x -> flushlinePlain <> x <> flushlinePlain) $ Plain $ \inh ->
317 unPlain p inh{plainInh_justify=True}
319 -- | Commit 'plainState_buffer' upto there, so that it won't be justified.
320 flushlinePlain :: Spaceable d => Plain d
321 flushlinePlain = Plain $ \_inh st k ->
322 k( (joinLinePlainChunk (collapsePlainChunkSpaces <$> List.reverse (plainState_buffer st)) <>)
324 { plainState_bufferStart = plainState_bufferStart st + plainState_bufferWidth st
325 , plainState_bufferWidth = 0
326 , plainState_buffer = mempty
330 collapsePlainChunkSpaces :: PlainChunk d -> PlainChunk d
331 collapsePlainChunkSpaces = \case
332 PlainChunk_Spaces s -> PlainChunk_Spaces (if s > 0 then 1 else 0)
335 instance Spaceable d => Wrappable (Plain d) where
336 setWidth w p = Plain $ \inh ->
337 unPlain p inh{plainInh_width=w}
338 breakpoint = Plain $ \inh st k fits overflow ->
339 k(id, st {plainState_breakIndent = plainInh_indent inh})
341 {-overflow-}(\_r -> unPlain newlineJustifyingPlain inh st k fits overflow)
342 breakspace = Plain $ \inh st k fits overflow ->
343 k( if plainInh_justify inh then id else (space <>)
345 { plainState_buffer =
346 if plainInh_justify inh
347 then case plainState_buffer st of
348 PlainChunk_Spaces s:bs -> PlainChunk_Spaces (s+1):bs
349 bs -> PlainChunk_Spaces 1:bs
350 else plainState_buffer st
351 , plainState_bufferWidth = plainState_bufferWidth st + 1
352 , plainState_breakIndent = plainInh_indent inh
356 {-overflow-}(\_r -> unPlain newlineJustifyingPlain inh st k fits overflow)
357 breakalt x y = Plain $ \inh st k fits overflow ->
358 -- NOTE: breakalt must be y if and only if x does not fit,
359 -- hence the use of dummyK to limit the test
360 -- to overflows raised within x, and drop those raised after x.
361 unPlain x inh st dummyK
362 {-fits-} (\_r -> unPlain x inh st k fits overflow)
363 {-overflow-}(\_r -> unPlain y inh st k fits overflow)
365 dummyK (px,_sx) fits _overflow =
366 -- NOTE: if px fits, then appending mempty fits
368 endline = Plain $ \inh st k fits _overflow ->
369 let col = plainState_bufferStart st + plainState_bufferWidth st in
370 case plainInh_width inh >>= (`minusNaturalMaybe` col) of
371 Nothing -> k (id, st) fits fits
374 { plainState_bufferWidth = plainState_bufferWidth st + w
376 k (id,newState) fits fits
378 -- | Like 'newline', but justify 'plainState_buffer' before.
379 newlineJustifyingPlain :: Spaceable d => Plain d
380 newlineJustifyingPlain = Plain $ \inh st ->
384 <> propagatePlain (plainState_breakIndent st)
388 indentPlain = Plain $ \inh ->
390 (plainInh_indenting inh)
391 inh{plainInh_justify=False}
392 newlinePlain = Plain $ \inh st k ->
394 (if plainInh_justify inh
395 then justifyLinePlain inh st
399 { plainState_bufferStart = 0
400 , plainState_bufferWidth = 0
401 , plainState_buffer = mempty
403 propagatePlain breakIndent = Plain $ \inh st1 k fits overflow ->
407 -- NOTE: the text after this newline overflows,
408 -- so propagate the overflow before this 'newline',
409 -- if and only if there is a 'breakspace' before this 'newline'
410 -- whose replacement by a 'newline' indents to a lower indent
411 -- than this 'newline''s indent.
412 -- Otherwise there is no point in propagating the overflow.
413 if breakIndent < plainInh_indent inh
419 instance (From (Word String) d, Spaceable d) =>
420 From String (Plain d) where
423 List.intersperse newline .
426 instance (From (Word String) d, Spaceable d) =>
427 IsString (Plain d) where
430 instance (From (Word Text) d, Spaceable d) =>
431 From Text (Plain d) where
434 List.intersperse newline .
437 instance (From (Word TL.Text) d, Spaceable d) =>
438 From TL.Text (Plain d) where
441 List.intersperse newline .
445 instance (From (Word Char) d, Spaceable d) =>
446 From Char (Plain d) where
447 from ' ' = breakspace
449 from c = from (Word c)
451 instance (From [SGR] d, Semigroup d) => From [SGR] (Plain d) where
452 from sgr = Plain $ \inh st k ->
453 if plainInh_justify inh
454 then k (id, st {plainState_buffer = PlainChunk_Ignored (from sgr) : plainState_buffer st})
455 else k ((from sgr <>), st)
460 PlainInh d -> PlainState d -> d
461 justifyLinePlain inh PlainState{..} =
462 case plainInh_width inh of
463 Nothing -> joinLinePlainChunk $ List.reverse plainState_buffer
465 if maxWidth < plainState_bufferStart
466 || maxWidth < plainInh_indent inh
467 then joinLinePlainChunk $ List.reverse plainState_buffer
469 let superfluousSpaces = Fold.foldr
472 PlainChunk_Ignored{} -> 0
473 PlainChunk_Word{} -> 0
474 PlainChunk_Spaces s -> s`minusNatural`(min 1 s))
475 0 plainState_buffer in
477 -- NOTE: cap the spaces at 1,
478 -- to let justifyWidth decide where to add spaces.
479 plainState_bufferWidth`minusNatural`superfluousSpaces in
481 -- NOTE: when minBufferWidth is not breakable,
482 -- the width of justification can be wider than
483 -- what remains to reach maxWidth.
485 maxWidth`minusNatural`plainState_bufferStart
487 let wordCount = countWordsPlain plainState_buffer in
488 unLine $ padLinePlainChunkInits justifyWidth $
489 (minBufferWidth,wordCount,List.reverse plainState_buffer)
491 -- | @('countWordsPlain' ps)@ returns the number of words in @(ps)@
492 -- clearly separated by spaces.
493 countWordsPlain :: [PlainChunk d] -> Natural
494 countWordsPlain = go False 0
496 go inWord acc = \case
498 PlainChunk_Word{}:xs ->
500 then go inWord acc xs
501 else go True (acc+1) xs
502 PlainChunk_Spaces s:xs
503 | s == 0 -> go inWord acc xs
504 | otherwise -> go False acc xs
505 PlainChunk_Ignored{}:xs -> go inWord acc xs
507 -- | @('justifyPadding' a b)@ returns the padding lengths
508 -- to reach @(a)@ in @(b)@ pads,
509 -- using the formula: @(a '==' m'*'(q '+' q'+'1) '+' ('r'-'m)'*'(q'+'1) '+' (b'-'r'-'m)'*'q)@
510 -- where @(q+1)@ and @(q)@ are the two padding lengths used and @(m = min (b-r) r)@.
512 -- A simple implementation of 'justifyPadding' could be:
514 -- 'justifyPadding' a b =
515 -- 'join' ('List.replicate' m [q,q'+'1])
516 -- <> ('List.replicate' (r'-'m) (q'+'1)
517 -- <> ('List.replicate' ((b'-'r)'-'m) q
519 -- (q,r) = a`divMod`b
522 justifyPadding :: Natural -> Natural -> [Natural]
523 justifyPadding a b = go r (b-r) -- NOTE: r >= 0 && b-r >= 0 due to 'divMod'
525 (q,r) = a`quotRemNatural`b
527 go 0 bmr = List.replicate (fromIntegral bmr) q -- when min (b-r) r == b-r
528 go rr 0 = List.replicate (fromIntegral rr) (q+1) -- when min (b-r) r == r
529 go rr bmr = q:(q+1) : go (rr`minusNatural`1) (bmr`minusNatural`1)
531 padLinePlainChunkInits ::
533 Width -> (Natural, Natural, [PlainChunk d]) -> Line d
534 padLinePlainChunkInits maxWidth (lineWidth,wordCount,line) = Line $
535 if maxWidth <= lineWidth
536 -- The gathered line reached or overreached the maxWidth,
537 -- hence no padding id needed.
539 -- The case maxWidth <= lineWidth && wordCount == 1
540 -- can happen if first word's length is < maxWidth
541 -- but second word's len is >= maxWidth.
542 then joinLinePlainChunk line
544 -- Share the missing spaces as evenly as possible
545 -- between the words of the line.
546 padLinePlainChunk line $ justifyPadding (maxWidth-lineWidth) (wordCount-1)
548 -- | Just concat 'PlainChunk's with no justification.
549 joinLinePlainChunk :: Monoid d => Spaceable d => [PlainChunk d] -> d
550 joinLinePlainChunk = mconcat . (runPlainChunk <$>)
552 -- | Interleave 'PlainChunk's with 'Width's from 'justifyPadding'.
553 padLinePlainChunk :: Spaceable d => [PlainChunk d] -> [Width] -> d
554 padLinePlainChunk = go
556 go (w:ws) lls@(l:ls) =
558 PlainChunk_Spaces _s -> spaces (fromIntegral (l+1)) <> go ws ls
559 _ -> runPlainChunk w <> go ws lls
560 go (w:ws) [] = runPlainChunk w <> go ws []
564 instance (Semigroup d, From [SGR] d) => Colorable16 (Plain d) where
565 reverse = plainSGR $ SetSwapForegroundBackground True
566 black = plainSGR $ SetColor Foreground Dull Black
567 red = plainSGR $ SetColor Foreground Dull Red
568 green = plainSGR $ SetColor Foreground Dull Green
569 yellow = plainSGR $ SetColor Foreground Dull Yellow
570 blue = plainSGR $ SetColor Foreground Dull Blue
571 magenta = plainSGR $ SetColor Foreground Dull Magenta
572 cyan = plainSGR $ SetColor Foreground Dull Cyan
573 white = plainSGR $ SetColor Foreground Dull White
574 blacker = plainSGR $ SetColor Foreground Vivid Black
575 redder = plainSGR $ SetColor Foreground Vivid Red
576 greener = plainSGR $ SetColor Foreground Vivid Green
577 yellower = plainSGR $ SetColor Foreground Vivid Yellow
578 bluer = plainSGR $ SetColor Foreground Vivid Blue
579 magentaer = plainSGR $ SetColor Foreground Vivid Magenta
580 cyaner = plainSGR $ SetColor Foreground Vivid Cyan
581 whiter = plainSGR $ SetColor Foreground Vivid White
582 onBlack = plainSGR $ SetColor Background Dull Black
583 onRed = plainSGR $ SetColor Background Dull Red
584 onGreen = plainSGR $ SetColor Background Dull Green
585 onYellow = plainSGR $ SetColor Background Dull Yellow
586 onBlue = plainSGR $ SetColor Background Dull Blue
587 onMagenta = plainSGR $ SetColor Background Dull Magenta
588 onCyan = plainSGR $ SetColor Background Dull Cyan
589 onWhite = plainSGR $ SetColor Background Dull White
590 onBlacker = plainSGR $ SetColor Background Vivid Black
591 onRedder = plainSGR $ SetColor Background Vivid Red
592 onGreener = plainSGR $ SetColor Background Vivid Green
593 onYellower = plainSGR $ SetColor Background Vivid Yellow
594 onBluer = plainSGR $ SetColor Background Vivid Blue
595 onMagentaer = plainSGR $ SetColor Background Vivid Magenta
596 onCyaner = plainSGR $ SetColor Background Vivid Cyan
597 onWhiter = plainSGR $ SetColor Background Vivid White
598 instance (Semigroup d, From [SGR] d) => Decorable (Plain d) where
599 bold = plainSGR $ SetConsoleIntensity BoldIntensity
600 underline = plainSGR $ SetUnderlining SingleUnderline
601 italic = plainSGR $ SetItalicized True
606 SGR -> Plain d -> Plain d
607 plainSGR newSGR p = before <> middle <> after
609 before = Plain $ \inh st k ->
610 let d = from [newSGR] in
611 if plainInh_justify inh
613 { plainState_buffer =
614 PlainChunk_Ignored d :
618 middle = Plain $ \inh ->
619 unPlain p inh{plainInh_sgr=newSGR:plainInh_sgr inh}
620 after = Plain $ \inh st k ->
621 let d = from $ Reset : List.reverse (plainInh_sgr inh) in
622 if plainInh_justify inh
624 { plainState_buffer =
625 PlainChunk_Ignored d :