]> Git — Sourcephile - haskell/symantic-document.git/blob - Symantic/Document/Plain.hs
api: add custom indenting text
[haskell/symantic-document.git] / Symantic / Document / Plain.hs
1 {-# LANGUAGE OverloadedStrings #-}
2 {-# LANGUAGE NoMonomorphismRestriction #-}
3 {-# LANGUAGE UndecidableInstances #-}
4 module Symantic.Document.Plain where
5
6 import Control.Monad (Monad(..))
7 import Data.Bool
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
27
28 import Symantic.Document.API
29
30 -- * Type 'Plain'
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
37 { unPlain ::
38 {-curr-}PlainInh d ->
39 {-curr-}PlainState d ->
40 {-ok-}( ({-prepend-}(d->d), {-new-}PlainState d) -> PlainFit d) ->
41 PlainFit d
42 -- NOTE: equivalent to:
43 -- ReaderT PlainInh (StateT (PlainState d) (Cont (PlainFit d))) (d->d)
44 }
45 instance (Show d, Spaceable d) => Show (Plain d) where
46 show = show . runPlain
47
48 runPlain :: Spaceable d => Plain d -> d
49 runPlain x =
50 unPlain x
51 defPlainInh
52 defPlainState
53 {-k-}(\(px,_sx) fits _overflow ->
54 -- NOTE: if px fits, then appending mempty fits
55 fits (px mempty) )
56 {-fits-}id
57 {-overflow-}id
58
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'
64 -- must be written.
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'.
71 } deriving (Show)
72
73 defPlainState :: PlainState d
74 defPlainState = PlainState
75 { plainState_buffer = mempty
76 , plainState_bufferStart = 0
77 , plainState_bufferWidth = 0
78 , plainState_breakIndent = 0
79 }
80
81 -- ** Type 'PlainInh'
82 data PlainInh d = PlainInh
83 { plainInh_width :: !(Maybe Column)
84 , plainInh_justify :: !Bool
85 , plainInh_indent :: !Indent
86 , plainInh_indenting :: !(Plain d)
87 }
88
89 defPlainInh :: Spaceable d => PlainInh d
90 defPlainInh = PlainInh
91 { plainInh_width = Nothing
92 , plainInh_justify = False
93 , plainInh_indent = 0
94 , plainInh_indenting = mempty
95 }
96
97 -- ** Type 'PlainFit'
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) ->
104 d
105
106 -- ** Type 'PlainChunk'
107 data PlainChunk d
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
116 showsPrec p x =
117 showParen (p>10) $
118 case x of
119 PlainChunk_Ignored d ->
120 showString "Z " .
121 showsPrec 11 d
122 PlainChunk_Word (Word d) ->
123 showString "W " .
124 showsPrec 11 d
125 PlainChunk_Spaces s ->
126 showString "S " .
127 showsPrec 11 s
128 instance Lengthable d => Lengthable (PlainChunk d) where
129 width = \case
130 PlainChunk_Ignored{} -> 0
131 PlainChunk_Word d -> width d
132 PlainChunk_Spaces s -> s
133 nullWidth = \case
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)
139
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
145
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) ->
150 k (px.py,sy)
151 instance Monoid d => Monoid (Plain d) where
152 mempty = Plain $ \_inh st k -> k (id,st)
153 mappend = (<>)
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 ->
158 unPlain
159 ( newlinePlain
160 <> indentPlain
161 <> propagatePlain (plainState_breakIndent st)
162 <> flushlinePlain
163 ) inh st
164 where
165 indentPlain = Plain $ \inh ->
166 unPlain
167 (plainInh_indenting inh)
168 inh{plainInh_justify=False}
169 newlinePlain = Plain $ \inh st k ->
170 k (\next ->
171 (if plainInh_justify inh
172 then joinLinePlainChunk $ List.reverse $ plainState_buffer st
173 else mempty
174 )<>newline<>next
175 , st
176 { plainState_bufferStart = 0
177 , plainState_bufferWidth = 0
178 , plainState_buffer = mempty
179 })
180 propagatePlain breakIndent = Plain $ \inh st1 k fits overflow ->
181 k (id,st1)
182 fits
183 {-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
191 then overflow
192 else fits
193 )
194 space = spaces 1
195 spaces n = Plain $ \inh st@PlainState{..} k fits overflow ->
196 let newWidth = plainState_bufferStart + plainState_bufferWidth + n in
197 if plainInh_justify inh
198 then
199 let newState = st
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
205 } in
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
210 else
211 let newState = st
212 { plainState_bufferWidth = plainState_bufferWidth + n
213 } in
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
222 if wordWidth <= 0
223 then k (id,st) fits overflow
224 else
225 let newBufferWidth = plainState_bufferWidth + wordWidth in
226 let newWidth = plainState_bufferStart + newBufferWidth in
227 if plainInh_justify inh
228 then
229 let newState = st
230 { plainState_buffer =
231 PlainChunk_Word (Word (from s)) :
232 plainState_buffer
233 , plainState_bufferWidth = newBufferWidth
234 } in
235 case plainInh_width inh of
236 Just maxWidth | maxWidth < newWidth ->
237 overflow $ k (id, newState) fits overflow
238 _ -> k (id, newState) fits overflow
239 else
240 let newState = st
241 { plainState_bufferWidth = newBufferWidth
242 } in
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
249 from =
250 mconcat .
251 List.intersperse breakspace .
252 (from <$>) .
253 words .
254 unLine
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
258 unPlain p inh
259 { plainInh_indent = col
260 , plainInh_indenting =
261 if plainInh_indent inh <= col
262 then
263 plainInh_indenting inh <>
264 spaces (col`minusNatural`plainInh_indent inh)
265 else spaces col
266 } st
267 setIndent d i p = Plain $ \inh ->
268 unPlain p inh
269 { plainInh_indent = i
270 , plainInh_indenting = d
271 }
272 incrIndent d i p = Plain $ \inh ->
273 unPlain p inh
274 { plainInh_indent = plainInh_indent inh + i
275 , plainInh_indenting = plainInh_indenting inh <> d
276 }
277
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
282 unPlain
283 (if col <= maxCol
284 then spaces (maxCol`minusNatural`col)
285 else mempty)
286 inh1 st1
287 in
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
293 unPlain
294 (case col`compare`maxCol of
295 LT -> spaces (maxCol`minusNatural`col)
296 EQ -> mempty
297 GT -> incrIndent (spaces m) m newline
298 ) inh1 st1
299 in
300 unPlain (p <> p1) inh0 st0
301 instance (Spaceable d, From (Word Char) d, From (Word String) d) => Listable (Plain d) where
302 ul ds =
303 catV $
304 (<$> ds) $ \d ->
305 from (Word '-')<>space<>flushlinePlain<>align d<>flushlinePlain
306 ol ds =
307 catV $ snd $
308 Fold.foldr
309 (\d (i, acc) ->
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}
315
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)) <>)
320 , st
321 { plainState_bufferStart = plainState_bufferStart st + plainState_bufferWidth st
322 , plainState_bufferWidth = 0
323 , plainState_buffer = mempty
324 }
325 )
326
327 collapsePlainChunkSpaces :: PlainChunk d -> PlainChunk d
328 collapsePlainChunkSpaces = \case
329 PlainChunk_Spaces s -> PlainChunk_Spaces (if s > 0 then 1 else 0)
330 x -> x
331
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})
337 fits
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 <>)
341 , st
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
350 }
351 )
352 fits
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)
361 where
362 dummyK (px,_sx) fits _overflow =
363 -- NOTE: if px fits, then appending mempty fits
364 fits (px mempty)
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
369 Just w ->
370 let newState = st
371 { plainState_bufferWidth = plainState_bufferWidth st + w
372 } in
373 k (id,newState) fits fits
374
375 -- | Like 'newline', but justify 'plainState_buffer' before.
376 newlineJustifyingPlain :: Spaceable d => Plain d
377 newlineJustifyingPlain = Plain $ \inh st ->
378 unPlain
379 ( newlinePlain
380 <> indentPlain
381 <> propagatePlain (plainState_breakIndent st)
382 <> flushlinePlain
383 ) inh st
384 where
385 indentPlain = Plain $ \inh ->
386 unPlain
387 (plainInh_indenting inh)
388 inh{plainInh_justify=False}
389 newlinePlain = Plain $ \inh st k ->
390 k (\next ->
391 (if plainInh_justify inh
392 then justifyLinePlain inh st
393 else mempty
394 )<>newline<>next
395 , st
396 { plainState_bufferStart = 0
397 , plainState_bufferWidth = 0
398 , plainState_buffer = mempty
399 })
400 propagatePlain breakIndent = Plain $ \inh st1 k fits overflow ->
401 k (id,st1)
402 fits
403 {-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
411 then overflow
412 else fits
413 )
414
415 -- String
416 instance (From (Word String) d, Spaceable d) =>
417 From String (Plain d) where
418 from =
419 mconcat .
420 List.intersperse newline .
421 (from <$>) .
422 lines
423 instance (From (Word String) d, Spaceable d) =>
424 IsString (Plain d) where
425 fromString = from
426 -- Text
427 instance (From (Word Text) d, Spaceable d) =>
428 From Text (Plain d) where
429 from =
430 mconcat .
431 List.intersperse newline .
432 (from <$>) .
433 lines
434 instance (From (Word TL.Text) d, Spaceable d) =>
435 From TL.Text (Plain d) where
436 from =
437 mconcat .
438 List.intersperse newline .
439 (from <$>) .
440 lines
441 -- Char
442 instance (From (Word Char) d, Spaceable d) =>
443 From Char (Plain d) where
444 from ' ' = breakspace
445 from '\n' = newline
446 from c = from (Word c)
447
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)
453
454 justifyLinePlain ::
455 Spaceable d =>
456 PlainInh d -> PlainState d -> d
457 justifyLinePlain inh PlainState{..} =
458 case plainInh_width inh of
459 Nothing -> joinLinePlainChunk $ List.reverse plainState_buffer
460 Just maxWidth ->
461 if maxWidth < plainState_bufferStart
462 || maxWidth < plainInh_indent inh
463 then joinLinePlainChunk $ List.reverse plainState_buffer
464 else
465 let superfluousSpaces = Fold.foldr
466 (\c acc ->
467 acc + case c of
468 PlainChunk_Ignored{} -> 0
469 PlainChunk_Word{} -> 0
470 PlainChunk_Spaces s -> s`minusNatural`(min 1 s))
471 0 plainState_buffer in
472 let minBufferWidth =
473 -- NOTE: cap the spaces at 1,
474 -- to let justifyWidth decide where to add spaces.
475 plainState_bufferWidth`minusNatural`superfluousSpaces in
476 let justifyWidth =
477 -- NOTE: when minBufferWidth is not breakable,
478 -- the width of justification can be wider than
479 -- what remains to reach maxWidth.
480 max minBufferWidth $
481 maxWidth`minusNatural`plainState_bufferStart
482 in
483 let wordCount = countWordsPlain plainState_buffer in
484 unLine $ padLinePlainChunkInits justifyWidth $
485 (minBufferWidth,wordCount,List.reverse plainState_buffer)
486
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
491 where
492 go inWord acc = \case
493 [] -> acc
494 PlainChunk_Word{}:xs ->
495 if inWord
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
502
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)@.
507 --
508 -- A simple implementation of 'justifyPadding' could be:
509 -- @
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
514 -- where
515 -- (q,r) = a`divMod`b
516 -- m = 'min' (b-r) r
517 -- @
518 justifyPadding :: Natural -> Natural -> [Natural]
519 justifyPadding a b = go r (b-r) -- NOTE: r >= 0 && b-r >= 0 due to 'divMod'
520 where
521 (q,r) = a`quotRemNatural`b
522
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)
526
527 padLinePlainChunkInits ::
528 Spaceable d =>
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.
534 || wordCount <= 1
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
539 else
540 -- Share the missing spaces as evenly as possible
541 -- between the words of the line.
542 padLinePlainChunk line $ justifyPadding (maxWidth-lineWidth) (wordCount-1)
543
544 -- | Just concat 'PlainChunk's with no justification.
545 joinLinePlainChunk :: Monoid d => Spaceable d => [PlainChunk d] -> d
546 joinLinePlainChunk = mconcat . (runPlainChunk <$>)
547
548 -- | Interleave 'PlainChunk's with 'Width's from 'justifyPadding'.
549 padLinePlainChunk :: Spaceable d => [PlainChunk d] -> [Width] -> d
550 padLinePlainChunk = go
551 where
552 go (w:ws) lls@(l:ls) =
553 case w of
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 []
557 go [] _ls = mempty