symantic-document-1.5.2.20200320: src/Symantic/Document/Plain.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE UndecidableInstances #-}
module Symantic.Document.Plain where
import Control.Monad (Monad(..))
import Data.Bool
import Data.Char (Char)
import Data.Eq (Eq(..))
import Data.Function (($), (.), id)
import Data.Functor ((<$>))
import Data.Maybe (Maybe(..))
import Data.Monoid (Monoid(..))
import Data.Ord (Ord(..), Ordering(..))
import Data.Semigroup (Semigroup(..))
import Data.String (String, IsString(..))
import Data.Text (Text)
import Data.Tuple (snd)
import GHC.Natural (minusNatural,minusNaturalMaybe,quotRemNatural)
import Numeric.Natural (Natural)
import Prelude (fromIntegral, Num(..), pred)
import System.Console.ANSI hiding (SGR)
import Text.Show (Show(..), showString, showParen)
import qualified Data.Foldable as Fold
import qualified Data.List as List
import qualified Data.Text.Lazy as TL
import Symantic.Document.Lang
-- * Type 'Plain'
-- | Church encoded for performance concerns.
-- Kind like 'ParsecT' in @megaparsec@ but a little bit different
-- due to the use of 'PlainFit' for implementing 'breakingSpace' correctly
-- when in the left hand side of ('<>').
-- Prepending is done using continuation, like in a difference list.
newtype Plain d = Plain
{ unPlain ::
{-curr-}PlainInh d ->
{-curr-}PlainState d ->
{-ok-}( ({-prepend-}(d->d), {-new-}PlainState d) -> PlainFit d) ->
PlainFit d
-- NOTE: equivalent to:
-- ReaderT PlainInh (StateT (PlainState d) (Cont (PlainFit d))) (d->d)
}
instance (Show d, Spaceable d) => Show (Plain d) where
show = show . runPlain
runPlain :: Spaceable d => Plain d -> d
runPlain x =
unPlain x
defPlainInh
defPlainState
{-k-}(\(px,_sx) fits _overflow ->
-- NOTE: if px fits, then appending mempty fits
fits (px mempty) )
{-fits-}id
{-overflow-}id
-- ** Type 'PlainState'
data PlainState d = PlainState
{ plainState_buffer :: ![PlainChunk d]
, plainState_bufferStart :: !Column
-- ^ The 'Column' from which the 'plainState_buffer'
-- must be written.
, plainState_bufferWidth :: !Width
-- ^ The 'Width' of the 'plainState_buffer' so far.
, plainState_breakIndent :: !Indent
-- ^ The amount of 'Indent' added by 'breakspace'
-- that can be reached by breaking the 'space'
-- into a 'newlineJustifyingPlain'.
} deriving (Show)
defPlainState :: PlainState d
defPlainState = PlainState
{ plainState_buffer = mempty
, plainState_bufferStart = 0
, plainState_bufferWidth = 0
, plainState_breakIndent = 0
}
-- ** Type 'PlainInh'
data PlainInh d = PlainInh
{ plainInh_width :: !(Maybe Column)
, plainInh_justify :: !Bool
, plainInh_indent :: !Indent
, plainInh_indenting :: !(Plain d)
, plainInh_sgr :: ![SGR]
}
defPlainInh :: Spaceable d => PlainInh d
defPlainInh = PlainInh
{ plainInh_width = Nothing
, plainInh_justify = False
, plainInh_indent = 0
, plainInh_indenting = mempty
, plainInh_sgr = []
}
-- ** Type 'PlainFit'
-- | Double continuation to qualify the returned document
-- as fitting or overflowing the given 'plainInh_width'.
-- It's like @('Bool',d)@ in a normal style
-- (a non continuation-passing-style).
type PlainFit d = {-fits-}(d -> d) ->
{-overflow-}(d -> d) ->
d
-- ** Type 'PlainChunk'
data PlainChunk d
= PlainChunk_Ignored !d
-- ^ Ignored by the justification but kept in place.
-- Used for instance to put ANSI sequences.
| PlainChunk_Word !(Word d)
| PlainChunk_Spaces !Width
-- ^ 'spaces' preserved to be interleaved
-- correctly with 'PlainChunk_Ignored'.
instance Show d => Show (PlainChunk d) where
showsPrec p x =
showParen (p>10) $
case x of
PlainChunk_Ignored d ->
showString "Z " .
showsPrec 11 d
PlainChunk_Word (Word d) ->
showString "W " .
showsPrec 11 d
PlainChunk_Spaces s ->
showString "S " .
showsPrec 11 s
instance Lengthable d => Lengthable (PlainChunk d) where
width = \case
PlainChunk_Ignored{} -> 0
PlainChunk_Word d -> width d
PlainChunk_Spaces s -> s
nullWidth = \case
PlainChunk_Ignored{} -> True
PlainChunk_Word d -> nullWidth d
PlainChunk_Spaces s -> s == 0
instance From [SGR] d => From [SGR] (PlainChunk d) where
from sgr = PlainChunk_Ignored (from sgr)
runPlainChunk :: Spaceable d => PlainChunk d -> d
runPlainChunk = \case
PlainChunk_Ignored d -> d
PlainChunk_Word (Word d) -> d
PlainChunk_Spaces s -> spaces s
instance Semigroup d => Semigroup (Plain d) where
Plain x <> Plain y = Plain $ \inh st k ->
x inh st $ \(px,sx) ->
y inh sx $ \(py,sy) ->
k (px.py,sy)
instance Monoid d => Monoid (Plain d) where
mempty = Plain $ \_inh st k -> k (id,st)
mappend = (<>)
instance Spaceable d => Spaceable (Plain d) where
-- | The default 'newline' does not justify 'plainState_buffer',
-- for that use 'newlineJustifyingPlain'.
newline = Plain $ \inh st ->
unPlain
( newlinePlain
<> indentPlain
<> propagatePlain (plainState_breakIndent st)
<> flushlinePlain
) inh st
where
indentPlain = Plain $ \inh ->
unPlain
(plainInh_indenting inh)
inh{plainInh_justify=False}
newlinePlain = Plain $ \inh st k ->
k (\next ->
(if plainInh_justify inh
then joinLinePlainChunk $ List.reverse $ plainState_buffer st
else mempty
)<>newline<>next
, st
{ plainState_bufferStart = 0
, plainState_bufferWidth = 0
, plainState_buffer = mempty
})
propagatePlain breakIndent = Plain $ \inh st1 k fits overflow ->
k (id,st1)
fits
{-overflow-}(
-- NOTE: the text after this newline overflows,
-- so propagate the overflow before this 'newline',
-- if and only if there is a 'breakspace' before this 'newline'
-- whose replacement by a 'newline' indents to a lower indent
-- than this 'newline''s indent.
-- Otherwise there is no point in propagating the overflow.
if breakIndent < plainInh_indent inh
then overflow
else fits
)
space = spaces 1
spaces n = Plain $ \inh st@PlainState{..} k fits overflow ->
let newWidth = plainState_bufferStart + plainState_bufferWidth + n in
if plainInh_justify inh
then
let newState = st
{ plainState_buffer =
case plainState_buffer of
PlainChunk_Spaces s:buf -> PlainChunk_Spaces (s+n):buf
buf -> PlainChunk_Spaces n:buf
, plainState_bufferWidth = plainState_bufferWidth + n
} in
case plainInh_width inh of
Just maxWidth | maxWidth < newWidth ->
overflow $ k (id{-(d<>)-}, newState) fits overflow
_ -> k (id{-(d<>)-}, newState) fits overflow
else
let newState = st
{ plainState_bufferWidth = plainState_bufferWidth + n
} in
case plainInh_width inh of
Just maxWidth | maxWidth < newWidth ->
overflow $ k ((spaces n <>), newState) fits fits
_ -> k ((spaces n <>), newState) fits overflow
instance (From (Word s) d, Semigroup d, Lengthable s) =>
From (Word s) (Plain d) where
from s = Plain $ \inh st@PlainState{..} k fits overflow ->
let wordWidth = width s in
if wordWidth <= 0
then k (id,st) fits overflow
else
let newBufferWidth = plainState_bufferWidth + wordWidth in
let newWidth = plainState_bufferStart + newBufferWidth in
if plainInh_justify inh
then
let newState = st
{ plainState_buffer =
PlainChunk_Word (Word (from s)) :
plainState_buffer
, plainState_bufferWidth = newBufferWidth
} in
case plainInh_width inh of
Just maxWidth | maxWidth < newWidth ->
overflow $ k (id, newState) fits overflow
_ -> k (id, newState) fits overflow
else
let newState = st
{ plainState_bufferWidth = newBufferWidth
} in
case plainInh_width inh of
Just maxWidth | maxWidth < newWidth ->
overflow $ k ((from s <>), newState) fits fits
_ -> k ((from s <>), newState) fits overflow
instance (From (Word s) d, Lengthable s, Spaceable d, Splitable s) =>
From (Line s) (Plain d) where
from =
mconcat .
List.intersperse breakspace .
(from <$>) .
words .
unLine
instance Spaceable d => Indentable (Plain d) where
align p = (flushlinePlain <>) $ Plain $ \inh st ->
let col = plainState_bufferStart st + plainState_bufferWidth st in
unPlain p inh
{ plainInh_indent = col
, plainInh_indenting =
if plainInh_indent inh <= col
then
plainInh_indenting inh <>
spaces (col`minusNatural`plainInh_indent inh)
else spaces col
} st
setIndent d i p = Plain $ \inh ->
unPlain p inh
{ plainInh_indent = i
, plainInh_indenting = d
}
incrIndent d i p = Plain $ \inh ->
unPlain p inh
{ plainInh_indent = plainInh_indent inh + i
, plainInh_indenting = plainInh_indenting inh <> d
}
fill m p = Plain $ \inh0 st0 ->
let maxCol = plainState_bufferStart st0 + plainState_bufferWidth st0 + m in
let p1 = Plain $ \inh1 st1 ->
let col = plainState_bufferStart st1 + plainState_bufferWidth st1 in
unPlain
(if col <= maxCol
then spaces (maxCol`minusNatural`col)
else mempty)
inh1 st1
in
unPlain (p <> p1) inh0 st0
fillOrBreak m p = Plain $ \inh0 st0 ->
let maxCol = plainState_bufferStart st0 + plainState_bufferWidth st0 + m in
let p1 = Plain $ \inh1 st1 ->
let col = plainState_bufferStart st1 + plainState_bufferWidth st1 in
unPlain
(case col`compare`maxCol of
LT -> spaces (maxCol`minusNatural`col)
EQ -> mempty
GT -> incrIndent (spaces m) m newline
) inh1 st1
in
unPlain (p <> p1) inh0 st0
instance (Spaceable d, From (Word Char) d, From (Word String) d) => Listable (Plain d) where
ul ds =
catV $
(<$> ds) $ \d ->
from (Word '-')<>space<>flushlinePlain<>align d{-<>flushlinePlain-}
ol ds =
catV $ snd $
Fold.foldr
(\d (i, acc) ->
(pred i, (from i<>from (Word '.')<>space<>flushlinePlain<>align d{-<>flushlinePlain-}) : acc)
) (Fold.length ds, []) ds
instance Spaceable d => Justifiable (Plain d) where
justify p = (\x -> flushlinePlain <> x <> flushlinePlain) $ Plain $ \inh ->
unPlain p inh{plainInh_justify=True}
-- | Commit 'plainState_buffer' upto there, so that it won't be justified.
flushlinePlain :: Spaceable d => Plain d
flushlinePlain = Plain $ \_inh st k ->
k( (joinLinePlainChunk (collapsePlainChunkSpaces <$> List.reverse (plainState_buffer st)) <>)
, st
{ plainState_bufferStart = plainState_bufferStart st + plainState_bufferWidth st
, plainState_bufferWidth = 0
, plainState_buffer = mempty
}
)
collapsePlainChunkSpaces :: PlainChunk d -> PlainChunk d
collapsePlainChunkSpaces = \case
PlainChunk_Spaces s -> PlainChunk_Spaces (if s > 0 then 1 else 0)
x -> x
instance Spaceable d => Wrappable (Plain d) where
setWidth w p = Plain $ \inh ->
unPlain p inh{plainInh_width=w}
breakpoint = Plain $ \inh st k fits overflow ->
k(id, st {plainState_breakIndent = plainInh_indent inh})
fits
{-overflow-}(\_r -> unPlain newlineJustifyingPlain inh st k fits overflow)
breakspace = Plain $ \inh st k fits overflow ->
k( if plainInh_justify inh then id else (space <>)
, st
{ plainState_buffer =
if plainInh_justify inh
then case plainState_buffer st of
PlainChunk_Spaces s:bs -> PlainChunk_Spaces (s+1):bs
bs -> PlainChunk_Spaces 1:bs
else plainState_buffer st
, plainState_bufferWidth = plainState_bufferWidth st + 1
, plainState_breakIndent = plainInh_indent inh
}
)
fits
{-overflow-}(\_r -> unPlain newlineJustifyingPlain inh st k fits overflow)
breakalt x y = Plain $ \inh st k fits overflow ->
-- NOTE: breakalt must be y if and only if x does not fit,
-- hence the use of dummyK to limit the test
-- to overflows raised within x, and drop those raised after x.
unPlain x inh st dummyK
{-fits-} (\_r -> unPlain x inh st k fits overflow)
{-overflow-}(\_r -> unPlain y inh st k fits overflow)
where
dummyK (px,_sx) fits _overflow =
-- NOTE: if px fits, then appending mempty fits
fits (px mempty)
endline = Plain $ \inh st k fits _overflow ->
let col = plainState_bufferStart st + plainState_bufferWidth st in
case plainInh_width inh >>= (`minusNaturalMaybe` col) of
Nothing -> k (id, st) fits fits
Just w ->
let newState = st
{ plainState_bufferWidth = plainState_bufferWidth st + w
} in
k (id,newState) fits fits
-- | Like 'newline', but justify 'plainState_buffer' before.
newlineJustifyingPlain :: Spaceable d => Plain d
newlineJustifyingPlain = Plain $ \inh st ->
unPlain
( newlinePlain
<> indentPlain
<> propagatePlain (plainState_breakIndent st)
<> flushlinePlain
) inh st
where
indentPlain = Plain $ \inh ->
unPlain
(plainInh_indenting inh)
inh{plainInh_justify=False}
newlinePlain = Plain $ \inh st k ->
k (\next ->
(if plainInh_justify inh
then justifyLinePlain inh st
else mempty
)<>newline<>next
, st
{ plainState_bufferStart = 0
, plainState_bufferWidth = 0
, plainState_buffer = mempty
})
propagatePlain breakIndent = Plain $ \inh st1 k fits overflow ->
k (id,st1)
fits
{-overflow-}(
-- NOTE: the text after this newline overflows,
-- so propagate the overflow before this 'newline',
-- if and only if there is a 'breakspace' before this 'newline'
-- whose replacement by a 'newline' indents to a lower indent
-- than this 'newline''s indent.
-- Otherwise there is no point in propagating the overflow.
if breakIndent < plainInh_indent inh
then overflow
else fits
)
-- String
instance (From (Word String) d, Spaceable d) =>
From String (Plain d) where
from =
mconcat .
List.intersperse newline .
(from <$>) .
lines
instance (From (Word String) d, Spaceable d) =>
IsString (Plain d) where
fromString = from
-- Text
instance (From (Word Text) d, Spaceable d) =>
From Text (Plain d) where
from =
mconcat .
List.intersperse newline .
(from <$>) .
lines
instance (From (Word TL.Text) d, Spaceable d) =>
From TL.Text (Plain d) where
from =
mconcat .
List.intersperse newline .
(from <$>) .
lines
-- Char
instance (From (Word Char) d, Spaceable d) =>
From Char (Plain d) where
from ' ' = breakspace
from '\n' = newline
from c = from (Word c)
instance (From [SGR] d, Semigroup d) => From [SGR] (Plain d) where
from sgr = Plain $ \inh st k ->
if plainInh_justify inh
then k (id, st {plainState_buffer = PlainChunk_Ignored (from sgr) : plainState_buffer st})
else k ((from sgr <>), st)
-- * Justifying
justifyLinePlain ::
Spaceable d =>
PlainInh d -> PlainState d -> d
justifyLinePlain inh PlainState{..} =
case plainInh_width inh of
Nothing -> joinLinePlainChunk $ List.reverse plainState_buffer
Just maxWidth ->
if maxWidth < plainState_bufferStart
|| maxWidth < plainInh_indent inh
then joinLinePlainChunk $ List.reverse plainState_buffer
else
let superfluousSpaces = Fold.foldr
(\c acc ->
acc + case c of
PlainChunk_Ignored{} -> 0
PlainChunk_Word{} -> 0
PlainChunk_Spaces s -> s`minusNatural`(min 1 s))
0 plainState_buffer in
let minBufferWidth =
-- NOTE: cap the spaces at 1,
-- to let justifyWidth decide where to add spaces.
plainState_bufferWidth`minusNatural`superfluousSpaces in
let justifyWidth =
-- NOTE: when minBufferWidth is not breakable,
-- the width of justification can be wider than
-- what remains to reach maxWidth.
max minBufferWidth $
maxWidth`minusNatural`plainState_bufferStart
in
let wordCount = countWordsPlain plainState_buffer in
unLine $ padLinePlainChunkInits justifyWidth $
(minBufferWidth,wordCount,List.reverse plainState_buffer)
-- | @('countWordsPlain' ps)@ returns the number of words in @(ps)@
-- clearly separated by spaces.
countWordsPlain :: [PlainChunk d] -> Natural
countWordsPlain = go False 0
where
go inWord acc = \case
[] -> acc
PlainChunk_Word{}:xs ->
if inWord
then go inWord acc xs
else go True (acc+1) xs
PlainChunk_Spaces s:xs
| s == 0 -> go inWord acc xs
| otherwise -> go False acc xs
PlainChunk_Ignored{}:xs -> go inWord acc xs
-- | @('justifyPadding' a b)@ returns the padding lengths
-- to reach @(a)@ in @(b)@ pads,
-- using the formula: @(a '==' m'*'(q '+' q'+'1) '+' ('r'-'m)'*'(q'+'1) '+' (b'-'r'-'m)'*'q)@
-- where @(q+1)@ and @(q)@ are the two padding lengths used and @(m = min (b-r) r)@.
--
-- A simple implementation of 'justifyPadding' could be:
-- @
-- 'justifyPadding' a b =
-- 'join' ('List.replicate' m [q,q'+'1])
-- <> ('List.replicate' (r'-'m) (q'+'1)
-- <> ('List.replicate' ((b'-'r)'-'m) q
-- where
-- (q,r) = a`divMod`b
-- m = 'min' (b-r) r
-- @
justifyPadding :: Natural -> Natural -> [Natural]
justifyPadding a b = go r (b-r) -- NOTE: r >= 0 && b-r >= 0 due to 'divMod'
where
(q,r) = a`quotRemNatural`b
go 0 bmr = List.replicate (fromIntegral bmr) q -- when min (b-r) r == b-r
go rr 0 = List.replicate (fromIntegral rr) (q+1) -- when min (b-r) r == r
go rr bmr = q:(q+1) : go (rr`minusNatural`1) (bmr`minusNatural`1)
padLinePlainChunkInits ::
Spaceable d =>
Width -> (Natural, Natural, [PlainChunk d]) -> Line d
padLinePlainChunkInits maxWidth (lineWidth,wordCount,line) = Line $
if maxWidth <= lineWidth
-- The gathered line reached or overreached the maxWidth,
-- hence no padding id needed.
|| wordCount <= 1
-- The case maxWidth <= lineWidth && wordCount == 1
-- can happen if first word's length is < maxWidth
-- but second word's len is >= maxWidth.
then joinLinePlainChunk line
else
-- Share the missing spaces as evenly as possible
-- between the words of the line.
padLinePlainChunk line $ justifyPadding (maxWidth-lineWidth) (wordCount-1)
-- | Just concat 'PlainChunk's with no justification.
joinLinePlainChunk :: Monoid d => Spaceable d => [PlainChunk d] -> d
joinLinePlainChunk = mconcat . (runPlainChunk <$>)
-- | Interleave 'PlainChunk's with 'Width's from 'justifyPadding'.
padLinePlainChunk :: Spaceable d => [PlainChunk d] -> [Width] -> d
padLinePlainChunk = go
where
go (w:ws) lls@(l:ls) =
case w of
PlainChunk_Spaces _s -> spaces (fromIntegral (l+1)) <> go ws ls
_ -> runPlainChunk w <> go ws lls
go (w:ws) [] = runPlainChunk w <> go ws []
go [] _ls = mempty
-- * Escaping
instance (Semigroup d, From [SGR] d) => Colorable16 (Plain d) where
reverse = plainSGR $ SetSwapForegroundBackground True
black = plainSGR $ SetColor Foreground Dull Black
red = plainSGR $ SetColor Foreground Dull Red
green = plainSGR $ SetColor Foreground Dull Green
yellow = plainSGR $ SetColor Foreground Dull Yellow
blue = plainSGR $ SetColor Foreground Dull Blue
magenta = plainSGR $ SetColor Foreground Dull Magenta
cyan = plainSGR $ SetColor Foreground Dull Cyan
white = plainSGR $ SetColor Foreground Dull White
blacker = plainSGR $ SetColor Foreground Vivid Black
redder = plainSGR $ SetColor Foreground Vivid Red
greener = plainSGR $ SetColor Foreground Vivid Green
yellower = plainSGR $ SetColor Foreground Vivid Yellow
bluer = plainSGR $ SetColor Foreground Vivid Blue
magentaer = plainSGR $ SetColor Foreground Vivid Magenta
cyaner = plainSGR $ SetColor Foreground Vivid Cyan
whiter = plainSGR $ SetColor Foreground Vivid White
onBlack = plainSGR $ SetColor Background Dull Black
onRed = plainSGR $ SetColor Background Dull Red
onGreen = plainSGR $ SetColor Background Dull Green
onYellow = plainSGR $ SetColor Background Dull Yellow
onBlue = plainSGR $ SetColor Background Dull Blue
onMagenta = plainSGR $ SetColor Background Dull Magenta
onCyan = plainSGR $ SetColor Background Dull Cyan
onWhite = plainSGR $ SetColor Background Dull White
onBlacker = plainSGR $ SetColor Background Vivid Black
onRedder = plainSGR $ SetColor Background Vivid Red
onGreener = plainSGR $ SetColor Background Vivid Green
onYellower = plainSGR $ SetColor Background Vivid Yellow
onBluer = plainSGR $ SetColor Background Vivid Blue
onMagentaer = plainSGR $ SetColor Background Vivid Magenta
onCyaner = plainSGR $ SetColor Background Vivid Cyan
onWhiter = plainSGR $ SetColor Background Vivid White
instance (Semigroup d, From [SGR] d) => Decorable (Plain d) where
bold = plainSGR $ SetConsoleIntensity BoldIntensity
underline = plainSGR $ SetUnderlining SingleUnderline
italic = plainSGR $ SetItalicized True
plainSGR ::
Semigroup d =>
From [SGR] d =>
SGR -> Plain d -> Plain d
plainSGR newSGR p = before <> middle <> after
where
before = Plain $ \inh st k ->
let d = from [newSGR] in
if plainInh_justify inh
then k (id, st
{ plainState_buffer =
PlainChunk_Ignored d :
plainState_buffer st
})
else k ((d <>), st)
middle = Plain $ \inh ->
unPlain p inh{plainInh_sgr=newSGR:plainInh_sgr inh}
after = Plain $ \inh st k ->
let d = from $ Reset : List.reverse (plainInh_sgr inh) in
if plainInh_justify inh
then k (id, st
{ plainState_buffer =
PlainChunk_Ignored d :
plainState_buffer st
})
else k ((d <>), st)